Mastering Infrastructure as Code with AWS CDK
Shukhratbek
Mamadaliev
Lead Software Engineer @ EPAM Anywhere
2016-2022 - EPAM Belarus
2022-now - EPAM Uzbekistan
Node.js - AWS
@ShukhratBek26
shuhratbek
shuhratbek.26@gmail.com
The Evolution of Infrastructure as Code
Manual Configuration
Scripting
Configuration Management Tools
IaC Frameworks
Infrastructure as Code as a Service
Benefits
-
Declarative Configuration
-
Version Control
-
Automation
-
Consistency and Reproducibility
-
Scalability and Elasticity
-
Collaboration and DevOps Practices
Mastering AWS CDK
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");
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)
Stacks
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");
Apps
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");
AWS CDK CLI
- cdk init
- cdk deploy
- cdk watch
- cdk destroy
npm install -g aws-cdk
- cdk list
- cdk bootstrap
- cdk diff
- cdk synth
- cdk metadata
- cdk context
- cdk docs
- cdk doctor
App lifecycle
Best Practices for Success
Coding best practices
-
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
Construct best practices
-
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
Application best practices
-
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
Unit test your infrastructure
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());
}
CDK in Action
Static web site
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
Static web site
// 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
Static web site
#!/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
CRUD REST API
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
CRUD REST API
// 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
EventBridge Rule scheduled Lambda
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);
}
}
Resources
Thanks
Q&A
Mastering Infrastructure as Code with AWS CDK
By Shuhratbek Mamadaliyev
Mastering Infrastructure as Code with AWS CDK
- 107