Building a dynamic AWS Pipeline with CDK
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.
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
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 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
}
}
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:
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 proxyremindersmgtservice
: -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
We can use AWS Console and going to DeveloperTools -> CodeCommit
to confirm new repository - FriendRemindersV2
has been created successfully.
- 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
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 theInfrastructures
to keep all buildspec file. - In the new folder
RemindersManagement
, create a buildspec file calledunittestspec.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: noreports:
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 ContainerLinuxBuildImage.STANDARD_4_0
to run all steps defined in theunittestspec.yml
file. It usesource
property to refer source code that being stored by CodeCommit RepositoryFriendRemindersV2
. - 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 attachAdministratorAccess
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
When CDK deploying is completed, we can go to CloudFormation -> Stacks
in AWS Console to confirm new stack:
Using AWS Console, we can also go to CodeBuild project, DeveloperTools -> CodeBuild
to confirm the new Pipeline has been created and executed.
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
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.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
- Compile the TypeScript file and run cdk deploy command in the
Infrastructures
:
npm run build
cdk deploy
- In the AWS Console, the Pipeline has an update with the new stage for building Docker Image.
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
In AWS Console, we can see Deploy stage has been added in the Pipeline:
Going to the CloudFormation -> Stacks
in AWS Console, we can see ECS Cluster’s resources are being created in the RemindersManagementBuildStack
stack:
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.
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.