09 January 2024
AWS CodePipeline
Use CDK to build and deploy an application to AWS using CodePipeline
Lucian Suta
#aws #cdk #codebuild #codedeploy #codepipeline

Introduction

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.

Code

The code has been organized in three projects available on GitHub

  • trainee-frontend
    A simple Swift application whose purpose is to be built and deployed by the trainee-cdk projects below.
  • trainee-cdk-ec2
    CDK project that demonstrates deployment to tagged EC2 instances.
  • trainee-cdk-asg
    CDK project that demonstrates deployment to autoscaling groups (ASG).

Application

The most relevant files in this project are the configuration files used by the code pipeline

The scripts in Scripts/CodeDeploy are needed by CodeDeploy to manage the lifecycle of the application.

CDK Projects

There 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.

Infrastructure Construct

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.

Pipeline Construct

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.

Source Stage

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.

Build Stage

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.

Deploy 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.

Reach us at: contact {at} defsense {dot} eu
Built with Nuxt, Tailwind and deployed on Cloudflare
Copyright © 2024 Defsense