Container deployment to ECS and CloudFormation

The case

I was tasked to build the CI/CD process for application running on ECS. In general, the process should contain the following steps:

  • Getting the code
  • Building the docker image
  • Pushing the docker to image repo
  • Creation of a new Task Definition
  • Updating the task

The ECS cluster, LoadBalancer and Target groups are already existing.

The flow

The code is stored in SCM - in our case the CodeCommit will be used. Once the new code arrives to the SCM - it should trigger the build and deployment mechanics. To achieve this - the CodePipeline can be used. This powerful tool is designed to build pipelines (oh, that's evident :D). The build will be done on Jenkins machine, that will publish the artifacts to S3 bucket; once the build is done - the CodePipeline will transfer the artifacts to CloudFormation, that will take care of creating/updating the service.

Jenkins

So let's start with creation of Jenkins job, that will actually build the docker image and will generate the artifacts for us. Leaving the manipulation with code out of the scope, as actually we are speaking about pipeline itself.

The first thing to do is: to install AWS CodePipiline plugin In Source Code Management section please choose the "AWS CodePipeline" option. Provide the rest of the settings according to your configuration.

Note: You need to provide your Secret key and Access key only if your Jenkins machine is not EC2 instance. Otherwise you have to use IAM Role to control the permissions.

Please, pay attention to options under CodePipeline Action Type section. It's very important to put here relevant values, as you will need them to build the CodePipeline.

Jenkins configuration

Move forward to Build Triggers section, and configure the Poll SCM option to * * * * *

To finish the Jenkins side - add the AWS CodePipeline Publisher as post-build action.

Post-build action

In the post-build action configuration, add the Output Location and leave the both fields blank. We're done here. Let's move forward to CodePipeline.

CodePipeline

Our pipeline will have 3 steps: Source, Build and Deployment:

Pipeline

The source step will transfer the source code from SCM to the Build step, that is represented by Jenkins.

source

Make sure, that the Output Artifacts are defined (e.g. MyApp). These artifacts should be transferred to Build step.

The Build step will be fulfilled by Jenkins. In our case, the build step should do the following:

  1. Build the docker image
  2. Push the image to image Repository (ECR in our case)
  3. Generate the artifacts for CloudFormation (Deploy step)

Build

Make sure, that your build step is getting the input artifact from source step (MyApp) and provides the output artifact (BuiltApp). In our case, we need to provide the CloudFormation Template and CloudFormation configuration.

The general idea here - is to use generic parametrised template and handle the configuration during the build. So the template can be provided as part of the code, and can be stored in SCM, while configuration should be built during the build.

Here is the example of basic template and configuration files:

template.yml:

Parameters:
  taskdefinitionparam:
    Type: String
  imageparam:
    Type: String
  containerPort:
    Type: Number
  hostPort:
    Type: Number
  desiredCount:
    Type: Number
  ecsCluster:
    Type: String
  tgArn:
    Type: String
Resources:
  servicetaskdefinition: 
    Type: "AWS::ECS::TaskDefinition"
    Properties: 
      Family: !Ref 'taskdefinitionparam'
      ContainerDefinitions: 
        - 
          Name: !Ref 'taskdefinitionparam'
          Image: !Ref imageparam
          PortMappings: 
            - 
              ContainerPort: !Ref 'containerPort'
              HostPort: !Ref 'hostPort'
          Essential: "true"
          Memory: "256"
  nginxservice:
    Type: "AWS::ECS::Service"
    Properties:
      Cluster: !Ref 'ecsCluster'
      DesiredCount: !Ref 'desiredCount'
      LoadBalancers:
      - ContainerName: !Ref 'taskdefinitionparam'
        ContainerPort: !Ref 'containerPort'
        TargetGroupArn: !Ref 'tgArn'
      TaskDefinition: !Ref 'servicetaskdefinition'
      Role: "arn:aws:iam::111111111111:role/ecsServiceRole"

conf.json:

{
  "Parameters" : {
    "taskdefinitionparam" : "Nginx",
    "imageparam" : "111111111111.dkr.ecr.us-east-1.amazonaws.com/nginx:latest",
    "containerPort" : "80",
    "hostPort": "0",
    "ecsCluster" : "arn:aws:ecs:us-east-1:111111111111:cluster/testCluster",
    "tgArn" : "arn:aws:elasticloadbalancing:us-east-1:111111111111:targetgroup/TestTG/c311ea2a1656b7a1",
    "desiredCount": "2"
  }
}

In given example the configuration file should be generated by Jenkins. Then, Jenkins packs the workspace and stores the package as the artifact on S3 bucket (AWS CodePipeline Publisher action). This is BuiltApp artifact, that will be used on Deployment stage.

CloudFormation (Deployment stage)

As all the files were packed to the artifact, we can easily pick the needed in Pipeline step configuration:

Deployment

So each file should be defined in the following form: ArtifactName::FileName in our case the template is BuiltApp::template.yml, the configuration - BuiltApp::conf.json.

We are done! Now once code is pushed to CodeCommit - the pipeline will be triggered, Jenkins will build new docker image, push it to image repository and will provide updated configuration. This configuration will be used to create or update the CloudFormation stack, containing the task definition and service arrangement.