Implementing AWS CDK CI/CD with CDK Pipelines

Cloud Migration Scenarios

Four scenarios to migrate to AWS – from infrastructure to ML

The AWS Cloud Development Kit (AWS CDK) makes it easy to build cloud infrastructure. And CDK Pipelines makes it "painless" to deploy cloud infrastructure. So let’s create an AWS CDK CI/CD pipeline, and build and run our application on AWS.

AWS CDK CI/CD Pipeline

AWS CDK CI/CD Pipeline

An AWS CDK CI/CD Pipeline, essentially, downloads the application sources, builds the CloudFormation-templates, updates itself and deploys the application. Note that the self-update enables easy addition/removal of pipeline stages.

The entire pipeline, amazingly enough, is implemented by two constructs: CdkPipeline and Stage. CdkPipeline defines the CodePipeline and associated stages and actions. Stage defines the applications stack-set.

CDK Pipelines ECS Deployment

Let’s test CDK Pipelines by deploying to AWS ECS. Since ECS is well-supported by CDK and CloudFormation, I expect a pretty painless implementation.

The application source, Dockerfile and cloud infrastructure are provided at GitHub.

ECS CDK CI/CD Pipeline

The ECS CI/CD pipeline includes an app build stage to create container images. Because the images are built after the CloudFormation-templates, we have to figure out how to specify them. Since the CdkPipeline.addApplicationStagedoesn’t support parameterOverrides, I implemented a tagging convention. All images are tagged with the GitHub commit id. And since the application- and infrastructure code are versioned together, we build valid CloudFormation-templates.

Remark It’s possible to use parameterOverrides by creating the CodePipeline stages yourself. Since this defeats the purpose of CdkPipeline, I don’t recommend doing so.

Building the pipeline is pretty painless. All CI/CD infrastructure is defined by the following code.

import * as cdk from '@aws-cdk/core';
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import * as codebuild from '@aws-cdk/aws-codebuild';
import * as ecr from '@aws-cdk/aws-ecr'
import * as iam from '@aws-cdk/aws-iam';
import * as pipelines from '@aws-cdk/pipelines';
import { LocalDeploymentStage } from './local-deployment';

export class CicdInfraStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const sourceArtifact = new codepipeline.Artifact();
    const cdkOutputArtifact = new codepipeline.Artifact();

    // The core CdkPipeline is created and consists of DownloadSources, cdk synth and cdk deploy <this stack>
    const pipeline = new pipelines.CdkPipeline(this, 'CdkPipeline', {
      pipelineName: 'cdk-cdkpipeline',
      cloudAssemblyArtifact: cdkOutputArtifact,
      sourceAction: new codepipeline_actions.GitHubSourceAction({
        actionName: 'DownloadSources',
        owner: 'binxio',
        repo: 'blog-cdk-cicd-cdkpipeline',
        oauthToken: cdk.SecretValue.secretsManager('/github.com/binxio', {
          jsonField: 'token'
        }),
        output: sourceArtifact,
      }),
      synthAction: pipelines.SimpleSynthAction.standardNpmSynth({
        sourceArtifact: sourceArtifact,
        cloudAssemblyArtifact: cdkOutputArtifact,
        subdirectory: 'cdk',
      }),
    });

    const repository = new ecr.Repository(this, 'Repository', {
      repositoryName: 'cdk-cicd/app',
    });
    const buildRole = new iam.Role(this, 'DockerBuildRole', {
      assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
    });
    repository.grantPullPush(buildRole);

    // The docker build stage is added
    const buildStage = pipeline.addStage('AppBuild')
    buildStage.addActions(new codepipeline_actions.CodeBuildAction({
      actionName: 'DockerBuild',
      input: sourceArtifact,
      project: new codebuild.Project(this, 'DockerBuild', {
        role: buildRole,
        environment: {
          buildImage: codebuild.LinuxBuildImage.STANDARD_4_0,
          privileged: true,
        },
        buildSpec: this.getDockerBuildSpec(repository.repositoryUri),
      }),
    }));

    // The deployment of Stage 'Local' is added
    const localStage = new LocalDeploymentStage(this, 'AppDeployLocal');
    pipeline.addApplicationStage(localStage);
  }

  getDockerBuildSpec(repositoryUri: string): codebuild.BuildSpec {
    return ...
  }
}

And after the CI/CD Pipeline stack deployment, the pipeline executes. Painlessly.

ECS CodePipeline execution

Discussion

AWS CDK keeps getting better. The constructs composition model is powerful and creates impressive products such as CDK Pipelines. CDK Pipelines, however, still lacks some scaling options. Currently, for example, each pipeline creates it’s own Artifacts bucket.

In this experiment I tested the known path. More experiments are required to determine CDK Pipelines quality with existing infra deployments, complex Stage (stack-set) deployments and cross-account deployments.

Conclusion

Implementing a AWS CDK CI/CD pipeline is nearly painless with CDK Pipelines. So start using AWS CDK to build and run your application on AWS.

As a cloud consultant I enjoy taking software engineering practices to the cloud. Continuously improving the customers systems, tools and processes by focusing on integration and quality.
Share this article: Tweet this post / Post on LinkedIn