Link Search Menu Expand Document

AWS CodePipeline (Revised)

Advanced

In previous articles of my microservicesvn blog, we go through many steps and AWS technologies to setup CI/CD process for our microservice deployment - RemindersManagament in the FriendReminders demo application. Those steps are:

  • Setup source code repository based on AWS CodeCommit
  • Setup Docker Image repository based on Elastic Container Registry (ECR)
  • Using CodeBuild to compile, testing source code, then build / upload Image to ECR
  • Create / configure ECS with one of deployment strategies (Blue/Green or Rolling updates)
  • Using CodePipeline to configure the deployment into ECS Cluster

In each step, we use AWS Console, or command-lines to define and configure the services. Those activities can help us to understand the basic and principles of CI/CD in a cloud environment like AWS. Once having solid knowledge about AWS stack, we’ll take advantage of Cloud Development Kit (CDK) service that will help us to optimise and simplify all setup steps we did previously.

Following sections will provide guideline to setup a CI/CD for our mircroserivces based on CDK.

Content

  1. Source Code Preparation
  2. Defining ECS Cluster
  3. Creating CDK Pipeline Stack
  4. Building Docker Image in ECR
  5. Deploy Docker Containers to ECS
  6. Conclusion

Source Code Preparation

Assuming we already developed the RemindersManagement microservice for the FriendReminders application based on Docker containers.

The logic of the microservice is simple, it is using the Web-based API approach to manage a list of reminders. The data is kept in an in-memory database so it can run independently in the local development. When deploying to the AWS, it should use an RDS service such as PostgreSQL DB or MS SQL provided by AWS.

SwaggerUI Output Swagger UI of RemindersManagement API

If you don’t have source code, you can clone it from the Github Demo link. The structure of demo source code is explained as below:

  • Infrastructure: store AWS CDK code that we will develop to create CI/CD pipeline
  • Services: store all microservices source code of FriendReminders application
    • RemindersManagement: a microservice demo source code
      • RemindersManagement.API: logic implementation based on Web-API
      • RemindersManagement.Build: CDK code creating ECS for microservice
      • RemindersManagement.FuntionalTests: Functional test script
      • RemindersManagement.UnitTests: unit tests of microservices

Finally, the microservice handles all coming requests through a reverse proxy - NGINX to enhance security level and improve its performance by offload some tasks into NGINX proxy scope so it can focus on the business logic processing only.


Defining ECS Cluster

To create ECS Cluster for deployment of the microservice, we will create a CDK project based on TypeScript language.

Step 1: In the folder RemindersManagement.Build, using cdk init command to create and initialise a CDK application

cdk init app --language typescript

Output

CDK Project Init with TypeScript CDK Project Init with TypeScript

The command will create some folders inside RemindersManagement.Build. There are two important files:

  • bin\reminders_management.build.ts:
  • lib\reminders_management.build-stack.ts

eminders_management.build.ts is the entry file in the CDK project. It defines an App construct in a Stack construct called RemindersManagementBuildStack.

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { RemindersManagementBuildStack } from '../lib/reminders_management.build-stack';

const app = new cdk.App();
new RemindersManagementBuildStack(app, 'RemindersManagementBuildStack');

reminders_management.build-stack.ts: is where we implement RemindersManagementBuildStack logic

import * as cdk from '@aws-cdk/core';

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

    // The code that defines your stack goes here
  }
}

Guideline

To use TypeScript, we have to install Node Package Manager (NPM) and TypeScript in your local environment. You can use this link AWS CDK in TypeScript to refer some prerequisites setup for using CDK.

In order to confirm project’s deployment, we will use two commands:

  • Compile TypeScript code to JavaScript code
npm run build

Output

> reminders_management.build@0.1.0 build /Users/anh/Workspace/git/temp/FriendReminders/Services/RemindersManagement/RemindersManagement.Build
> tsc
  • Deploy the stack to the default AWS account/region
cdk deploy

Output

RemindersManagementBuildStack: deploying...
RemindersManagementBuildStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (2/2)

 ✅  RemindersManagementBuildStack

Stack ARN:
arn:aws:cloudformation:ap-southeast-2:729365137003:stack/RemindersManagementBuildStack/92b357a0-197f-11eb-89f8-0a85bdb8ca7e

We can also use AWS Console, CloudFormation -> Stacks to confirm the new stack is created successfully:

CDK Stack in AWS Console CDK Stack in AWS Console

Step 2: Install CDK packages for ECS

In the folder RemindersManagement.Build, using npm install command to setup some CDK packages. These packages are provided by AWS to provide build-in constructs that can help developers create the applications more efficient.

npm install @aws-cdk/aws-ec2 @aws-cdk/aws-ecs @aws-cdk/aws-ecr @aws-cdk/aws-ecs-patterns @aws-cdk/aws-applicationautoscaling

After install successfully, we modify eminders_management.build-stack.ts to import those packages:

import * as cdk from '@aws-cdk/core';
import * as ec2 from "@aws-cdk/aws-ec2";
import * as ecs from "@aws-cdk/aws-ecs";
import * as ecr from "@aws-cdk/aws-ecr";
import * as iam from "@aws-cdk/aws-iam";
import * as ecs_patterns from "@aws-cdk/aws-ecs-patterns";
import * as auto_scale from "@aws-cdk/aws-applicationautoscaling";

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

    // The code that defines your stack goes here
  }
}

Step 3: Create ECS Fargate Service

To create an ECS Fargate Service, we have to define following components

  • Virtual Private Network
  • ECS Cluster
  • Task Definition
  • Fargate Service
  • Load Balancer
  • Auto Scaling Group

By using CDK, we can define those components with few lines of code. Let update eminders_management.build-stack.ts file to include those definitions:

import * as cdk from '@aws-cdk/core';
import * as ec2 from "@aws-cdk/aws-ec2";
import * as ecs from "@aws-cdk/aws-ecs";
import * as ecr from "@aws-cdk/aws-ecr";
import * as iam from "@aws-cdk/aws-iam";
import * as ecs_patterns from "@aws-cdk/aws-ecs-patterns";
import * as auto_scale from "@aws-cdk/aws-applicationautoscaling";

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

    const imageTag = this.node.tryGetContext('imageTag');

    // The code that defines your stack goes here
    const vpc = new ec2.Vpc(this, "FriendRemindersVpc", {
      maxAzs: 2 // Default is all AZs in region
    });

    const cluster = new ecs.Cluster(this, "FriendRemindersCluster", {
      vpc: vpc
    });

    // Creating a Task Definition
    const taskDef = new ecs.FargateTaskDefinition(this, 'RemindersMgtTaskDef', {
      cpu: 1024,
      memoryLimitMiB: 4096,
    });

    // Importing existing ECR repositories
    const proxyRepo = ecr.Repository.fromRepositoryName(this, "nginx", 'nginx')
    const serviceRepo = ecr.Repository.fromRepositoryName(this, "remindersmgtservice", 'remindersmgtservice')

    // Creating the service container
    const proxyContainer = taskDef.addContainer("nginx", {
      image: ecs.ContainerImage.fromEcrRepository(proxyRepo, "fargate")
    });

    // Specifying the application's port mappings
    proxyContainer.addPortMappings({
      hostPort: 80,
      containerPort: 80
    })

    // Creating the service container
    const serviceContainer = taskDef.addContainer("remindersmgtservice", {
      image: ecs.ContainerImage.fromEcrRepository(serviceRepo, imageTag)
    });

    // Create a load-balanced Fargate service and make it public
    const loadBalancedFargateService = new ecs_patterns.ApplicationMultipleTargetGroupsFargateService(this, "FriendRemindersService", {
      cluster: cluster, // Required
      cpu: 512, // Default is 256
      memoryLimitMiB: 2048, // Default is 512      
      desiredCount: 1, // Default is 1
      taskDefinition: taskDef,
    });

    // ECR Permission
    loadBalancedFargateService.taskDefinition.executionRole?.addManagedPolicy(
      iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryPowerUser'));

    loadBalancedFargateService.targetGroup.configureHealthCheck({
      path: "/health",
    });      

    // Auto Scaling    
    const scalableTarget = loadBalancedFargateService.service.autoScaleTaskCount({
      minCapacity: 1,
      maxCapacity: 2,
    });

    scalableTarget.scaleOnSchedule('DaytimeScaleDown', {
      schedule: auto_scale.Schedule.cron({ hour: '8', minute: '0'}),
      minCapacity: 1,
      maxCapacity: 2,
    });
    
    scalableTarget.scaleOnSchedule('EveningRushScaleUp', {
      schedule: auto_scale.Schedule.cron({ hour: '20', minute: '0'}),
      minCapacity: 1,
      maxCapacity: 2,
    });    
  }
}

There are some important points we need to understand in this modification

  • Using higher-level CDK Construct - ecs_patterns to create ECS Fargate with Load Balancer
  • Using ecr.Repository to refer to some existing ECR repositoties that we created in previous article - ECR.
  • Applying Sidecar pattern for the microservice, so it will contain two Docker Images:
    • nginx - NGINX reverse proxy
    • remindersmgtservice: - RemindersManagement microservices
  • Defining a parameter called imageTag. It will refer to the latest Docker Image created by build process in a Pipeline stack that we will define in the some next steps.

Note

The basic idea is that we will have two CDK projects. The first CDK project creates ECS Fargate Service for running the microservice RemindersManagement. The second CDK project creates a Pipeline stack that help us to build and upload Docker Image everytime developers push new source code updates to the CodeCommit service. The Pipeline stack works as a bootstrap program since it can update itself (for example, adding new stage in the Pipeline) and trigger a process to update ECS Infrastructure if there is any modification in the first CDK project.


Creating CDK Pipeline Stack

In this section, we will implement a Pipeline stack that creating a CI/CD process for RemindersManagement service deployment.

Step 1: In the folder Infrastructure, using cdk init command to create another CDK application

cdk init app --language typescript

Install some NPM packages from CDK library with following command:

npm install @aws-cdk/aws-codecommit @aws-cdk/aws-codebuild @aws-cdk/aws-codepipeline @aws-cdk/aws-codepipeline-actions @aws-cdk/aws-iam

Import NPM package in file: lib\infrastructures-stack.ts:

import * as cdk from '@aws-cdk/core';
import * as codecommit from '@aws-cdk/aws-codecommit';
import * as codebuild from '@aws-cdk/aws-codebuild';
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import * as iam from '@aws-cdk/aws-iam';

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

    // The code that defines your stack goes here
  }
}

Step 2: Define CodeCommit repository using CDK construct

  • Update content of lib\infrastructures-stack.ts to define a CodeCommit repository
import * as cdk from '@aws-cdk/core';
import * as codecommit from '@aws-cdk/aws-codecommit';
import * as codebuild from '@aws-cdk/aws-codebuild';
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import * as iam from '@aws-cdk/aws-iam';

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

    // Create a new repository or refer to an existing one if it was created
    const repo = new codecommit.Repository(this, "FriendRemindersV2", {
      repositoryName: "FriendRemindersV2",
      description: "New repository for FriendReminders project."
    });    
  }
}
  • Create CodeCommit repository naming FriendRemindersV2 by build and deploy CDK project
npm run build
cdk deploy

CDK CodeCommit Repository Creation CDK CodeCommit Repository Creation

We can use AWS Console and going to DeveloperTools -> CodeCommit to confirm new repository - FriendRemindersV2 has been created successfully.

CDK CodeCommit Repository in AWS Console CDK CodeCommit Repository in AWS Console

  • Connect local repo to the new CodeCommit repository.

In the root solution folder FriendReminders, using the commands:

// remote current git setting in the project if existing (when you clone from github)
rm -rf .git

// initialise git
git init
git add *
git commit -m 'feat: initial commit'

// add remote git and push source code
git remote add origin ssh://git-codecommit.ap-southeast-2.amazonaws.com/v1/repos/FriendRemindersV2
git push --set-upstream origin master

Source Code in AWS CodeCommit Source Code in AWS CodeCommit

Step 3: Adding Build & Unit Testing stage in the Pipeline

When the source code is ready in the CodeCommit repository, we move to the next step to create a new Build stage in the Pipeline. In this stage, we will use some dotnet command to build source code, running unit test, and create test report by using CodeBuild service.

  • Create a new folder RemindersManagement in the Infrastructures to keep all buildspec file.

  • In the new folder RemindersManagement, create a buildspec file called unittestspec.yml with following content

version: 0.2

phases:
  install:
    runtime-versions:
        dotnet: 3.1

  build:
    commands:
      - echo Unit Test started on `date`
      - dotnet test -c Release ./Services/RemindersManagement/RemindersManagement.UnitTests/RemindersManagement.UnitTests.csproj --logger trx --results-directory ./TestResults /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=../../../TestResults/
      - echo Unit Test completed on `date`

artifacts:
  files:
    - '**/*'
    - TestResults/*
  discard-paths: no

reports:
  GeneralTestsReport:
      file-format: VisualStudioTrx
      files:
          - '**/*'
      base-directory: './TestResults'
  CoverageTestsReport:
      file-format: CoberturaXml
      files:
          - '**/*'
      base-directory: './TestResults'      
  • Update content of lib\infrastructures-stack.ts to add new CodeBuild construct
import * as cdk from '@aws-cdk/core';
import * as codecommit from '@aws-cdk/aws-codecommit';
import * as codebuild from '@aws-cdk/aws-codebuild';
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import * as iam from '@aws-cdk/aws-iam';

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

    // The code that defines your stack goes here
    const repo = new codecommit.Repository(this, "FriendRemindersV2", {
      repositoryName: "FriendRemindersV2",
      description: "New repository for FriendReminders project."
    });

    // Define service role for CodeBuild service
    const serviceRole = iam.Role.fromRoleArn(this, 
      "codebuild-FriendRemindersBuild-service-role",
      "arn:aws:iam::729365137003:role/service-role/codebuild-FriendRemindersBuild-service-role");

    // Test Project
    const testProject = new codebuild.Project(this, "FriendRemindersTestV2", {
      projectName: "FriendRemindersTestV2",
      buildSpec: codebuild.BuildSpec.fromSourceFilename('Infrastructures/RemindersManagement/unittestspec.yml'),
      description: "FriendReminders Test Project created by CDK.",
      environment: {
        buildImage: codebuild.LinuxBuildImage.STANDARD_4_0,
        privileged: true,
      },
      source: codebuild.Source.codeCommit({
        repository: repo,
        branchOrRef: "refs/heads/master"
      }),
      role: serviceRole,
    });

    // Pipeline
    const sourceOuput = new codepipeline.Artifact();
    const pipeline = new codepipeline.Pipeline(this, "FriendRemindersPipelineV2", {
      stages: [
        {
          stageName: 'Source',
          actions: [
            new codepipeline_actions.CodeCommitSourceAction({
              actionName: 'CodeCommit_Source',
              repository: repo,
              output: sourceOuput
            }),
          ]
        },
        {
          stageName: 'Test',
          actions: [
            new codepipeline_actions.CodeBuildAction({
              actionName: 'UnitTest_Runner',
              input: sourceOuput,
              project: testProject,
            }),
          ]
        }
      ]
    });    
  }
}    

In the above source code, we define two additional constructs: CodeBuild and CodePipeline.

  • The CodeBuild construct is defined via an CodeBuild Project naming FriendRemindersTestV2. It is using Docker Container LinuxBuildImage.STANDARD_4_0 to run all steps defined in the unittestspec.yml file. It use source property to refer source code that being stored by CodeCommit Repository FriendRemindersV2.

  • The Pipeline Construct define a pipeline with two stages at this moment. The Source stage refer to Source Code repository. The Test stage is handled by CodeBuild construct that we just define. By default, Pipeline can use CloudWatch event to listen all updates in the CodeCommit repository. Therefore, everytime developer update and push source code in the main branch refs/heads/master, the Pipeline will be notify and trigger the whole process autimatically.

Note

The CodeBuild constract is using an existing service role codebuild-FriendRemindersBuild-service-role to allow CodeBuild to invoke some services such as ECR, CloudFormation etc…The policies attached the service role will be various depending on its purposes. Since I want to re-use this service role in every stages (build, deploy etc), i just attach AdministratorAccess policy on this service role so CodeBuild service can do everything with Administrator priviledges although this way is not a recomended due to security concerns. In additional, we can also define the service by using CDK code, but i let you to do this step as a small assignment.

After update the Pipeline, we build and deploy the stack by using following command in the folder Infrastructure:

npm run build
cdk deploy

Create CodePipeline by using CDK Create CodePipeline by using CDK Stack

When CDK deploying is completed, we can go to CloudFormation -> Stacks in AWS Console to confirm new stack:

CodePipeline Stack in CloudFormation CodePipeline Stack in CloudFormation

Using AWS Console, we can also go to CodeBuild project, DeveloperTools -> CodeBuild to confirm the new Pipeline has been created and executed.

New Pipeline Execution created by CDK New Pipeline created by CDK

Click on the link Details in the Test phase, then go to the Reports section, we can see the Unit Testing report created by CodeBuild service

UnitTest Reports List UnitTest Reports List

An Unit Testing Report in Details An Unit Testing Report in Details


Building Docker Image in ECR

When source code is compiled and tested successfully, we can add a new stage for building Docker Image in the Pipeline.

  • Update content of lib\infrastructures-stack.ts as the belowing
import * as cdk from '@aws-cdk/core';
import * as codecommit from '@aws-cdk/aws-codecommit';
import * as codebuild from '@aws-cdk/aws-codebuild';
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import * as iam from '@aws-cdk/aws-iam';

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

    // The code that defines your stack goes here
    const repo = new codecommit.Repository(this, "FriendRemindersV2", {
      repositoryName: "FriendRemindersV2",
      description: "New repository for FriendReminders project."
    });

    // Define service role for CodeBuild service
    const serviceRole = iam.Role.fromRoleArn(this, 
      "codebuild-FriendRemindersBuild-service-role",
      "arn:aws:iam::729365137003:role/service-role/codebuild-FriendRemindersBuild-service-role");

    // Test Project
    const testProject = new codebuild.Project(this, "FriendRemindersTestV2", {
      projectName: "FriendRemindersTestV2",
      buildSpec: codebuild.BuildSpec.fromSourceFilename('Infrastructures/RemindersManagement/unittestspec.yml'),
      description: "FriendReminders Test Project created by CDK.",
      environment: {
        buildImage: codebuild.LinuxBuildImage.STANDARD_4_0,
        privileged: true,
      },
      source: codebuild.Source.codeCommit({
        repository: repo,
        branchOrRef: "refs/heads/master"
      }),
      role: serviceRole,
    });

    // Build Project
    const buildProject = new codebuild.Project(this, "FriendRemindersBuildV2", {
      projectName: "FriendRemindersBuildV2",
      buildSpec: codebuild.BuildSpec.fromSourceFilename('Infrastructures/RemindersManagement/buildspec.yml'),
      description: "FriendReminders Build Project created by CDK.",
      environment: {
        buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_3,
        privileged: true,
      },
      source: codebuild.Source.codeCommit({
        repository: repo,
        branchOrRef: "refs/heads/master"
      }),
      role: serviceRole,
    });

    // Pipeline
    const sourceOuput = new codepipeline.Artifact();
    const pipeline = new codepipeline.Pipeline(this, "FriendRemindersPipelineV2", {
      stages: [
        {
          stageName: 'Source',
          actions: [
            new codepipeline_actions.CodeCommitSourceAction({
              actionName: 'CodeCommit_Source',
              repository: repo,
              output: sourceOuput
            }),
          ]
        },
        {
          stageName: 'Test',
          actions: [
            new codepipeline_actions.CodeBuildAction({
              actionName: 'UnitTest_Runner',
              input: sourceOuput,
              project: testProject,
            }),
          ]
        },
        {
          stageName: 'Build',
          actions: [
            new codepipeline_actions.CodeBuildAction({
              actionName: 'Build_DockerImage_ECR',
              input: sourceOuput,
              project: buildProject,
            })
          ]
        }        
      ]
    });    
  }
}
  • In the folder Infrastructures/RemindersManagement, we create a new buildspec file - buildspec.yml that will be used by new CodeBuild construct:
version: 0.2

phases:
  install:
    runtime-versions:
        dotnet: 3.1
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws --version
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
      - REPOSITORY_URI=729365137003.dkr.ecr.ap-southeast-2.amazonaws.com/remindersmgtservice
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $REPOSITORY_URI:latest ./Services/RemindersManagement/RemindersManagement.API
      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
  post_build:
    commands:
      - echo Pushing the Docker images...
      - docker push $REPOSITORY_URI:latest
      - docker push $REPOSITORY_URI:$IMAGE_TAG
      - echo Build completed on `date`
  • Save the changes, commit and push new source code to AWS CodeCommit

Commit and push source code to AWS CodeCommit Commit and push changes to AWS CodeCommit

  • Compile the TypeScript file and run cdk deploy command in the Infrastructures:
npm run build
cdk deploy

Run build and deploy change in the Pipeline stack Build and deploy change in Pipeline stack

  • In the AWS Console, the Pipeline has an update with the new stage for building Docker Image.

New build stage in the Pipeline for Docker Image Build Build stage in Pipeline for Docker Image Build


Deploy Docker Containers to ECS

In this step, we will deploy Docker Container of RemindersManagement service to the ECS Clluster that was defined in the previous step.

  • In folder Infrastructure\RemindersManagement, create a new buildspec file - deployspec.yml to define deployment steps:
version: 0.2

phases:
  install:
    runtime-versions:
        nodejs: 12
    commands:
      - npm install -g aws-cdk
      - npm install -g typescript
      - cdk --version
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws --version
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
  build:
    commands:
      - echo Deploy started on `date`
      - echo Build CDK project...
      - cd ./Services/RemindersManagement/RemindersManagement.Build
      - npm install
      - npm run build
      - cdk deploy --require-approval never  -c imageTag=$IMAGE_TAG
      - echo Deploy completed on the `date`

The file logic is simple. It refers to the CDK project that we have in RemindersManagement.Build, running build command then deploy the CDK project to create / or update ECS infrastructure. The cdk deploy use the parameter IMAGE_TAG (has value of git commit’s hash) to refer the Docker Image stored in ECR Repository.

  • We also need to update lib\infrastructures-stack.ts to define new deploy stage and include it in the current Pipeline.
import * as cdk from '@aws-cdk/core';
import * as codecommit from '@aws-cdk/aws-codecommit';
import * as codebuild from '@aws-cdk/aws-codebuild';
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import * as iam from '@aws-cdk/aws-iam';

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

    // The code that defines your stack goes here
    const repo = new codecommit.Repository(this, "FriendRemindersV2", {
      repositoryName: "FriendRemindersV2",
      description: "New repository for FriendReminders project."
    });

    // Define service role for CodeBuild service
    const serviceRole = iam.Role.fromRoleArn(this, 
      "codebuild-FriendRemindersBuild-service-role",
      "arn:aws:iam::729365137003:role/service-role/codebuild-FriendRemindersBuild-service-role");

    // Test Project
    const testProject = new codebuild.Project(this, "FriendRemindersTestV2", {
      projectName: "FriendRemindersTestV2",
      buildSpec: codebuild.BuildSpec.fromSourceFilename('Infrastructures/RemindersManagement/unittestspec.yml'),
      description: "FriendReminders Test Project created by CDK.",
      environment: {
        buildImage: codebuild.LinuxBuildImage.STANDARD_4_0,
        privileged: true,
      },
      source: codebuild.Source.codeCommit({
        repository: repo,
        branchOrRef: "refs/heads/master"
      }),
      role: serviceRole,
    });

    // Build project
    const buildProject = new codebuild.Project(this, "FriendRemindersBuildV2", {
      projectName: "FriendRemindersBuildV2",
      buildSpec: codebuild.BuildSpec.fromSourceFilename('Infrastructures/RemindersManagement/buildspec.yml'),
      description: "FriendReminders Build Project created by CDK.",
      environment: {
        buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_3,
        privileged: true,
      },
      source: codebuild.Source.codeCommit({
        repository: repo,
        branchOrRef: "refs/heads/master"
      }),
      role: serviceRole,
    });

    // Deploy Project
    const deployProject = new codebuild.Project(this, "FriendRemindersDeployV2", {
      projectName: "FriendRemindersDeployV2",
      buildSpec: codebuild.BuildSpec.fromSourceFilename('Infrastructures/RemindersManagement/deployspec.yml'),
      description: "FriendReminders Deploy Project created by CDK.",
      environment: {
        buildImage: codebuild.LinuxBuildImage.STANDARD_4_0,
        privileged: true,
      },
      source: codebuild.Source.codeCommit({
        repository: repo,
        branchOrRef: "refs/heads/master"
      }),
      role: serviceRole
    });

    // Pipeline
    const sourceOuput = new codepipeline.Artifact();
    const pipeline = new codepipeline.Pipeline(this, "FriendRemindersPipelineV2", {
      stages: [
        {
          stageName: 'Source',
          actions: [
            new codepipeline_actions.CodeCommitSourceAction({
              actionName: 'CodeCommit_Source',
              repository: repo,
              output: sourceOuput
            }),
          ]
        },
        {
          stageName: 'Test',
          actions: [
            new codepipeline_actions.CodeBuildAction({
              actionName: 'UnitTest_Runner',
              input: sourceOuput,
              project: testProject,
            }),
          ]
        },
        {
          stageName: 'Build',
          actions: [
            new codepipeline_actions.CodeBuildAction({
              actionName: 'Build_DockerImage_ECR',
              input: sourceOuput,
              project: buildProject,
            })
          ]
        },
        {
          stageName: 'Deploy',
          actions: [
            new codepipeline_actions.CodeBuildAction({
              actionName: 'Deploy_DockerImage_ECS',
              input: sourceOuput,
              project: deployProject,
            })
          ]          
        }
      ]
    });
  }
}
  • Similar as previous steps, we save those changes, commit and push to AWS CodeCommit. Then we run few commands to build and deploy CDK project in the folder Infrastructure:
npm run build
cdk deploy

Create Deploy stage in the Pipeline Create Deploy stage in the Pipeline

In AWS Console, we can see Deploy stage has been added in the Pipeline:

The deploy Stage of Pipeline in AWS Console The deploy Stage of Pipeline in AWS Console

Going to the CloudFormation -> Stacks in AWS Console, we can see ECS Cluster’s resources are being created in the RemindersManagementBuildStack stack:

ECS Cluster Resources ECS Cluster Resources

When the stack’s execution finishes, we can click on Outputs tab to see an URL of the load balancer of ECS Cluster. This link will show the Swagger UI of RemindersManagement microsevcies that we are working with.

CDK CloudFormation Outputs CDK CloudFormation Outputs

Swagger UI of RemindersManagement microservice Swagger UI of RemindersManagement microservice


Conclusion

In this article, we have implemented some CDK Stacks to create a CI/CD Pipeline and an ECS deployment environment for a dotnet microservice. The Pipeline has several phases: source, test, build and deploy but it can be exentent easily and dynamically. Using CDK technology is not only providing an efficient way to built up infrastructure for a microservice, but also highly reuseable solution since we can apply it for other microservices or other solution. There are still some problems that i would list here as some assignments so that you can try to implement by yourself to learn more about CDK and microservice:

  • Using CDK to define a Service Role, and Policy rather than referring to an existing one in AWS IAM.
  • The Pipeline should be updated automatically when commit new source code rather than running cdk deploy command from local.
  • Storing source code in an other Repository, for example: GitHub (using GitHub action to trigger pipeline process)
  • Creating different infrastructure (ECS Cluster / Service) for source code in different branch so we can test changes separately.

Copyright © 2019-2022 Tuan Anh Le.