Lead Software Engineer @ EPAM Anywhere
2016-2022 - EPAM Belarus
2022-now - EPAM Uzbekistan
Node.js - AWS
@ShukhratBek26
shuhratbek
shuhratbek.26@gmail.com
Declarative Configuration
Version Control
Automation
Consistency and Reproducibility
Scalability and Elasticity
Collaboration and DevOps Practices
import { App, Stack, StackProps } from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
class HelloCdkStack extends Stack {
constructor(scope: App, id: string, props?: StackProps) {
super(scope, id, props);
new s3.Bucket(this, 'MyFirstBucket', {
versioned: true
});
}
}
const app = new App();
new HelloCdkStack(app, "HelloCdkStack");
1. AWS CloudFormation Resources (L1 Constructs)
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
class MyStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new s3.CfnBucket(this, 'MyBucket', {
bucketName: 'my-bucket-name'
});
}
}
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
class MyStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new s3.Bucket(this, 'MyBucket', {
bucketName: 'my-bucket-name'
});
}
}
import * as cdk from 'aws-cdk-lib';
import { StaticWebsite } from 'aws-cdk-lib/aws-s3-deployment';
class MyStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new StaticWebsite(this, 'MyStaticWebsite', {
sourcePath: 'path/to/your/static/files',
indexDoc: 'index.html',
errorDoc: 'error.html'
});
}
}
2. Higher-Level AWS Constructs (L2 Constructs)
3. Patterns (L3 Constructs)
import { App, Stack, StackProps } from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
class HelloCdkStack extends Stack {
constructor(scope: App, id: string, props?: StackProps) {
super(scope, id, props);
new s3.Bucket(this, 'MyFirstBucket', {
versioned: true
});
}
}
const app = new App();
new HelloCdkStack(app, "HelloCdkStack");
import { App, Stack, StackProps } from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
class HelloCdkStack extends Stack {
constructor(scope: App, id: string, props?: StackProps) {
super(scope, id, props);
new s3.Bucket(this, 'MyFirstBucket', {
versioned: true
});
}
}
const app = new App();
new HelloCdkStack(app, "HelloCdkStack");
npm install -g aws-cdk
Start simple and add complexity only when you need it
Align with the AWS Well-Architected Framework
Every application starts with a single package in a single repository
Move code into repositories based on code lifecycle or team ownership
Infrastructure and runtime code live in the same package
Model with constructs, deploy with stacks
Configure with properties and methods, not environment variables
Unit test your infrastructure
Don't change the logical ID of stateful resources
Constructs aren't enough for compliance
Make decisions at synthesis time
Use generated resource names, not physical names
Define removal policies and log retention
Separate your application into multiple stacks as dictated by deployment requirements
Commit cdk.context.json
to avoid non-deterministic behavior
Let the AWS CDK manage roles and security groups
Model all production stages in code
Measure everything
import { Capture, Match, Template } from "aws-cdk-lib/assertions";
import * as cdk from "aws-cdk-lib";
import * as sns from "aws-cdk-lib/aws-sns";
import { StateMachineStack } from "../lib/state-machine-stack";
describe("StateMachineStack", () => {
test("synthesizes the way we expect", () => {
const app = new cdk.App();
// Create the StateMachineStack.
const stateMachineStack = new StateMachineStack(app, "StateMachineStack");
// Prepare the stack for assertions.
const template = Template.fromStack(stateMachineStack);
template.hasResourceProperties("AWS::Lambda::Function", {
Handler: "handler",
Runtime: "nodejs14.x",
});
// Creates the subscription...
template.resourceCountIs("AWS::SNS::Subscription", 1);
// Capture some data from the state machine's definition.
const startAtCapture = new Capture();
const statesCapture = new Capture();
template.hasResourceProperties("AWS::StepFunctions::StateMachine", {
DefinitionString: Match.serializedJson(
Match.objectLike({
StartAt: startAtCapture,
States: statesCapture,
})
),
});
// Assert that the start state starts with "Start".
expect(startAtCapture.asString()).toEqual(expect.stringMatching(/^Start/));
// Assert that the start state actually exists in the states object of the
// state machine definition.
expect(statesCapture.asObject()).toHaveProperty(startAtCapture.asString());
}
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
import * as targets from 'aws-cdk-lib/aws-route53-targets';
import * as cloudfront_origins from 'aws-cdk-lib/aws-cloudfront-origins';
import { CfnOutput, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
export interface StaticSiteProps {
domainName: string;
siteSubDomain: string;
}
export class StaticSite extends Construct {
constructor(parent: Stack, name: string, props: StaticSiteProps) {
super(parent, name);
const zone = route53.HostedZone.fromLookup(this, 'Zone', { domainName: props.domainName });
const siteDomain = props.siteSubDomain + '.' + props.domainName;
const cloudfrontOAI = new cloudfront.OriginAccessIdentity(this, 'cloudfront-OAI', {
comment: `OAI for ${name}`
});
new CfnOutput(this, 'Site', { value: 'https://' + siteDomain });
// Content bucket
const siteBucket = new s3.Bucket(this, 'SiteBucket', {
bucketName: siteDomain,
publicReadAccess: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: RemovalPolicy.DESTROY, // NOT recommended for production code
autoDeleteObjects: true, // NOT recommended for production code
});
// Grant access to cloudfront
siteBucket.addToResourcePolicy(new iam.PolicyStatement({
actions: ['s3:GetObject'],
resources: [siteBucket.arnForObjects('*')],
principals: [new iam.CanonicalUserPrincipal(cloudfrontOAI.cloudFrontOriginAccessIdentityS3CanonicalUserId)]
}));
new CfnOutput(this, 'Bucket', { value: siteBucket.bucketName });
stack 1/2
// TLS certificate
const certificate = new acm.Certificate(this, 'SiteCertificate', {
domainName: siteDomain,
validation: acm.CertificateValidation.fromDns(zone),
});
new CfnOutput(this, 'Certificate', { value: certificate.certificateArn });
// CloudFront distribution
const distribution = new cloudfront.Distribution(this, 'SiteDistribution', {
certificate: certificate,
defaultRootObject: "index.html",
domainNames: [siteDomain],
minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
errorResponses:[
{
httpStatus: 403,
responseHttpStatus: 403,
responsePagePath: '/error.html',
ttl: Duration.minutes(30),
}
],
defaultBehavior: {
origin: new cloudfront_origins.S3Origin(siteBucket, {originAccessIdentity: cloudfrontOAI}),
compress: true,
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
}
})
new CfnOutput(this, 'DistributionId', { value: distribution.distributionId });
// Route53 alias record for the CloudFront distribution
new route53.ARecord(this, 'SiteAliasRecord', {
recordName: siteDomain,
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)),
zone
});
// Deploy site contents to S3 bucket
new s3deploy.BucketDeployment(this, 'DeployWithInvalidation', {
sources: [s3deploy.Source.asset('./site-contents')],
destinationBucket: siteBucket,
distribution,
distributionPaths: ['/*'],
});
}
}
stack 2/2
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { StaticSite } from './static-site';
/**
* This stack relies on getting the domain name from CDK context.
* Use 'cdk synth -c domain=mystaticsite.com -c subdomain=www'
* Or add the following to cdk.json:
* {
* "context": {
* "domain": "mystaticsite.com",
* "subdomain": "www",
* "accountId": "1234567890",
* }
* }
**/
class MyStaticSiteStack extends cdk.Stack {
constructor(parent: cdk.App, name: string, props: cdk.StackProps) {
super(parent, name, props);
new StaticSite(this, 'StaticSite', {
domainName: this.node.tryGetContext('domain'),
siteSubDomain: this.node.tryGetContext('subdomain'),
});
}
}
const app = new cdk.App();
new MyStaticSiteStack(app, 'MyStaticSite', {
/**
* This is required for our use of hosted-zone lookup.
*
* Lookups do not work at all without an explicit environment
* specified; to use them, you must specify env.
* @see https://docs.aws.amazon.com/cdk/latest/guide/environments.html
*/
env: {
account: app.node.tryGetContext('accountId'),
/**
* Stack must be in us-east-1, because the ACM certificate for a
* global CloudFront distribution must be requested in us-east-1.
*/
region: 'us-east-1',
}
});
app.synth();
app
import { IResource, LambdaIntegration, MockIntegration, PassthroughBehavior, RestApi } from 'aws-cdk-lib/aws-apigateway';
import { AttributeType, Table } from 'aws-cdk-lib/aws-dynamodb';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { App, Stack, RemovalPolicy } from 'aws-cdk-lib';
import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
import { join } from 'path'
export class ApiLambdaCrudDynamoDBStack extends Stack {
constructor(app: App, id: string) {
super(app, id);
const dynamoTable = new Table(this, 'items', {
partitionKey: {
name: 'itemId',
type: AttributeType.STRING
},
tableName: 'items',
removalPolicy: RemovalPolicy.DESTROY, // NOT recommended for production code
});
const nodeJsFunctionProps: NodejsFunctionProps = {
bundling: {
externalModules: [
'aws-sdk', // Use the 'aws-sdk' available in the Lambda runtime
],
},
depsLockFilePath: join(__dirname, 'lambdas', 'package-lock.json'),
environment: {
PRIMARY_KEY: 'itemId',
TABLE_NAME: dynamoTable.tableName,
},
runtime: Runtime.NODEJS_14_X,
}
// Create a Lambda function for each of the CRUD operations
const getOneLambda = new NodejsFunction(this, 'getOneItemFunction', {
entry: join(__dirname, 'lambdas', 'get-one.ts'),
...nodeJsFunctionProps,
});
const getAllLambda = new NodejsFunction(this, 'getAllItemsFunction', {
entry: join(__dirname, 'lambdas', 'get-all.ts'),
...nodeJsFunctionProps,
});
const createOneLambda = new NodejsFunction(this, 'createItemFunction', {
entry: join(__dirname, 'lambdas', 'create.ts'),
...nodeJsFunctionProps,
});
const updateOneLambda = new NodejsFunction(this, 'updateItemFunction', {
entry: join(__dirname, 'lambdas', 'update-one.ts'),
...nodeJsFunctionProps,
});
const deleteOneLambda = new NodejsFunction(this, 'deleteItemFunction', {
entry: join(__dirname, 'lambdas', 'delete-one.ts'),
...nodeJsFunctionProps,
});
part 1/2
// Grant the Lambda function read access to the DynamoDB table
dynamoTable.grantReadWriteData(getAllLambda);
dynamoTable.grantReadWriteData(getOneLambda);
dynamoTable.grantReadWriteData(createOneLambda);
dynamoTable.grantReadWriteData(updateOneLambda);
dynamoTable.grantReadWriteData(deleteOneLambda);
// Integrate the Lambda functions with the API Gateway resource
const getAllIntegration = new LambdaIntegration(getAllLambda);
const createOneIntegration = new LambdaIntegration(createOneLambda);
const getOneIntegration = new LambdaIntegration(getOneLambda);
const updateOneIntegration = new LambdaIntegration(updateOneLambda);
const deleteOneIntegration = new LambdaIntegration(deleteOneLambda);
// Create an API Gateway resource for each of the CRUD operations
const api = new RestApi(this, 'itemsApi', { restApiName: 'Items Service' });
const items = api.root.addResource('items');
items.addMethod('GET', getAllIntegration);
items.addMethod('POST', createOneIntegration);
addCorsOptions(items);
const singleItem = items.addResource('{id}');
singleItem.addMethod('GET', getOneIntegration);
singleItem.addMethod('PATCH', updateOneIntegration);
singleItem.addMethod('DELETE', deleteOneIntegration);
addCorsOptions(singleItem);
}
}
export function addCorsOptions(apiResource: IResource) {
apiResource.addMethod('OPTIONS', new MockIntegration({
integrationResponses: [{
statusCode: '200',
responseParameters: {
'method.response.header.Access-Control-Allow-Headers': "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'",
'method.response.header.Access-Control-Allow-Origin': "'*'",
'method.response.header.Access-Control-Allow-Credentials': "'false'",
'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,GET,PUT,POST,DELETE'",
},
}],
passthroughBehavior: PassthroughBehavior.NEVER,
requestTemplates: {
"application/json": "{\"statusCode\": 200}"
},
}), {
methodResponses: [{
statusCode: '200',
responseParameters: {
'method.response.header.Access-Control-Allow-Headers': true,
'method.response.header.Access-Control-Allow-Methods': true,
'method.response.header.Access-Control-Allow-Credentials': true,
'method.response.header.Access-Control-Allow-Origin': true,
},
}]
})
}
part 2/2
export class EventBridgeLambdaStack extends cdk.Stack {
constructor(app: cdk.App, id: string) {
super(app, id);
// SNS Topic
const topic = new sns.Topic(this, 'Topic', {
displayName: 'Lambda SNS Topic',
});
//Email Variable
const emailaddress = new CfnParameter(this, "email", {
type: "String",
description: "The name of the Amazon S3 bucket where uploaded files will be stored."});
// Subscription to the topic
topic.addSubscription(new subscriptions.EmailSubscription(emailaddress.valueAsString));
// Lambda Function to publish message to SNS
const lambdaFn = new lambda.Function(this, 'Singleton', {
code: new lambda.InlineCode(fs.readFileSync('lambda-handler.py', { encoding: 'utf-8' })),
handler: 'index.main',
timeout: cdk.Duration.seconds(300),
runtime: lambda.Runtime.PYTHON_3_9,
environment: {'TOPIC_ARN': topic.topicArn}
});
// Run the eventbridge every minute
const rule = new events.Rule(this, 'Rule', {
schedule: events.Schedule.expression('cron(* * ? * * *)')
});
// Add the lambda function as a target to the eventbridge
rule.addTarget(new targets.LambdaFunction(lambdaFn));
// Add the permission to the lambda function to publish to SNS
const snsTopicPolicy = new iam.PolicyStatement({
actions: ['sns:publish'],
resources: ['*'],
});
// Add the permission to the lambda function to publish to SNS
lambdaFn.addToRolePolicy(snsTopicPolicy);
}
}