How to Set Up a CI/CD Workflow, Part Two: Deploying Docker Images in AWS Using ECS - HedgeDoc
  2505 views
How to Set Up a CI/CD Workflow, Part Two: Deploying Docker Images in AWS Using ECS == There are huge benefits to having a continuous deployment (CD) strategy. Automated deployments mean your team can pursue faster release cycles and experiment with live features more frequently. Automated processes mean fewer people have to worry about how to do something manually. A good CD setup is also essential when teams reach a certain size: having a clear process to release helps to avoid miscommunication. In a [previous article](https://monadical.com/posts/set-up-ci-workflow.html), I explained how to set up a pipeline to send images from GitHub to AWS ECR. In this article, I will focus on implementing the missing steps required for a fully functional CD pipeline. <center> ![docker logo](https://docs.monadical.com/uploads/upload_217069157fa4ce37bab61da18501434c.png) </center> There are several services for deploying images to production environments in the AWS ecosystem. I will explore a pipeline that takes the images received at AWS ECR and deploys them to AWS ECS using AWS CodePipeline. At a very high level, the AWS services that I will use to set up the pipeline are: - AWS ECR (Elastic Container Registry): Repository where you upload your images. - AWS ECS (Elastic Container Service): The service that will serve as the deployment target for your images. This could be replaced by EBS (Elastic Beanstalk) or EKS (Elastic Kubernetes Service) with some modifications. - AWS CodePipeline: This service listens to events and reacts to them. It can be configured to serve a wide range of use cases. In this case, it will listen to AWS ECR image uploads. - AWS ELB (Elastic Load Balancer): This balances the load between the available instances. This demonstration will only use a single container, but AWS ELB is still needed to enable a public interface for the service. <center> Target Architecture </center> ![target architecture](https://docs.monadical.com/uploads/upload_966497560d2d09ccb5a50dcb381eb6a2.png) The first thing you need is a functioning ECR repository. If you don’t know how to set one up, have a look at my article, [From GitHub Actions to AWS ECR](https://monadical.com/posts/set-up-ci-workflow.html). To create the load balancer, go to the EC2 console and select 'Create Load Balancer' in the Load Balancers section: Create a new Application Load Balancer: ![create load balancer](https://docs.monadical.com/uploads/upload_564ceed8afa652261c077e6f7b06ed26.png) <center> ![application load balancer](https://docs.monadical.com/uploads/upload_c794e62c43e2e92045bc9c1471f50392.png) </center> Enter a name and enable a couple of availability zones: <center> ![configure load balancer](https://docs.monadical.com/uploads/upload_3bb7efbac629a6ff0c1b226cdc760827.png) </center> Leave the defaults for the next step. Take note of the subnets you use, as they will need to match the ones you assign to the cluster service later on. (If you don’t do this, you might have issues with health checks.) You will get a warning about not using a secure endpoint (HTTPS). This can be explored later, but it will require an additional service to manage a SSL certificate and, depending on the setup, there may be a significant amount of work to do. How to set up a secure connection is out of the scope of this blog post, but if you’re interested in how to do that, check out [the official documentation](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-update-certificates.html). For the security group, just enable anything that allows traffic from anywhere to port 80. If there aren’t any available, you can create a new one with these requirements: ![security groups](https://docs.monadical.com/uploads/upload_4a7c706be3e1c0c6ad67470087dc493b.png) In the `Configure Routing` section, create a new group, assign a name, and leave the rest as is (http in port 80 with target type Instance). Leave the rest of the steps with the defaults and create the load balancer. The next resource you will need is a cluster to deploy to. However, this requires a task definition, which you will need to create first. Go to AWS Elastic Container Service in the AWS console: <center> ![console container services](https://docs.monadical.com/uploads/upload_9d8b533e8b4f28fe450c2c3aff036c6a.png) </center> The task definition will dictate how your task will be deployed and which strategy and resources you will use: ![task definition - launch type](https://docs.monadical.com/uploads/upload_ecececf7e3a4a5c27c7c57649b1e61d4.png) Use FARGATE and click continue. In the next step, pick a name for your task definition, and select your preferred task size: ![task definition - task size](https://docs.monadical.com/uploads/upload_a01c4dbabf84e6b1b3bb4ec3d3e1b332.png) <center> ![container definition](https://docs.monadical.com/uploads/upload_58e4936a35427e2db10b4737b319a8cf.png) </center> For the next step you will need your ECR repository URL. Remember it has the following structure: `(aws_account_id.dkr.ecr.region.amazonaws.com)`. You can also find this information by going to the ECR service and checking it. If you prefer, you can use images from Docker Hub. Whatever name you use in this step (nginx in this case) will be needed later, so make a note of it. As mentioned in the [documentation](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#standard_container_definition_params), after you create your cluster, it will use these container definitions to pull the latest image and tag from the repository. However, subsequent updates to the images will not be propagated to running instances. You will need to follow some additional steps to prevent stale containers. In the case of the nginx image, you will need a port mapping to port 80, to allow the service to forward the traffic in that port to the container. Also, in the health check you can add `/docker-entrypoint.sh` in the command field. This is the command that this container uses to start and is what the task definition will use to check it is still running. ![task definition creation success](https://docs.monadical.com/uploads/upload_14775191f7d8978e89326b5209e2a7ce.png) If everything has been entered correctly, you can now create the cluster. Click on ‘Clusters’, and then create a cluster: <center> ![clusters - elastic container service](https://docs.monadical.com/uploads/upload_ad1af3935d7a46d7f44f66614f8fce3b.png) </center> You can leave the first template as it is: ![cluster template](https://docs.monadical.com/uploads/upload_e7bfce56e3266e7f96690e047b38b086.png) Pick a name for your cluster. You may want to create a new VPC if you want to keep it isolated from the rest of your services, however, this is optional. If you create a new VPC, remember to update your load balancer, or it will be unable to find your cluster. ![](https://docs.monadical.com/uploads/upload_21bfeea5387c384e3f1abf149365bf6f.png) You should see a screen like this: ![configure cluster](https://docs.monadical.com/uploads/upload_3a697cbcea32dd69f6e6e119b301eb26.png) If you click ‘View Cluster’, you should see something similar to this: ![cluster success](https://docs.monadical.com/uploads/upload_53abb280875b7dd1fc55e3b5e03e1fd6.png) In the services tab, click ‘Create’: ![ecs - services list](https://docs.monadical.com/uploads/upload_465d1dc30876a4ea52d39363170c4a8e.png) Pick FARGATE, and set the number of tasks to 1 (you will need to change this in a production setup, because replication is important for availability). You can use the service name of your choice. For task definition, pick the one you just created a couple of steps back. For the next step, you will set up the load balancer you created earlier. Use the same subnets in the networking section and select the proper application load balancer: ![ecs - configure service](https://docs.monadical.com/uploads/upload_0678b41e09227cec068c41aebe7676fe.png) In the load balancing section, you will see that our `nginx` container is listed as available to load balance. Click on ‘Add to Load Balancer’: ![ecs - configure network](https://docs.monadical.com/uploads/upload_11ddf0428e90beece1409023c16b89a2.png) You will need to modify the settings according to your container after you click ‘Add to load balancer’: ![load balancer configuration](https://docs.monadical.com/uploads/upload_8a5ce3e568274f6aab929bd6c1e8e7de.png) Notice that the `Path pattern` is not empty by default. Set it to `/`. Proceed with the defaults for the last step, and create the service. You will eventually have the task running in your service: ![load balancer configuration](https://docs.monadical.com/uploads/upload_e4ff2499b427ccb89c73762b7145ab1a.png) Go back to the EC2 console and check the load balancer. Once the health check is blue, we are ready to go: ![load balancer health check](https://docs.monadical.com/uploads/upload_19102d7b3837dcf8ba7369e41142a849.png) You can copy the DNS name to your browser. It should work properly now: ![load balancer dns](https://docs.monadical.com/uploads/upload_25713488e6609ecace8c3eab34a980aa.png) Now you are ready for the next step: you need to set up a pipeline in AWS CodePipeline. To do this, go to the AWS console and click on ‘CodePipeline’: <center> ![nginx response](https://docs.monadical.com/uploads/upload_30bfd1e4526e90232e1f14b440af621b.png) </center> Once there, you should see this screen: ![console developer tools](https://docs.monadical.com/uploads/upload_8ef2d401fde45b0491abd3a28c91072b.png) To get started, click `Create pipeline`: ![create pipeline](https://docs.monadical.com/uploads/upload_7cf0aaad35c24b5a6c617f3b70c0fe51.png) Input a name, and leave the rest as it is by default. On the next screen, select AWS ECR as the source: ![code pipeline settings](https://docs.monadical.com/uploads/upload_714580cb9f3b846057cf396a47fefd13.png) This pipeline must listen to your ECR repository so that it’s triggered every time a new image is uploaded with the `latest` tag. In my case, I’ll be using the ECR repository I created for the previous blog post. For the build step, select AWS CodeBuild as build provider and create a new project: <center> ![create build project](https://docs.monadical.com/uploads/upload_60c0aa5562540ec2f930d02e06d75473.png) </center> This is necessary because the source step does not return an artifact compatible with the deploy stage requirements. We need to generate a file: `imagedefinitions.json`. Give the project a name and set up the environment as follows: ![code build environment](https://docs.monadical.com/uploads/upload_4c0b5a1cadb48d778436adc903d5c803.png) In the buildspec, you will need to have the [following](https://stackoverflow.com/questions/58849736/did-not-find-the-image-definition-file-imagedefinitions-json): ```yaml version: 0.2 phases: install: runtime-versions: docker: 18 build: commands: - apt-get install jq -y - ContainerName="nginx" - ImageURI=$(cat imageDetail.json | jq -r '.ImageURI') - printf '[{"name":"CONTAINER_NAME","imageUri":"IMAGE_URI"}]' > imagedefinitions.json - sed -i -e "s|CONTAINER_NAME|$ContainerName|g" imagedefinitions.json - sed -i -e "s|IMAGE_URI|$ImageURI|g" imagedefinitions.json - cat imagedefinitions.json artifacts: files: - imagedefinitions.json ``` Be careful with the `ContainerName`. If you used a different name in your container definition while setting up your ECS cluster, you will need to use that name instead. ![code build buildspec](https://docs.monadical.com/uploads/upload_449f6db16e91640ed72267da70719920.png) Finally, the build stage should look like this: ![code pipeline build stage](https://docs.monadical.com/uploads/upload_65b43b29be8718a3f63b5285d3821e39.png) In the deploy stage, add the ECS cluster definitions you created earlier: <center> ![code pipeline deploy stage](https://docs.monadical.com/uploads/upload_fabdc85abc2dcdf3652bd3dfd26c98eb.png) </center> Click ‘Next’ and ‘Create the pipeline’ to finish. The pipeline will now run and after it is finished, you should see something similar to this: ![create pipeline success](https://docs.monadical.com/uploads/upload_79199b8eb77e0db3e4219136799c6cdd.png) Hold your applause -- this pipeline hasn’t done anything interesting yet. After all, your setup was already fetching an image and booting up correctly before you set this up. The difference, however, is that now whenever you upload a new image to ECR with the `latest` tag, it will automatically be deployed. You can test this by updating your application and checking that it has changed. In my case, I updated the static file that the Docker image was serving and, after creating a new release in my repo (which triggers the action to upload to ECR), it was automatically deployed: ![successful nginx message](https://docs.monadical.com/uploads/upload_1a68df31872da78b85e2dee6869862ae.png) There are a lot of improvements that you can implement on this setup, but this will serve as a solid basis. If you don’t have an overwhelming number of tasks to run, this is a cost effective way to deploy high availability services. For example, you will want to play with blue/green deployments to avoid the small downtime introduced when the new container is starting up. If you follow the instructions in this blog post and the [previous one](https://monadical.com/posts/set-up-ci-workflow.html), you should have a fully functional continuous deployment pipeline. This will reduce the need for someone to manually deploy updates and services. Your team will be able to focus on improving your applications and won’t have to worry about how to update them every time you need a new version out. You can also check the [part three](https://monadical.com/posts/set-up-ci-workflow-part-three.html), where I briefly explain how to setup a different container, along with some basic testing and linting for your project. --- <center> <img src="https://monadical.com/static/logo-black.png" style="height: 80px"/><br/> Monadical.com | Full-Stack Consultancy *We build software that outlasts us* </center> ---



Recent posts:


Back to top