Building a dynamic AWS Pipeline with CDK

Tuan Anh Le
andy.le
Published in
16 min readOct 31, 2020

Reference: https://www.microservicesvn.com/docs/cicd/codepipeline_revised.html

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

Source Code Preparation

Assuming we already developed the RemindersManagement microservice for the FriendRemindersapplication 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.

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
  • Services\RemindersManagement: a microservice demo source code
  • Services\RemindersManagement.API: logic implementation based on Web-API
  • Services\RemindersManagement.Build: CDK code creating ECS for microservice
  • Services\RemindersManagement.FuntionalTests: Functional test script
  • Services\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

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

reminders_management.build.ts is the entry file in the CDK project. It defines an App construct in a Stackconstruct 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
}
}

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)
✅ RemindersManagementBuildStackStack 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

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.

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 bootstrapprogram 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

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
  • Connect local repo to the new CodeCommit repository.

In the root solution folder FriendReminders, using the commands:

// remote current git setting in 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

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.2phases:
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.

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 Stack

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

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 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
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.ymlthat will be used by new CodeBuild construct:
version: 0.2phases:
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 changes to AWS CodeCommit
  • Compile the TypeScript file and run cdk deploy command in the Infrastructures:
npm run build
cdk deploy
Build and deploy change in Pipeline stack
  • In the AWS Console, the Pipeline has an update with the new stage for building Docker Image.
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.2phases:
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

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

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

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

CDK CloudFormation Outputs
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.

--

--