The following document goes through how to write an CDK stack that creates an EC2 instance and integrate it with an API Gateway through a VPC link and create a CI/CD pipeline with CodePipeline, CodeDeploy, and CodeCommit.
- Create an EC2 instance
- Create a VPC instance
- Create a key pair for SSH access
- Create an instance by specifying its subnets, type, and image
- Set the security group
- Allow port 80 to be accessed from the VPC block
- Enable SSH access
- Optional: execute a startup script to quickly setup the server on port 80
- Create a target group
- Connect it with the EC2 instance and VPC from step 1
- Set the protocol as TCP for it to work with the network load balancer
- Create a network load balancer (nlb)
- Add a listener for TCP protocol and port 80
- Connect it with the target group from step 3
- Create a REST API Gateway
- Create a VPC link
- Set that target as the created nlb
- Create a VPC integration
- Create a REST API instance
- Add a proxy resource and an ANY method to the API
- Create a stage with variables
- Create a VPC link
- Create a CodePipeline
Read more about CodePipeline code
The IP for the network load balancer is the same for the VPC.
import * as cdk from 'aws-cdk-lib'
import * as apiGateway from 'aws-cdk-lib/aws-apigateway'
import * as ec2 from 'aws-cdk-lib/aws-ec2'
import * as targets from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets'
import {
NetworkLoadBalancer,
NetworkTargetGroup,
Protocol,
TargetType,
} from 'aws-cdk-lib/aws-elasticloadbalancingv2'
import * as codedeploy from 'aws-cdk-lib/aws-codedeploy'
import * as codecommit from 'aws-cdk-lib/aws-codecommit'
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline'
import * as codepipelineActions from 'aws-cdk-lib/aws-codepipeline-actions'
import * as iam from 'aws-cdk-lib/aws-iam'
import { HttpMethod } from '@aws-cdk/aws-apigatewayv2-alpha'
import { Construct } from 'constructs'
import { readFileSync } from 'fs'
const VPC_IP = '10.0.0.0/16'
const createEC2Instance = (parentThis: Construct) => {
// create VPC in which we'll launch the Instance
const vpc = new ec2.Vpc(parentThis, 'my-cdk-vpc', {
ipAddresses: ec2.IpAddresses.cidr(VPC_IP),
natGateways: 0,
subnetConfiguration: [
{ name: 'public', cidrMask: 24, subnetType: ec2.SubnetType.PUBLIC },
],
})
// create a key pair for ssh access
const keyPair = new ec2.CfnKeyPair(parentThis, 'key-pair', {
keyName: 'MyKeyPairB',
})
const role = new iam.Role(parentThis, 'MyRole', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromManagedPolicyArn(
parentThis,
'ManagedPolicy',
'arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole'
),
iam.ManagedPolicy.fromManagedPolicyArn(
parentThis,
'ManagedPolicy2',
'arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforAWSCodeDeploy'
),
],
})
// create the EC2 Instance
const ec2Instance = new ec2.Instance(parentThis, 'ec2-instance', {
vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PUBLIC,
},
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.T2,
ec2.InstanceSize.MICRO
),
machineImage: new ec2.GenericLinuxImage({
// ubuntu image AMI ID
// from https://cloud-images.ubuntu.com/locator/ec2/
'us-east-1': 'ami-0557a15b87f6559cf',
}),
keyName: keyPair.keyName,
role,
instanceName: 'CdkInstanceVfinal',
})
// bash script to quickly setup the server
const userDataScript = readFileSync('./lib/startup.sh', 'utf8')
// add the User Data script to the Instance
ec2Instance.addUserData(userDataScript)
return { ec2Instance, vpc }
}
const createTargetGroup = (
parentThis: Construct,
vpc: cdk.aws_ec2.IVpc,
ec2Instance: cdk.aws_ec2.Instance
) => {
return new NetworkTargetGroup(parentThis, 'target-group', {
// Has to be TCP in order to work with the load balancer
protocol: Protocol.TCP,
port: 80,
targetType: TargetType.INSTANCE,
healthCheck: {
protocol: Protocol.HTTP,
path: '/health',
},
// EC2 instances
targets: [new targets.InstanceTarget(ec2Instance)],
vpc,
})
}
const createNlb = (
parentThis: Construct,
vpc: cdk.aws_ec2.Vpc,
targetGroup: cdk.aws_elasticloadbalancingv2.NetworkTargetGroup
) => {
// create a network load balancer
const nlb = new NetworkLoadBalancer(parentThis, 'nlb', {
loadBalancerName: 'MyLoadBalancer-cdk',
vpc,
internetFacing: true,
})
// create a new listener
const listener = nlb.addListener('Listener', {
port: 80,
protocol: Protocol.TCP,
defaultTargetGroups: [targetGroup],
})
return { nlb, listener }
}
const setEc2SecurityGroup = (
parentThis: Construct,
vpc: cdk.aws_ec2.Vpc,
ec2Instance: cdk.aws_ec2.Instance
) => {
// create a Security Group for the Instance
const SecurityGroup = new ec2.SecurityGroup(parentThis, 'ec2-sg', {
vpc,
allowAllOutbound: true,
})
// enable SSH
SecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(22),
'allow SSH access from anywhere'
)
// enable HTTP for the load balancer only
SecurityGroup.addIngressRule(
ec2.Peer.ipv4(VPC_IP),
ec2.Port.tcp(80),
'allow HTTP traffic from anywhere'
)
ec2Instance.addSecurityGroup(SecurityGroup)
return SecurityGroup
}
const createRestApiWithVpc = (
parentThis: Construct,
nlb: cdk.aws_elasticloadbalancingv2.NetworkLoadBalancer
) => {
// create vpc link
const link = new apiGateway.VpcLink(parentThis, 'vpc-link', {
// list of load balancers
targets: [nlb],
})
// create vpc integration
const vpcIntegration = new apiGateway.Integration({
type: apiGateway.IntegrationType.HTTP_PROXY,
integrationHttpMethod: HttpMethod.ANY,
options: {
connectionType: apiGateway.ConnectionType.VPC_LINK,
vpcLink: link,
// sets the integration {proxy} parameter as the method request's {proxy}
requestParameters: {
'integration.request.path.proxy': 'method.request.path.proxy',
},
},
uri: 'http://${stageVariables.VPCNLB}/{proxy}',
})
// create new REST API
const restApi = new apiGateway.RestApi(parentThis, 'vpc-api')
// add proxy resource and an ANY method
const proxy = restApi.root.addProxy({
anyMethod: false,
})
// create ANY method with vpc integration
proxy.addMethod(HttpMethod.ANY, vpcIntegration, {
requestParameters: {
// allow {proxy} parameter to be set
'method.request.path.proxy': true,
},
})
// create stage deployment instance
const deployment = new apiGateway.Deployment(parentThis, 'Deployment', {
api: restApi,
})
// create a new stage with environment variables
new apiGateway.Stage(parentThis, 'main-stage', {
deployment,
stageName: 'main',
variables: {
VPCNLB: nlb.loadBalancerDnsName,
},
})
// prints REST API stage invoke URL
new cdk.CfnOutput(parentThis, 'Rest API URL', { value: restApi.url })
}
const createCommitRepo = (parentThis: Construct) =>
new codecommit.Repository(parentThis, 'CodeCommitRepo', {
repositoryName: 'MyCdkRepo',
})
const setupCodeDeployApp = (
parentThis: Construct,
targetGroup: cdk.aws_elasticloadbalancingv2.NetworkTargetGroup
) => {
const application = new codedeploy.ServerApplication(
parentThis,
'CodeDeployApplication'
)
// create deployment group
const deploymentGroup = new codedeploy.ServerDeploymentGroup(
parentThis,
'CodeDeployDeploymentGroup',
{
application,
deploymentGroupName: 'MyDeploymentGroup',
// adds EC2 instances matching tags
ec2InstanceTags: new codedeploy.InstanceTagSet({
// any instance with tags satisfying
// will match this group
Name: ['CdkInstanceVfinal'],
}),
loadBalancer: codedeploy.LoadBalancer.network(targetGroup),
// auto creates a role for it
// role: <someRole>
}
)
return { application, deploymentGroup }
}
const setupCodePipeline = (
parentThis: Construct,
application: cdk.aws_codedeploy.ServerApplication,
deploymentGroup: cdk.aws_codedeploy.ServerDeploymentGroup,
commitRepo: cdk.aws_codecommit.Repository
) => {
const pipeline = new codepipeline.Pipeline(parentThis, 'MyPipeline', {
// If this is true, our CodeCommit repo in S3 will be encrypted
// and the deploying stage will fail because it can't decrypt the files
// couldn't find a solution yet
crossAccountKeys: false,
})
// create source & deploy stages
const sourceStage = pipeline.addStage({
stageName: 'Source',
})
const deployStage = pipeline.addStage({
stageName: 'Deploy',
})
const sourceOutput = new codepipeline.Artifact('source')
const sourceStageAction = new codepipelineActions.CodeCommitSourceAction({
repository: codecommit.Repository.fromRepositoryName(
parentThis,
'CodeRepo',
commitRepo.repositoryName
),
actionName: 'Source',
// based on my understanding, it stores the project code
// after pulling/ cloning it into a store(artifact)
// then the deploy stage takes the artifact or our project code as an input
output: sourceOutput,
})
// set the action of what to happen in this stage
sourceStage.addAction(sourceStageAction)
const deployStageAction =
new codepipelineActions.CodeDeployServerDeployAction({
deploymentGroup:
codedeploy.ServerDeploymentGroup.fromServerDeploymentGroupAttributes(
parentThis,
'Deployment-group',
{
application,
deploymentGroupName: deploymentGroup.deploymentGroupName,
}
),
actionName: 'Deploy',
input: sourceOutput,
})
// set the action of what to happen in this stage
deployStage.addAction(deployStageAction)
return pipeline
}
export class CdkStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const { ec2Instance, vpc } = createEC2Instance(this)
const targetGroup = createTargetGroup(this, vpc, ec2Instance)
const { nlb } = createNlb(this, vpc, targetGroup)
setEc2SecurityGroup(this, vpc, ec2Instance)
createRestApiWithVpc(this, nlb)
// CodeCommit
const commitRepo = createCommitRepo(this)
// CodeDeploy
const { application, deploymentGroup } = setupCodeDeployApp(
this,
targetGroup
)
// CodePipeline
setupCodePipeline(this, application, deploymentGroup, commitRepo)
// outputs the repo URL in the terminal
new cdk.CfnOutput(this, 'apiUrl', {
value: commitRepo.repositoryCloneUrlHttp,
})
}
}
- API Gateway example CDK
- AWS EC2 example CDK
- AWS Network load balancer CDK
- Aside from the one above, some GitHub code searching and reading the CDK docs. I think I got used to all of this.