This article is a first in a series of articles inspired by a recent project we worked on, an online training platform developed in Swift using the Vapor Framework and deployed on Amazon Web Services.
Our deliverables included a Cloud Development Kit (CDK) project to define the infrastructure needed to run the training platform as well as the CodePipeline necessary to build and deploy the code in an automated manner.
The purpose of these articles is to show how this can be done. This first article presents the projects and the functionality that is common to both deployment scenarios. The next two articles will present the impementation details for each scenario.
The code has been organized in three projects available on GitHub
trainee-frontend
trainee-cdk
projects below.trainee-cdk-ec2
trainee-cdk-asg
The most relevant files in this project are the configuration files used by the code pipeline
buildspec.yml
is needed by CodeBuild
to build the application.appspec.yml
is needed by CodeDeploy
to deploy the application.The scripts in Scripts/CodeDeploy
are needed by CodeDeploy
to manage the lifecycle of the application.
ApplicationStart.sh
starts the applicationApplicationStop.sh
kills the application processBeforeInstall.sh
cleans up an old version before a new installThere are two CDK projects, one for each type of configuration. Their differences are covered in the next two articles in the series. Here, we're going to look at their common functionality.
Each CDK project is written in TypeScript and contains the following relevant files and folders
/bin
trainee.ts
/lib
trainee-props.ts
trainee-infra.ts
trainee-pipeline.ts
trainee-stack.ts
The file trainee.ts
is main entry point for the CDK project where
the stack is created. This is where you specify the parameters for the stack.
const app = new cdk.App();
const props = {};
new TraineeStack(app, 'trainee', props);
The file trainee-stack.ts
declares parameters for the stack
and implements it using the two infrastructure and pipeline constructs.
export interface TraineeStackProps extends cdk.StackProps {
gitHub: TraineeGitHubProps
keyPairName: string
}
export class TraineeStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: TraineeStackProps) {
super(scope, id, props);
const infrastructure = new TraineeInfrastructure(this, 'infrastructure', props);
const pipeline = new TraineePipeline(this, 'pipeline', props);
cdk.Tags.of(this).add('app:name', 'trainee')
}
}
In general, the stack passes the parameters it receives to the two constructs but, in some cases, it may need to add new ones.
The stack also tags all resources created. This is useful but also needed by the tagged EC2 instance pipeline which uses these tags to determine what EC2 instances to deploy to.
The infrastructure construct trainee-infra.ts
defines all resources
needed to run the trainee-frontend
project above. The resources common to both
deployment scenarios include a VPC, a security group, an IAM role for EC2 instances,
a launch template.
The VPC uses default values that CDK chooses but you may want to stop NAT gateways from being created.
const vpc = new ec2.Vpc(this, 'trainee-vpc', {
vpcName: 'trainee-vpc',
natGateways: 0,
});
The security group is configured to allow HTTP and SSH traffic.
const sg = new ec2.SecurityGroup(this, 'trainee-sg', {
securityGroupName: 'trainee-sg',
vpc: vpc,
allowAllOutbound: true,
});
sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'Allow SSH from anywhere');
sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(8080), 'Allow HTTP from anywhere');
The IAM role is configured to allow EC2 instances to run the CodeDeploy agent.
const instanceRole = new iam.Role(this, 'trainee-instance-role', {
roleName: 'trainee-instance-role',
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonEC2RoleforAWSCodeDeploy')
]
});
The template used to launch EC2 instances to run our application.
It specifies the t3.micro
instance type, the AMI image to be used in each
deployment region, the SSH key pair to be used (if we want to be able to SSH
into the instance)
const template = new ec2.LaunchTemplate(this, 'trainee-launch-template-asg', {
launchTemplateName: "trainee-asg",
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
machineImage: ec2.MachineImage.genericLinux({
'eu-north-1': 'ami-0fe8bec493a81c7da'
}),
keyName: props.keyPairName,
securityGroup: sg,
instanceMetadataTags: true,
requireImdsv2: true,
httpTokens: ec2.LaunchTemplateHttpTokens.REQUIRED,
role: instanceRole,
})
The other constructs differ between the two deployment scenarios so we will take a closer look at them in the next articles.
The code pipeline construct trainee-pipeline.ts
has three stages:
one for getting the source code from GitHub, one for building the source code,
one for deploying the application and the necessary resources.
Each stage can have one or more actions but, in our example,
we only need one action per stage.
Each action has input and output artifacts, except source actions which only produce outputs. The artifacts are kept in an S3 artifact bucket configured for the pipeline.
const artifactBucket = new s3.Bucket(this, "trainee-artifact-bucket", {
bucketName: "trainee-artifact-bucket",
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
The pipeline is constructed using a name and the artifact bucket.
const pipeline = new cp.Pipeline(this, 'trainee-pipeline', {
pipelineName: "trainee-pipeline",
artifactBucket: artifactBucket,
});
After it is constructed, we can add the three stages to it.
This stage contains a GitHubSourceAction
named GetCode
that gets the source code for trainee-frontend
from its GitHub repository
and makes it available for later processing.
const sourceOutput = new cp.Artifact("trainee-source-artifact");
const oauthToken = cdk.SecretValue.secretsManager(props.gitHub.oauthTokenSecretName);
const sourceAction = new cpa.GitHubSourceAction({
actionName: "GetCode",
repo: props.gitHub.repository.name,
branch: props.gitHub.repository.branch,
owner: props.gitHub.repository.owner,
oauthToken: oauthToken,
output: sourceOutput
});
The action is configured with information about the repository, an OAuth token for the GitHub account, and the output artifact.
The actual OAuth token is kept in AWS Secrets Manager for security reasons.
This stage defines a CodeBuild project named trainee-project
that is configured
to use a swift:5.9-jammy
Docker image to build code.
const buildProject = new cb.PipelineProject(this, "trainee-project", {
projectName: "trainee-project",
environment: {
buildImage: cb.LinuxBuildImage.fromDockerRegistry("swift:5.9-jammy")
}
});
The project is used by a CodeBuild action named BuildCode
that takes the source code from the first stage and builds it using
the buildspec.yml
configuration file that comes with it.
const buildOutput = new cp.Artifact("trainee-build-artifact");
const buildAction = new cpa.CodeBuildAction({
actionName: "BuildCode",
input: sourceOutput,
outputs: [buildOutput],
project: buildProject,
});
The configuration file also tells CodeBuild to include
the appspec.yml
file and the scripts in the Scripts/CodeBuild
folder
among the artifacts produced for the next pipeline stage.
The deployment stage is configured using an application, deployment group, and action.
const application = new cd.ServerApplication(this, "trainee-application", {
applicationName: "trainee-application"
});
const deploymentGroup = new cd.ServerDeploymentGroup(this, "trainee-deployment-group", {
deploymentGroupName: "trainee-deployment-group",
application: application,
deploymentConfig: cd.ServerDeploymentConfig.ONE_AT_A_TIME,
installAgent: ###,
ec2InstanceTags: ###,
autoScalingGroups: ###,
});
const deployAction = new cpa.CodeDeployServerDeployAction({
actionName: "DeployCode",
input: buildOutput,
deploymentGroup: deploymentGroup,
});
The deployment group construct is the only one that differs between the two configurations we are considering (tagged EC2 instances and autoscaling groups). We will cover the details in the next two articles.
This stage defines a CodeDeploy action named DeployCode
that takes
the artifacts produced in the previous stage and deploys them to EC2 instances
using the appspec.yml
configuration file.