Posts Tagged CloudFormation
Amazon ECR Cross-Account Access for Containerized Applications on ECS
Posted by Gary A. Stafford in AWS, Build Automation, Cloud, DevOps, Go, Software Development on October 28, 2019
There is an updated April 2021 version of this post, which uses AWS CLI version 2 commands for ECR and updated versions of the Docker images. Please refer to this newer post.
Recently, I was asked a question regarding sharing Docker images from one AWS Account’s Amazon Elastic Container Registry (ECR) with another AWS Account who was deploying to Amazon Elastic Container Service (ECS) with AWS Fargate. The answer was relatively straightforward, use ECR Repository Policies to allow cross-account access to pull images. However, the devil is always in the implementation details. Constructing ECR Repository Policies can depend on your particular architecture, choice deployment tools, and method of account access. In this brief post, we will explore a common architectural scenario that requires configuring ECR Repository Policies to support sharing images across AWS Accounts.
Sharing Images
There are two scenarios I frequently encounter, which require sharing ECR-based Docker images across multiple AWS Accounts. In the first scenario, a vendor wants to securely share a Docker Image with their customer. Many popular container security and observability solutions function in this manner.
Below, we see an example where an application platform consists of three containers. Two of the container’s images originated from the customer’s own ECR repositories (right side). The third container’s image originated from their vendor’s ECR registry (left side).
In the second scenario, an enterprise operates multiple AWS Accounts to create logical security boundaries between environments and responsibilities. The first AWS Account contains the enterprise’s deployable binary assets, including ECR image repositories. The enterprise has additional accounts, one for each application environment, such as Dev, Test, Staging, and Production. The ECR images in the repository account need to be accessed from each of the environment accounts, often across multiple AWS Regions.
Below, we see an example where the deployed application platform consists of three containers, of which all images originated from the ECR repositories (left side). The images are pulled into the Production account for deployment to ECS (right side).
Demonstration
In this post, we will explore the first scenario, a vendor wants to securely share a Docker Image with their customer. We will demonstrate how to share images across AWS Accounts for use with Docker Swarm and ECS with Fargate, using ECR Repository Policies. To accomplish this scenario, we will use an existing application I have created, a RESTful, HTTP-based NLP (Natural Language Processing) API, consisting of three Golang microservices. The edge service, nlp-client
, communicates with the rake-app
service and the prose-app
service.
The scenario in the demonstration is that the customer has developed the nlp-client
and prose-app
container-based services, as part of their NLP application. Instead of developing their own implementation of the RAKE (Rapid Automatic Keyword Extraction) algorithm, they have licensed a version from a vendor, the rake-app
service, in the form of a Docker Image.
The NPL API exposes several endpoints, accessible through the nlp-client
service. The endpoints perform common NLP operations on text, such as extracting keywords, tokens, and entities. All the endpoints are visible by hitting the /routes
endpoint.
[ { "method": "POST", "path": "/tokens", "name": "main.getTokens" }, { "method": "POST", "path": "/entities", "name": "main.getEntities" }, { "method": "GET", "path": "/health", "name": "main.getHealth" }, { "method": "GET", "path": "/routes", "name": "main.getRoutes" }, { "method": "POST", "path": "/keywords", "name": "main.getKeywords" } ]
Requirements
To follow along with the demonstration, you will need two AWS Accounts, one representing the vendor and one representing one of their customers. It’s relatively simple to create additional AWS Accounts, all you need is a unique email address (easy with Gmail) and a credit card. Using AWS Organizations can make the task of creating and managing multiple accounts even easier.
I have purposefully used different AWS Regions within each account to demonstrate how you can share ECR images across both AWS Accounts and Regions. You will need a recent version of the AWS CLI and Docker. Lastly, you will need sufficient access to each AWS Account to create resources.
Source Code
The demonstration’s source code is contained in four public GitHub repositories. The first repository contains all the CloudFormation templates and the Docker Compose Stack file, as shown below.
. ├── LICENSE ├── README.md ├── cfn-templates │ ├── developer-user-group.yml │ ├── ecr-repo-not-shared.yml │ ├── ecr-repo-shared.yml │ ├── public-subnet-public-loadbalancer.yml │ └── public-vpc.yml └── docker └── stack.yml
Each of the other three GitHub repositories contains a single Go-based microservice, which together comprises the NLP application. Each respository also contains a Dockerfile.
. ├── Dockerfile ├── LICENSE ├── README.md ├── buildspec.yml └── main.go
The commands required to clone the four repositories are as follows.
git clone --branch master \ --single-branch --depth 1 --no-tags \ https://github.com/garystafford/ecr-cross-account-demo.git git clone --branch master \ --single-branch --depth 1 --no-tags \ https://github.com/garystafford/nlp-client.git git clone --branch master \ --single-branch --depth 1 --no-tags \ https://github.com/garystafford/prose-app.git git clone --branch master \ --single-branch --depth 1 --no-tags \ https://github.com/garystafford/rake-app.git
Process Overview
We will use AWS CloudFormation to create the necessary resources within both AWS Accounts. For the customer account, we will also use CloudFormation to create an ECS Cluster and an Amazon ECS Task Definition. The Task Definition defines how ECS will deploy our application, consisting of three Docker containers, using AWS Fargate. In addition to ECS, we will create an Amazon Virtual Private Cloud (VPC) to house the ECS cluster and a public-facing, Layer 7 Application Load Balancer (ALB) to load-balance our ECS-based application.
Throughout the post, I will use AWS Cloud9, the cloud-based integrated development environment (IDE), to execute all CloudFormation templates using the AWS CLI. I will also use Cloud9 to build and push the Docker images to the ECR repositories. Personally, I find Cloud9 easier to switch between multiple AWS Accounts and AWS Identity and Access Management (IAM) Users, using separate instances of Cloud9, verses using my local workstation. Conveniently, Cloud9 comes preinstalled with many of the tools you will need for this demonstration.
Creating ECR Repositories
In the first AWS Account, representing the vendor, we will execute two CloudFormation templates. The first template, developer-user-group.yml, creates the Development IAM Group and User. The Developer-01 IAM User will be given explicit access to the vendor’s rake-app
ECR repository. I suggest you change the DevUserPassword
parameter’s value to something more secure.
# change me IAM_USER_PSWD=T0pS3cr3Tpa55w0rD aws cloudformation create-stack \ --stack-name developer-user-group.yml \ --template-body file://cfn-templates/developer-user-group.yml \ --parameters \ ParameterKey=DevUserPassword,ParameterValue=${IAM_USER_PSWD} \ --capabilities CAPABILITY_NAMED_IAM
Below, we see an example of the resulting CloudFormation Stack showing the new Development IAM User and Group.
Next, we will execute the second CloudFormation template, ecr-repo-shared.yml, which creates the vendor’s rake-app
ECR image repository. The rake-app
repository will house a copy of the vendor’s rake-app
Docker Image. But first, let’s look at the CloudFormation template used to create the repository, specifically the RepositoryPolicyText
section. Here we define two repository policies:
- The
AllowPushPull
policy explicitly allows the Developer-01 IAM User to push and pull versions of the image to the ECR repository. We import the exported Amazon Resource Name (ARN) of the Developer-01 IAM User from the previous CloudFormation Stack Outputs. We have also allowed the AWS CodeBuild service access to the ECR repository. This is known as a Service-Linked Role. We will not use CodeBuild in this brief post. - The
AllowPull
policy allows anyone in the customer’s AWS Account (root
) to pull any version of the image. They cannot push, only pull. Of course, cross-account access can be restricted to a finer-grained set of the specific customer’s IAM Entities and source IP addresses.
Note the "ecr:GetAuthorizationToken"
policy Action. Later, when the customer needs to pull this vendor’s image, this Action will allow the customer’s User to log into the vendor’s ECR repository and receive an Authorization Token. The customer retrieves a token that is valid for a specified container registry for 12 hours.
RepositoryPolicyText: Version: '2012-10-17' Statement: - Sid: AllowPushPull Effect: Allow Principal: Service: codebuild.amazonaws.com AWS: Fn::ImportValue: !Join [':', [!Ref 'StackName', 'DevUserArn']] Action: - 'ecr:BatchCheckLayerAvailability' - 'ecr:BatchGetImage' - 'ecr:CompleteLayerUpload' - 'ecr:DescribeImages' - 'ecr:DescribeRepositories' - 'ecr:GetDownloadUrlForLayer' - 'ecr:GetRepositoryPolicy' - 'ecr:InitiateLayerUpload' - 'ecr:ListImages' - 'ecr:PutImage' - 'ecr:UploadLayerPart' - Sid: AllowPull Effect: Allow Principal: AWS: !Join [':', ['arn:aws:iam:', !Ref 'CustomerAccount', 'root']] Action: - 'ecr:GetAuthorizationToken' - 'ecr:BatchCheckLayerAvailability' - 'ecr:GetDownloadUrlForLayer' - 'ecr:BatchGetImage' - 'ecr:DescribeRepositories' # optional permission - 'ecr:DescribeImages' # optional permission
Before executing the following command to deploy the CloudFormation Stack, ecr-repo-shared.yml, replace the CustomerAccount
value, shown below, with your pseudo customer’s AWS Account ID.
# change me CUSTOMER_ACCOUNT=999888777666 # don't change me REPO_NAME=rake-app aws cloudformation create-stack \ --stack-name ecr-repo-${REPO_NAME} \ --template-body file://cfn-templates/ecr-repo-shared.yml \ --parameters \ ParameterKey=CustomerAccount,ParameterValue=${CUSTOMER_ACCOUNT} \ ParameterKey=RepoName,ParameterValue=${REPO_NAME} \ --capabilities CAPABILITY_NAMED_IAM
Below, we see an example of the resulting CloudFormation Stack showing the new ECR repository.
Below, we see the ECR repository policies applied correctly in the Permissions tab of the rake-app
repository. The first policy covers both the Developer-01 IAM User, referred to as an IAM Entity, as well as AWS CodeBuild, referred to as a Service Principal.
The second policy covers the customer’s AWS Account ID.
Repeat this process in the customer’s AWS Account. First, the CloudFormation template, developer-user-group.yml, containing Development IAM Group and Developer-01 User.
# change me IAM_USER_PSWD=T0pS3cr3Tpa55w0rD aws cloudformation create-stack \ --stack-name developer-user-group.yml \ --template-body file://cfn-templates/developer-user-group.yml \ --parameters \ ParameterKey=DevUserPassword,ParameterValue=${IAM_USER_PSWD} \ --capabilities CAPABILITY_NAMED_IAM
Next, we will execute the second CloudFormation template, ecr-repo-not-shared.yml, twice, once for each of the customer’s two ECR repositories, nlp-client
and prose-app
. First, let’s look at the template, specifically the RepositoryPolicyText
section. In this CloudFormation template, we only define a single policy. Identical to the vendor’s policy, the AllowPushPull
policy explicitly allows the previously-created Developer-01 IAM User to push and pull versions of the image to the ECR repository. There is no cross-account access required to the customer’s two ECR repositories.
RepositoryPolicyText: Version: '2012-10-17' Statement: - Sid: AllowPushPull Effect: Allow Principal: Service: codebuild.amazonaws.com AWS: Fn::ImportValue: !Join [':', [!Ref 'StackName', 'DevUserArn']] Action: - 'ecr:BatchCheckLayerAvailability' - 'ecr:BatchGetImage' - 'ecr:CompleteLayerUpload' - 'ecr:DescribeImages' - 'ecr:DescribeRepositories' - 'ecr:GetDownloadUrlForLayer' - 'ecr:GetRepositoryPolicy' - 'ecr:InitiateLayerUpload' - 'ecr:ListImages' - 'ecr:PutImage' - 'ecr:UploadLayerPart'
Execute the following commands to create the two CloudFormation Stacks. The Stacks use the same template with a different Stack name and RepoName
parameter values.
# nlp-client REPO_NAME=nlp-client aws cloudformation create-stack \ --stack-name ecr-repo-${REPO_NAME} \ --template-body file://cfn-templates/ecr-repo-not-shared.yml \ --parameters \ ParameterKey=RepoName,ParameterValue=${REPO_NAME} \ --capabilities CAPABILITY_NAMED_IAM # prose-app REPO_NAME=prose-app aws cloudformation create-stack \ --stack-name ecr-repo-${REPO_NAME} \ --template-body file://cfn-templates/ecr-repo-not-shared.yml \ --parameters \ ParameterKey=RepoName,ParameterValue=${REPO_NAME} \ --capabilities CAPABILITY_NAMED_IAM
Below, we see an example of the resulting two ECR repositories.
At this point, we have our three ECR repositories across the two AWS Accounts, with the proper ECR Repository Policies applied to each.
Building and Pushing Images to ECR
Next, we will build and push the three NLP application images to their corresponding ECR repositories. To confirm the ECR policies are working correctly, log in as the Developer-01 IAM User to perform the following actions.
Logged in as the vendor’s Developer-01 IAM User, build and push the Docker image to the rake-app
repository. The Dockerfile and Go source code is located in each GitHub repository. With Go and Docker multi-stage builds, we will make super small Docker images, based on Scratch, with just the compiled Go executable binary. At less than 10–20 MBs in size, pushing and pulling these Docker images, even across accounts, is very fast. Make sure you substitute the variable values below with your pseudo vendor’s AWS Account and Region. I am using the acroymn, ISV (Independent Software Vendor) for the vendor.
# change me ISV_ACCOUNT=111222333444 ISV_ECR_REGION=us-east-2 $(aws ecr get-login --no-include-email --region ${ISV_ECR_REGION}) docker build -t ${ISV_ACCOUNT}.dkr.ecr.${ISV_ECR_REGION}.amazonaws.com/rake-app:1.1.0 . --no-cache docker push ${ISV_ACCOUNT}.dkr.ecr.${ISV_ECR_REGION}.amazonaws.com/rake-app:1.1.0
Below, we see the output from the vendor’s Developer-01 IAM User logging into the rake-app
repository.
Then, we see the results of the vendor’s Development IAM User building and pushing the Docker Image to the rake-app
repository.
Next, logged in as the customer’s Developer-01 IAM User, build and push the Docker images to the ECR nlp-client
and prose-app
repositories. Again, make sure you substitute the variable values below with your pseudo customer’s AWS Account and preferred Region.
# change me CUSTOMER_ACCOUNT=999888777666 CUSTOMER_ECR_REGION=us-west-2 $(aws ecr get-login --no-include-email --region ${CUSTOMER_ECR_REGION}) docker build -t ${CUSTOMER_ACCOUNT}.dkr.ecr.${CUSTOMER_ECR_REGION}.amazonaws.com/nlp-client:1.1.0 . --no-cache docker push ${CUSTOMER_ACCOUNT}.dkr.ecr.${CUSTOMER_ECR_REGION}.amazonaws.com/nlp-client:1.1.0 docker build -t ${CUSTOMER_ACCOUNT}.dkr.ecr.${CUSTOMER_ECR_REGION}.amazonaws.com/prose-app:1.1.0 . --no-cache docker push ${CUSTOMER_ACCOUNT}.dkr.ecr.${CUSTOMER_ECR_REGION}.amazonaws.com/prose-app:1.1.0
At this point, each of the three ECR repositories has a Docker Image pushed to them.
Deploying Locally to Docker Swarm
As a simple demonstration of cross-account ECS access, we will start with Docker Swarm. Logged in as the customer’s Developer-01 IAM User and using the Docker Swarm Stack file included in the project, we can create and run a local copy of our NLP application in our customer’s account. First, we need to log into the vendor’s ECR repository in order to pull the image from the vendor’s ECR registry.
# change me ISV_ACCOUNT=111222333444 ISV_ECR_REGION=us-east-2 aws ecr get-login \ --registry-ids ${ISV_ACCOUNT} \ --region ${ISV_ECR_REGION} \ --no-include-email
The aws ecr get-login
command simplifies the login process by returning a (very lengthy) docker login
command in response (shown abridged below). According to AWS, the authorizationToken
returned for each registry specified is a base64 encoded string that can be decoded and used in a docker login
command to authenticate to an ECR registry.
docker login -u AWS -p eyJwYXlsb2FkI...joidENXMWg1WW0 \ https://111222333444.dkr.ecr.us-east-2.amazonaws.com
Copy, paste and execute the entire docker login
command back into your terminal. Below, we see an example of the expected terminal output from logging into the vendor’s ECR repository.
Once successfully logged in to the vendor’s ECR repository, we will pull the image. Using the docker describe-repositories
and docker describe-images
, we can list cross-account repositories and images your IAM User has access to if you are unsure.
aws ecr describe-repositories \ --registry-id ${ISV_ACCOUNT} \ --region ${ISV_ECR_REGION} \ --repository-name rake-app aws ecr describe-images \ --registry-id ${ISV_ACCOUNT} \ --region ${ISV_ECR_REGION} \ --repository-name rake-app docker pull ${ISV_ACCOUNT}.dkr.ecr.${ISV_ECR_REGION}.amazonaws.com/rake-app:1.1.0
Running the following command, you should see each of our three application Docker Images.
docker image ls --filter=reference='*amazonaws.com/*'
Below, we see an example of the expected terminal output from pulling the image and listing the images.
Build the Docker Stack Locally
Next, build the Docker Swarm Stack. The Docker Compose file, stack.yml, is shown below. Note the location of the Images.
version: '3.7' services: nlp-client: image: ${CUSTOMER_ACCOUNT}.dkr.ecr.${CUSTOMER_ECR_REGION}.amazonaws.com/nlp-client:1.1.0 networks: - nlp-demo ports: - 8080:8080 environment: - NLP_CLIENT_PORT - RAKE_ENDPOINT - PROSE_ENDPOINT - API_KEY rake-app: image: ${ISV_ACCOUNT}.dkr.ecr.${ISV_ECR_REGION}.amazonaws.com/rake-app:1.1.0 networks: - nlp-demo environment: - RAKE_PORT - API_KEY prose-app: image: ${CUSTOMER_ACCOUNT}.dkr.ecr.${CUSTOMER_ECR_REGION}.amazonaws.com/prose-app:1.1.0 networks: - nlp-demo environment: - PROSE_PORT - API_KEY networks: nlp-demo: volumes: data: {}
Execute the following commands to deploy the Docker Stack to Docker Swarm. Again, make sure you substitute the variable values below with your pseudo vendor and customer’s AWS Accounts and Regions. Additionally, API uses an API Key to protect all exposed endpoints, except the /health
endpoint, across all three services. You should change the default CloudFormation template’s API Key parameter to something more secure.
# change me export ISV_ACCOUNT=111222333444 export ISV_ECR_REGION=us-east-2 export CUSTOMER_ACCOUNT=999888777666 export CUSTOMER_ECR_REGION=us-west-2 export API_KEY=SuP3r5eCRetAutHK3y # don't change me export NLP_CLIENT_PORT=8080 export RAKE_PORT=8080 export PROSE_PORT=8080 export RAKE_ENDPOINT=http://rake-app:${RAKE_PORT} export PROSE_ENDPOINT=http://prose-app:${PROSE_PORT} docker swarm init docker stack deploy --compose-file stack.yml nlp
We can check the success of the deployment with the following commands.
docker stack ps nlp --no-trunc docker container ls
Below, we see an example of the expected terminal output.
With the Docker Stack, we can hit the nlp-client
service directly on localhost:8080
. Unlike Fargate, which requires unique static ports for each container in the task, with Docker, we can choose run all the containers on the same port without conflict since only the nlp-client
service is exposing port :8080
. Additionally, there is no load balancer in front of the Stack, unlike ECS, since we only have a single node in our Swarm, and thus a single container instance of each microservice.
To test that the images were pulled successfully and the Docker Stack is running, we can execute a curl command against any of the API endpoints, such as /keywords
. Below, I am using jq to pretty-print the JSON response payload.
#change me API_KEY=SuP3r5eCRetAutHK3y curl -s -X POST \ "http://localhost:${NLP_CLIENT_PORT}/keywords" \ -H 'Content-Type: application/json' \ -H "X-API-Key: ${API_KEY}" \ -d '{"text": "The Internet is the global system of interconnected computer networks that use the Internet protocol suite to link devices worldwide."}' | jq
The resulting JSON payload should look similar to the following output. These results indicate that the nlp-client
service was reached successfully and that it was then subsequently able to communicate with the rake-app
service, whose container image originated from the vendor’s ECR repository.
[ { "candidate": "interconnected computer networks", "score": 9 }, { "candidate": "link devices worldwide", "score": 9 }, { "candidate": "internet protocol suite", "score": 8 }, { "candidate": "global system", "score": 4 }, { "candidate": "internet", "score": 2 } ]
Creating Amazon ECS Environment
Although using Docker Swarm locally is a great way to understand how cross-account ECR access works, it is not a typical use case for deploying containerized applications on the AWS Platform. More often, you would use Amazon ECS, Amazon Elastic Kubernetes Service (EKS), or enterprise versions of third-party orchestrators such as Docker Enterprise, RedHat OpenShift, or Rancher.
Using CloudFormation and some very convenient CloudFormation templates supplied by Amazon as a starting point, we will create a complete ECS environment for our application. First, we will create a VPC to house the ECS cluster and a public-facing ALB to front our ECS-based application, using the public-vpc.yml template.
aws cloudformation create-stack \ --stack-name public-vpc \ --template-body file://public-vpc.yml \ --capabilities CAPABILITY_NAMED_IAM
Next, we will create the ECS cluster and an Amazon ECS Task Definition, using the public-subnet-public-loadbalancer.yml template. Again, the Task Definition defines how ECS will deploy our application using AWS Fargate. Amazon Fargate allows you to run containers without having to manage servers or clusters. No EC2 instances to manage! Woot! Below, in the CloudFormation template, we see the ContainerDefinitions
section of the TaskDefinition
resource, container three container definitions. Note the three images and their ECR locations.
ContainerDefinitions: - Name: nlp-client Cpu: 256 Memory: 1024 Image: !Join ['.', [!Ref AWS::AccountId, 'dkr.ecr', !Ref AWS::Region, 'amazonaws.com/nlp-client:1.1.0']] PortMappings: - ContainerPort: !Ref ContainerPortClient Essential: true LogConfiguration: LogDriver: awslogs Options: awslogs-region: !Ref AWS::Region awslogs-group: !Ref CloudWatchLogsGroup awslogs-stream-prefix: ecs Environment: - Name: NLP_CLIENT_PORT Value: !Ref ContainerPortClient - Name: RAKE_ENDPOINT Value: !Join [':', ['http://localhost', !Ref ContainerPortRake]] - Name: PROSE_ENDPOINT Value: !Join [':', ['http://localhost', !Ref ContainerPortProse]] - Name: API_KEY Value: !Ref ApiKey - Name: rake-app Cpu: 256 Memory: 1024 Image: !Join ['.', [!Ref VendorAccountId, 'dkr.ecr', !Ref VendorEcrRegion, 'amazonaws.com/rake-app:1.1.0']] Essential: true LogConfiguration: LogDriver: awslogs Options: awslogs-region: !Ref AWS::Region awslogs-group: !Ref CloudWatchLogsGroup awslogs-stream-prefix: ecs Environment: - Name: RAKE_PORT Value: !Ref ContainerPortRake - Name: API_KEY Value: !Ref ApiKey - Name: prose-app Cpu: 256 Memory: 1024 Image: !Join ['.', [!Ref AWS::AccountId, 'dkr.ecr', !Ref AWS::Region, 'amazonaws.com/prose-app:1.1.0']] Essential: true LogConfiguration: LogDriver: awslogs Options: awslogs-region: !Ref AWS::Region awslogs-group: !Ref CloudWatchLogsGroup awslogs-stream-prefix: ecs Environment: - Name: PROSE_PORT Value: !Ref ContainerPortProse - Name: API_KEY Value: !Ref ApiKey
Execute the following command to create the ECS cluster and an Amazon ECS Task Definition using the CloudFormation template.
# change me ISV_ACCOUNT=111222333444 ISV_ECR_REGION=us-east-2 AUTH_KEY=SuP3r5eCRetAutHK3y aws cloudformation create-stack \ --stack-name public-subnet-public-loadbalancer \ --template-body file://public-subnet-public-loadbalancer.yml \ --parameters \ ParameterKey=VendorAccountId,ParameterValue=${ISV_ACCOUNT} \ ParameterKey=VendorEcrRegion,ParameterValue=${ISV_ECR_REGION} \ ParameterKey=ApiKey,ParameterValue=${API_KEY} \ --capabilities CAPABILITY_NAMED_IAM
Below, we see an example of the expected output from the CloudFormation Management Console.
The CloudFormation template does not enable CloudWatch Container Insights by default. Insights collects, aggregates, and summarizes metrics and logs from your containerized applications. To enable Insights, execute the following command:
aws ecs put-account-setting \ --name "containerInsights" --value "enabled"
Confirming the Cross-account Policy
If everything went right in the previous steps, we should now have an ECS cluster running our containerized application, including the container built from the vendor’s Docker image. Below, we see an example of the ECS cluster, displayed in the management console.
Within the ECR cluster, we should observe a single running ECS Service. According to AWS, Amazon ECS allows you to run and maintain a specified number of instances of a task definition simultaneously in an Amazon ECS cluster. This is called a service.
We are running two instances of each container on ECS, thus two copies of the task within the single service. Each task runs its containers in a different Availability Zone for high-availability.
Drilling into the service, we should note the new ALB, associated with the new VPC, two public Subnets, and the corresponding Security Group.
Switching to the Task Definitions tab, we should see the details of our task. Note the three containers that compose the application. Note that two are located in the customer’s ECR repositories, and one is located in the vendor’s ECR repository.
Drilling in a little farther, we will see the details of each container definition, including environment variables, passed from ECR to the container, and on to the actual Go-binary, running in the container.
Reaching our Application on ECS
Whereas with our earlier Docker Swarm example, the curl command was issued against /localhost
, we now have the public-facing Application Load Balancer (ALB) in front of our ECS-based application. We will need to use the DNS name of your ALB as the host, to hit our application on ECS. The DNS address (A Record) can be obtained from the Load Balancer Management Console, as shown below, or from the Output tab of the public-vpc
CloudFormation Stack
Another difference between the earlier Docker Swarm example and ECS is the port. Although the edge service, nlp-client
, runs on port :8080
, the ALB acts as a reverse proxy, passing requests from port :80
on the ALB to port :8080
of the nlp-client
container instances (actually, the shared ENI of the running task). For the sake of brevity and simplicity, I did not set up a custom domain name for the ALB, nor HTTPS, as you normally would in Production.
To test our deployed ECS, we can use a tool like curl or Postman to test the API’s endpoints. Below, we see a POST against the /tokens
endpoint, using Postman. Don’t forget to you will need to add Auth, the ‘X-API-Key’ header key/value pair.
Cleaning Up
To clean up the demonstration’s AWS resources and Docker Stack, run the following scripts in the appropriate AWS Accounts. Importantly, similar to S3, you must delete all the Docker images in the ECR repositories first, before deleting the repository, or else you will receive a CloudFormation error. This includes untagged
images.
# customer account only aws ecr batch-delete-image \ --repository-name nlp-client \ --image-ids imageTag=1.1.0 aws ecr batch-delete-image \ --repository-name prose-app \ --image-ids imageTag=1.1.0 aws cloudformation delete-stack \ --stack-name ecr-repo-nlp-client aws cloudformation delete-stack \ --stack-name ecr-repo-prose-app aws cloudformation delete-stack \ --stack-name public-subnet-public-loadbalancer aws cloudformation delete-stack \ --stack-name public-vpc docker stack rm nlp # vendor account only aws ecr batch-delete-image \ --repository-name rake-app \ --image-ids imageTag=1.1.0 aws cloudformation delete-stack \ --stack-name ecr-repo-rake-app # both accounts aws cloudformation delete-stack \ --stack-name developer-user-group
Conclusion
In the preceding post, we saw how multiple AWS Accounts can share ECR-based Docker Images. There are variations and restrictions to the configuration of the ECR Repository Policies, depending on the deployment tools you are using, such as AWS CodeBuild, AWS CodeDeploy, or AWS Elastic Beanstalk. AWS does an excellent job of providing some examples in their documentation, including Amazon ECR Repository Policy Examples and Amazon Elastic Container Registry Identity-Based Policy Examples.
All opinions expressed in this post are my own and not necessarily the views of my current or past employers or their clients.
Getting Started with PostgreSQL using Amazon RDS, CloudFormation, pgAdmin, and Python
Posted by Gary A. Stafford in AWS, Cloud, Python, Software Development on August 9, 2019
Introduction
In the following post, we will explore how to get started with Amazon Relational Database Service (RDS) for PostgreSQL. CloudFormation will be used to build a PostgreSQL master database instance and a single read replica in a new VPC. AWS Systems Manager Parameter Store will be used to store our CloudFormation configuration values. Amazon RDS Event Notifications will send text messages to our mobile device to let us know when the RDS instances are ready for use. Once running, we will examine a variety of methods to interact with our database instances, including pgAdmin, Adminer, and Python.
Technologies
The primary technologies used in this post include the following.
PostgreSQL
According to its website, PostgreSQL, commonly known as Postgres, is the world’s most advanced Open Source relational database. Originating at UC Berkeley in 1986, PostgreSQL has more than 30 years of active core development. PostgreSQL has earned a strong reputation for its proven architecture, reliability, data integrity, robust feature set, extensibility. PostgreSQL runs on all major operating systems and has been ACID-compliant since 2001.
Amazon RDS for PostgreSQL
According to Amazon, Amazon Relational Database Service (RDS) provides six familiar database engines to choose from, including Amazon Aurora, PostgreSQL, MySQL, MariaDB, Oracle Database, and SQL Server. RDS is available on several database instance types - optimized for memory, performance, or I/O.
Amazon RDS for PostgreSQL makes it easy to set up, operate, and scale PostgreSQL deployments in the cloud. Amazon RDS supports the latest PostgreSQL version 11, which includes several enhancements to performance, robustness, transaction management, query parallelism, and more.
AWS CloudFormation
According to Amazon, CloudFormation provides a common language to describe and provision all the infrastructure resources within AWS-based cloud environments. CloudFormation allows you to use a JSON- or YAML-based template to model and provision all the resources needed for your applications across all AWS regions and accounts, in an automated and secure manner.
Demonstration
Architecture
Below, we see an architectural representation of what will be built in the demonstration. This is not a typical three-tier AWS architecture, wherein the RDS instances would be placed in private subnets (data tier) and accessible only by the application tier, running on AWS. The architecture for the demonstration is designed for interacting with RDS through external database clients such as pgAdmin, and applications like our local Python scripts, detailed later in the post.
Source Code
All source code for this post is available on GitHub in a single public repository, postgres-rds-demo.
. ├── LICENSE.md ├── README.md ├── cfn-templates │ ├── event.template │ ├── rds.template ├── parameter_store_values.sh ├── python-scripts │ ├── create_pagila_data.py │ ├── database.ini │ ├── db_config.py │ ├── query_postgres.py │ ├── requirements.txt │ └── unit_tests.py ├── sql-scripts │ ├── pagila-insert-data.sql │ └── pagila-schema.sql └── stack.yml
To clone the GitHub repository, execute the following command.
git clone --branch master --single-branch --depth 1 --no-tags \ https://github.com/garystafford/aws-rds-postgres.git
Prerequisites
For this demonstration, I will assume you already have an AWS account. Further, that you have the latest copy of the AWS CLI and Python 3 installed on your development machine. Optionally, for pgAdmin and Adminer, you will also need to have Docker installed.
Steps
In this demonstration, we will perform the following steps.
- Put CloudFormation configuration values in Parameter Store;
- Execute CloudFormation templates to create AWS resources;
- Execute SQL scripts using Python to populate the new database with sample data;
- Configure pgAdmin and Python connections to RDS PostgreSQL instances;
AWS Systems Manager Parameter Store
With AWS, it is typical to use services like AWS Systems Manager Parameter Store and AWS Secrets Manager to store overt, sensitive, and secret configuration values. These values are utilized by your code, or from AWS services like CloudFormation. Parameter Store allows us to follow the proper twelve-factor, cloud-native practice of separating configuration from code.
To demonstrate the use of Parameter Store, we will place a few of our CloudFormation configuration items into Parameter Store. The demonstration’s GitHub repository includes a shell script, parameter_store_values.sh
, which will put the necessary parameters into Parameter Store.
Below, we see several of the demo’s configuration values, which have been put into Parameter Store.
SecureString
Whereas our other parameters are stored in Parameter Store as String datatypes, the database’s master user password is stored as a SecureString data-type. Parameter Store uses an AWS Key Management Service (KMS) customer master key (CMK) to encrypt the SecureString parameter value.
SMS Text Alert Option
Before running the Parameter Store script, you will need to change the /rds_demo/alert_phone
parameter value in the script (shown below) to your mobile device number, including country code, such as ‘+12038675309’. Amazon SNS will use it to send SMS messages, using Amazon RDS Event Notification. If you don’t want to use this messaging feature, simply ignore this parameter and do not execute the event.template
CloudFormation template in the proceeding step.
aws ssm put-parameter \ --name /rds_demo/alert_phone \ --type String \ --value "your_phone_number_here" \ --description "RDS alert SMS phone number" \ --overwrite
Run the following command to execute the shell script, parameter_store_values.sh
, which will put the necessary parameters into Parameter Store.
sh ./parameter_store_values.sh
CloudFormation Templates
The GitHub repository includes two CloudFormation templates, cfn-templates/event.template
and cfn-templates/rds.template
. This event template contains two resources, which are an AWS SNS Topic and an AWS RDS Event Subscription. The RDS template also includes several resources, including a VPC, Internet Gateway, VPC security group, two public subnets, one RDS master database instance, and an AWS RDS Read Replica database instance.
The resources are split into two CloudFormation templates so we can create the notification resources, first, independently of creating or deleting the RDS instances. This will ensure we get all our SMS alerts about both the creation and deletion of the databases.
Template Parameters
The two CloudFormation templates contain a total of approximately fifteen parameters. For most, you can use the default values I have set or chose to override them. Four of the parameters will be fulfilled from Parameter Store. Of these, the master database password is treated slightly differently because it is secure (encrypted in Parameter Store). Below is a snippet of the template showing both types of parameters. The last two are fulfilled from Parameter Store.
DBInstanceClass: Type: String Default: "db.t3.small" DBStorageType: Type: String Default: "gp2" DBUser: Type: String Default: "{{resolve:ssm:/rds_demo/master_username:1}}" DBPassword: Type: String Default: "{{resolve:ssm-secure:/rds_demo/master_password:1}}" NoEcho: True
Choosing the default CloudFormation parameter values will result in two minimally-configured RDS instances running the PostgreSQL 11.4 database engine on a db.t3.small instance with 10 GiB of General Purpose (SSD) storage. The db.t3 DB instance is part of the latest generation burstable performance instance class. The master instance is not configured for Multi-AZ high availability. However, the master and read replica each run in a different Availability Zone (AZ) within the same AWS Region.
Parameter Versioning
When placing parameters into Parameter Store, subsequent updates to a parameter result in the version number of that parameter being incremented. Note in the examples above, the version of the parameter is required by CloudFormation, here, ‘1’. If you chose to update a value in Parameter Store, thus incrementing the parameter’s version, you will also need to update the corresponding version number in the CloudFormation template’s parameter.
{ "Parameter": { "Name": "/rds_demo/rds_username", "Type": "String", "Value": "masteruser", "Version": 1, "LastModifiedDate": 1564962073.774, "ARN": "arn:aws:ssm:us-east-1:1234567890:parameter/rds_demo/rds_username" } }
Validating Templates
Although I have tested both templates, I suggest validating the templates yourself, as you usually would for any CloudFormation template you are creating. You can use the AWS CLI CloudFormation validate-template
CLI command to validate the template. Alternately, or I suggest additionally, you can use CloudFormation Linter, cfn-lint
command.
aws cloudformation validate-template \ --template-body file://cfn-templates/rds.template cfn-lint -t cfn-templates/cfn-templates/rds.template
Create the Stacks
To execute the first CloudFormation template and create a CloudFormation Stack containing the two event notification resources, run the following create-stack
CLI command.
aws cloudformation create-stack \ --template-body file://cfn-templates/event.template \ --stack-name RDSEventDemoStack
The first stack only takes less than one minute to create. Using the AWS CloudFormation Console, make sure the first stack completes successfully before creating the second stack with the command, below.
aws cloudformation create-stack \ --template-body file://cfn-templates/rds.template \ --stack-name RDSDemoStack
Wait for my Text
In my tests, the CloudFormation RDS stack takes an average of 25–30 minutes to create and 15–20 minutes to delete, which can seem like an eternity. You could use the AWS CloudFormation console (shown below) or continue to use the CLI to follow the progress of the RDS stack creation.
However, if you recall, the CloudFormation event template creates an AWS RDS Event Subscription. This resource will notify us when the databases are ready by sending text messages to our mobile device.
In the CloudFormation events template, the RDS Event Subscription is configured to generate Amazon Simple Notification Service (SNS) notifications for several specific event types, including RDS instance creation and deletion.
MyEventSubscription: Properties: Enabled: true EventCategories: - availability - configuration change - creation - deletion - failover - failure - recovery SnsTopicArn: Ref: MyDBSNSTopic SourceType: db-instance Type: AWS::RDS::EventSubscription
Amazon SNS will send SMS messages to the mobile number you placed into Parameter Store. Below, we see messages generated during the creation of the two instances, displayed on an Apple iPhone.
Amazon RDS Dashboard
Once the RDS CloudFormation stack has successfully been built, the easiest way to view the results is using the Amazon RDS Dashboard, as shown below. Here we see both the master and read replica instances have been created and are available for our use.
The RDS dashboard offers CloudWatch monitoring of each RDS instance.
The RDS dashboard also provides detailed configuration information about each RDS instance.
The RDS dashboard’s Connection & security tab is where we can obtain connection information about our RDS instances, including the RDS instance’s endpoints. Endpoints information will be required in the next part of the demonstration.
Sample Data
Now that we have our PostgreSQL database instance and read replica successfully provisioned and configured on AWS, with an empty database, we need some test data. There are several sources of sample PostgreSQL databases available on the internet to explore. We will use the Pagila sample movie rental database by pgFoundry. Although the database is several years old, it provides a relatively complex relational schema (table relationships shown below) and plenty of sample data to query, about 100 database objects and 46K rows of data.
In the GitHub repository, I have included the two Pagila database SQL scripts required to install the sample database’s data structures (DDL), sql-scripts/pagila-schema.sql
, and the data itself (DML), sql-scripts/pagila-insert-data.sql
.
To execute the Pagila SQL scripts and install the sample data, I have included a Python script. If you do not want to use Python, you can skip to the Adminer section of this post. Adminer also has the capability to import SQL scripts.
Before running any of the included Python scripts, you will need to install the required Python packages and configure the database.ini
file.
Python Packages
To install the required Python packages using the supplied python-scripts/requirements.txt
file, run the below commands.
cd python-scripts pip3 install --upgrade -r requirements.txt
We are using two packages, psycopg2 and configparser, for the scripts. Psycopg is a PostgreSQL database adapter for Python. According to their website, Psycopg is the most popular PostgreSQL database adapter for the Python programming language. The configparser
module allows us to read configuration from files similar to Microsoft Windows INI files. The unittest package is required for a set of unit tests includes the project, but not discussed as part of the demo.
Database Configuration
The python-scripts/database.ini
file, read by configparser
, provides the required connection information to our RDS master and read replica instance’s databases. Use the input parameters and output values from the CloudFormation RDS template, or the Amazon RDS Dashboard to obtain the required connection information, as shown in the example, below. Your host
values will be unique for your master and read replica. The host values are the instance’s endpoint, listed in the RDS Dashboard’s Configuration tab.
[docker] host=localhost port=5432 database=pagila user=masteruser password=5up3r53cr3tPa55w0rd [master] host=demo-instance.dkfvbjrazxmd.us-east-1.rds.amazonaws.com port=5432 database=pagila user=masteruser password=5up3r53cr3tPa55w0rd [replica] host=demo-replica.dkfvbjrazxmd.us-east-1.rds.amazonaws.com port=5432 database=pagila user=masteruser password=5up3r53cr3tPa55w0rd
With the INI file configured, run the following command, which executes a supplied Python script, python-scripts/create_pagila_data.py
, to create the data structure and insert sample data into the master RDS instance’s Pagila database. The database will be automatically replicated to the RDS read replica instance. From my local laptop, I found the Python script takes approximately 40 seconds to create all 100 database objects and insert 46K rows of movie rental data. That is compared to about 13 seconds locally, using a Docker-based instance of PostgreSQL.
python3 ./create_pagila_data.py --instance master
The Python script’s primary function, create_pagila_db()
, reads and executes the two external SQL scripts.
def create_pagila_db(): """ Creates Pagila database by running DDL and DML scripts """ try: global conn with conn: with conn.cursor() as curs: curs.execute(open("../sql-scripts/pagila-schema.sql", "r").read()) curs.execute(open("../sql-scripts/pagila-insert-data.sql", "r").read()) conn.commit() print('Pagila SQL scripts executed') except (psycopg2.OperationalError, psycopg2.DatabaseError, FileNotFoundError) as err: print(create_pagila_db.__name__, err) close_conn() exit(1)
If the Python script executes correctly, you should see output indicating there are now 28 tables in our master RDS instance’s database.
pgAdmin
pgAdmin is a favorite tool for interacting with and managing PostgreSQL databases. According to its website, pgAdmin is the most popular and feature-rich Open Source administration and development platform for PostgreSQL.
The project includes an optional Docker Swarm stack.yml
file. The stack will create a set of three Docker containers, including a local copy of PostgreSQL 11.4, Adminer, and pgAdmin 4. Having a local copy of PostgreSQL, using the official Docker image, is helpful for development and trouble-shooting RDS issues.
Use the following commands to deploy the Swarm stack.
# create stack docker swarm init docker stack deploy -c stack.yml postgres # get status of new containers docker stack ps postgres --no-trunc docker container ls
If you do not want to spin up the whole Docker Swarm stack, you could use the docker run
command to create just a single pgAdmin Docker container. The pgAdmin 4 Docker image being used is the image recommended by pgAdmin.
docker pull dpage/pgadmin4 docker run -p 81:80 \ -e "PGADMIN_DEFAULT_EMAIL=user@domain.com" \ -e "PGADMIN_DEFAULT_PASSWORD=SuperSecret" \ -d dpage/pgadmin4 docker container ls | grep pgadmin4
Database Server Configuration
Once pgAdmin is up and running, we can configure the master and read replica database servers (RDS instances) using the connection string information from your database.ini
file or from the Amazon RDS Dashboard. Below, I am configuring the master RDS instance (server).
With that task complete, below, we see the master RDS instance and the read replica, as well as my local Docker instance configured in pgAdmin (left side of screengrab). Note how the Pagila database has been replicated automatically, from the RDS master to the read replica instance.
Building SQL Queries
Switching to the Query tab, we can run regular SQL queries against any of the database instances. Below, I have run a simple SELECT query against the master RDS instance’s Pagila database, returning the complete list of movie titles, along with their genre and release date.
The pgAdmin Query tool even includes an Explain tab to view a graphical representation of the same query, very useful for optimization. Here we see the same query, showing an analysis of the execution order. A popup window displays information about the selected object.
Query the Read Replica
To demonstrate the use of the read replica, below I’ve run the same query against the RDS read replica’s copy of the Pagila database. Any schema and data changes against the master instance are replicated to the read replica(s).
Adminer
Adminer is another good general-purpose database management tool, similar to pgAdmin, but with a few different capabilities. According to its website, with Adminer, you get a tidy user interface, rich support for multiple databases, performance, and security, all from a single PHP file. Adminer is my preferred tool for database admin tasks. Amazingly, Adminer works with MySQL, MariaDB, PostgreSQL, SQLite, MS SQL, Oracle, SimpleDB, Elasticsearch, and MongoDB.
Below, we see the Pagila database’s tables and views displayed in Adminer, along with some useful statistical information about each database object.
Similar to pgAdmin, we can also run queries, along with other common development and management tasks, from within the Adminer interface.
Import Pagila with Adminer
Another great feature of Adminer is the ability to easily import and export data. As an alternative to Python, you could import the Pagila data using Adminer’s SQL file import function. Below, you see an example of importing the Pagila database objects into the Pagila database, using the file upload function.
IDE
For writing my AWS infrastructure as code files and Python scripts, I prefer JetBrains PyCharm Professional Edition (v19.2). PyCharm, like all the JetBrains IDEs, has the ability to connect to and manage PostgreSQL database. You can write and run SQL queries, including the Pagila SQL import scripts. Microsoft Visual Studio Code is another excellent, free choice, available on multiple platforms.
Python and RDS
Although our IDE, pgAdmin, and Adminer are useful to build and test our queries, ultimately, we still need to connect to the Amazon RDS PostgreSQL instances and perform data manipulation from our application code. The GitHub repository includes a sample python script, python-scripts/query_postgres.py
. This script uses the same Python packages and connection functions as our Pagila data creation script we ran earlier. This time we will perform the same SELECT query using Python as we did previously with pgAdmin and Adminer.
cd python-scripts python3 ./query_postgres.py --instance master
With a successful database connection established, the scripts primary function, get_movies(return_count)
, performs the SELECT query. The function accepts an integer representing the desired number of movies to return from the SELECT query. A separate function within the script handles closing the database connection when the query is finished.
def get_movies(return_count=100): """ Queries for all films, by genre and year """ try: global conn with conn: with conn.cursor() as curs: curs.execute(""" SELECT title AS title, name AS genre, release_year AS released FROM film f JOIN film_category fc ON f.film_id = fc.film_id JOIN category c ON fc.category_id = c.category_id ORDER BY title LIMIT %s; """, (return_count,)) movies = [] row = curs.fetchone() while row is not None: movies.append(row) row = curs.fetchone() return movies except (psycopg2.OperationalError, psycopg2.DatabaseError) as err: print(get_movies.__name__, err) finally: close_conn() def main(): set_connection('docker') for movie in get_movies(10): print('Movie: {0}, Genre: {1}, Released: {2}' .format(movie[0], movie[1], movie[2]))
Below, we see an example of the Python script’s formatted output, limited to only the first ten movies.
Using the Read Replica
For better application performance, it may be optimal to redirect some or all of the database reads to the read replica, while leaving writes, updates, and deletes to hit the master instance. The script can be easily modified to execute the same query against the read replica rather than the master RDS instance by merely passing the desired section, ‘replica’ versus ‘master’, in the call to the set_connection(section)
function. The section parameter refers to one of the two sections in the database.ini
file. The configparser
module will handle retrieving the correct connection information.
set_connection('replica')
Cleaning Up
When you are finished with the demonstration, the easiest way to clean up all the AWS resources and stop getting billed is to delete the two CloudFormation stacks using the AWS CLI, in the following order.
aws cloudformation delete-stack \ --stack-name RDSDemoStack # wait until the above resources are completely deleted aws cloudformation delete-stack \ --stack-name RDSEventDemoStack
You should receive the following SMS notifications as the first CloudFormation stack is being deleted.
You can delete the running Docker stack using the following command. Note, you will lose all your pgAdmin server connection information, along with your local Pagila database.
docker stack rm postgres
Conclusion
In this brief post, we just scraped the surface of the many benefits and capabilities of Amazon RDS for PostgreSQL. The best way to learn PostgreSQL and the benefits of Amazon RDS is by setting up your own RDS instance, insert some sample data, and start writing queries in your favorite database client or programming language.
All opinions expressed in this post are my own and not necessarily the views of my current or past employers or their clients.
Managing AWS Infrastructure as Code using Ansible, CloudFormation, and CodeBuild
Posted by Gary A. Stafford in AWS, Build Automation, Cloud, Continuous Delivery, DevOps, Python, Technology Consulting on July 30, 2019
Introduction
When it comes to provisioning and configuring resources on the AWS cloud platform, there is a wide variety of services, tools, and workflows you could choose from. You could decide to exclusively use the cloud-based services provided by AWS, such as CodeBuild, CodePipeline, CodeStar, and OpsWorks. Alternatively, you could choose open-source software (OSS) for provisioning and configuring AWS resources, such as community editions of Jenkins, HashiCorp Terraform, Pulumi, Chef, and Puppet. You might also choose to use licensed products, such as Octopus Deploy, TeamCity, CloudBees Core, Travis CI Enterprise, and XebiaLabs XL Release. You might even decide to write your own custom tools or scripts in Python, Go, JavaScript, Bash, or other common languages.
The reality in most enterprises I have worked with, teams integrate a combination of AWS services, open-source software, custom scripts, and occasionally licensed products to construct complete, end-to-end, infrastructure as code-based workflows for provisioning and configuring AWS resources. Choices are most often based on team experience, vendor relationships, and an enterprise’s specific business use cases.
In the following post, we will explore one such set of easily-integrated tools for provisioning and configuring AWS resources. The tool-stack is comprised of Red Hat Ansible, AWS CloudFormation, and AWS CodeBuild, along with several complementary AWS technologies. Using these tools, we will provision a relatively simple AWS environment, then deploy, configure, and test a highly-available set of Apache HTTP Servers. The demonstration is similar to the one featured in a previous post, Getting Started with Red Hat Ansible for Google Cloud Platform.
Why Ansible?
With its simplicity, ease-of-use, broad compatibility with most major cloud, database, network, storage, and identity providers amongst other categories, Ansible has been a popular choice of Engineering teams for configuration-management since 2012. Given the wide variety of polyglot technologies used within modern Enterprises and the growing predominance of multi-cloud and hybrid cloud architectures, Ansible provides a common platform for enabling mature DevOps and infrastructure as code practices. Ansible is easily integrated with higher-level orchestration systems, such as AWS CodeBuild, Jenkins, or Red Hat AWX and Tower.
Technologies
The primary technologies used in this post include the following.
Red Hat Ansible
Ansible, purchased by Red Hat in October 2015, seamlessly provides workflow orchestration with configuration management, provisioning, and application deployment in a single platform. Unlike similar tools, Ansible’s workflow automation is agentless, relying on Secure Shell (SSH) and Windows Remote Management (WinRM). If you are interested in learning more on the advantages of Ansible, they’ve published a whitepaper on The Benefits of Agentless Architecture.
According to G2 Crowd, Ansible is a clear leader in the Configuration Management Software category, ranked right behind GitLab. Competitors in the category include GitLab, AWS Config, Puppet, Chef, Codenvy, HashiCorp Terraform, Octopus Deploy, and JetBrains TeamCity.
AWS CloudFormation
According to AWS, CloudFormation provides a common language to describe and provision all the infrastructure resources within AWS-based cloud environments. CloudFormation allows you to use a JSON- or YAML-based template to model and provision, in an automated and secure manner, all the resources needed for your applications across all AWS regions and accounts.
Codifying your infrastructure, often referred to as ‘Infrastructure as Code,’ allows you to treat your infrastructure as just code. You can author it with any IDE, check it into a version control system, and review the files with team members before deploying it.
AWS CodeBuild
According to AWS, CodeBuild is a fully managed continuous integration service that compiles your source code, runs tests, and produces software packages that are ready to deploy. With CodeBuild, you don’t need to provision, manage, and scale your own build servers. CodeBuild scales continuously and processes multiple builds concurrently, so your builds are not left waiting in a queue.
CloudBuild integrates seamlessly with other AWS Developer tools, including CodeStar, CodeCommit, CodeDeploy, and CodePipeline.
According to G2 Crowd, the main competitors to AWS CodeBuild, in the Build Automation Software category, include Jenkins, CircleCI, CloudBees Core and CodeShip, Travis CI, JetBrains TeamCity, and Atlassian Bamboo.
Other Technologies
In addition to the major technologies noted above, we will also be leveraging the following services and tools to a lesser extent, in the demonstration:
- AWS CodeCommit
- AWS CodePipeline
- AWS Systems Manager Parameter Store
- Amazon Simple Storage Service (S3)
- AWS Identity and Access Management (IAM)
- AWS Command Line Interface (CLI)
- CloudFormation Linter
- Apache HTTP Server
Demonstration
Source Code
All source code for this post is contained in two GitHub repositories. The CloudFormation templates and associated files are in the ansible-aws-cfn GitHub repository. The Ansible Roles and related files are in the ansible-aws-roles GitHub repository. Both repositories may be cloned using the following commands.
git clone --branch master --single-branch --depth 1 --no-tags \ https://github.com/garystafford/ansible-aws-cfn.git git clone --branch master --single-branch --depth 1 --no-tags \ https://github.com/garystafford/ansible-aws-roles.git
Development Process
The general process we will follow for provisioning and configuring resources in this demonstration are as follows:
- Create an S3 bucket to store the validated CloudFormation templates
- Create an Amazon EC2 Key Pair for Ansible
- Create two AWS CodeCommit Repositories to store the project’s source code
- Put parameters in Parameter Store
- Write and test the CloudFormation templates
- Configure Ansible and AWS Dynamic Inventory script
- Write and test the Ansible Roles and Playbooks
- Write the CodeBuild build specification files
- Create an IAM Role for CodeBuild and CodePipeline
- Create and test CodeBuild Projects and CodePipeline Pipelines
- Provision, deploy, and configure the complete web platform to AWS
- Test the final web platform
Prerequisites
For this demonstration, I will assume you already have an AWS account, the AWS CLI, Python, and Ansible installed locally, an S3 bucket to store the final CloudFormation templates and an Amazon EC2 Key Pair for Ansible to use for SSH.
Continuous Integration and Delivery Overview
In this demonstration, we will be building multiple CI/CD pipelines for provisioning and configuring our resources to AWS, using several AWS services. These services include CodeCommit, CodeBuild, CodePipeline, Systems Manager Parameter Store, and Amazon Simple Storage Service (S3). The diagram below shows the complete CI/CD workflow we will build using these AWS services, along with Ansible.
AWS CodeCommit
According to Amazon, AWS CodeCommit is a fully-managed source control service that makes it easy to host secure and highly scalable private Git repositories. CodeCommit eliminates the need to operate your own source control system or worry about scaling its infrastructure.
Start by creating two AWS CodeCommit repositories to hold the two GitHub projects your cloned earlier. Commit both projects to your own AWS CodeCommit repositories.
Configuration Management
We have several options for storing the configuration values necessary to provision and configure the resources on AWS. We could set configuration values as environment variables directly in CodeBuild. We could set configuration values from within our Ansible Roles. We could use AWS Systems Manager Parameter Store to store configuration values. For this demonstration, we will use a combination of all three options.
AWS Systems Manager Parameter Store
According to Amazon, AWS Systems Manager Parameter Store provides secure, hierarchical storage for configuration data management and secrets management. You can store data such as passwords, database strings, and license codes as parameter values, as either plain text or encrypted.
The demonstration uses two CloudFormation templates. The two templates have several parameters. A majority of those parameter values will be stored in Parameter Store, retrieved by CloudBuild, and injected into the CloudFormation template during provisioning.
The Ansible GitHub project includes a shell script, parameter_store_values.sh
, to put the necessary parameters into Parameter Store. The script requires the AWS Command Line Interface (CLI) to be installed locally. You will need to change the KEY_PATH
key value in the script (snippet shown below) to match the location your private key, part of the Amazon EC2 Key Pair you created earlier for use by Ansible.
KEY_PATH="/path/to/private/key" # put encrypted parameter to Parameter Store aws ssm put-parameter \ --name $PARAMETER_PATH/ansible_private_key \ --type SecureString \ --value "file://${KEY_PATH}" \ --description "Ansible private key for EC2 instances" \ --overwrite
SecureString
Whereas all other parameters are stored in Parameter Store as String datatypes, the private key is stored as a SecureString datatype. Parameter Store uses an AWS Key Management Service (KMS) customer master key (CMK) to encrypt the SecureString parameter value. The IAM Role used by CodeBuild (discussed later) will have the correct permissions to use the KMS key to retrieve and decrypt the private key SecureString parameter value.
CloudFormation
The demonstration uses two CloudFormation templates. The first template, network-stack.template
, contains the AWS network stack resources. The template includes one VPC, one Internet Gateway, two NAT Gateways, four Subnets, two Elastic IP Addresses, and associated Route Tables and Security Groups. The second template, compute-stack.template
, contains the webserver compute stack resources. The template includes an Auto Scaling Group, Launch Configuration, Application Load Balancer (ALB), ALB Listener, ALB Target Group, and an Instance Security Group. Both templates originated from the AWS CloudFormation template sample library, and were modified for this demonstration.
The two templates are located in the cfn_templates
directory of the CloudFormation project, as shown below in the tree view.
. ├── LICENSE.md ├── README.md ├── buildspec_files │ ├── build.sh │ └── buildspec.yml ├── cfn_templates │ ├── compute-stack.template │ └── network-stack.template ├── codebuild_projects │ ├── build.sh │ └── cfn-validate-s3.json ├── codepipeline_pipelines │ ├── build.sh │ └── cfn-validate-s3.json └── requirements.txt
The templates require no modifications for the demonstration. All parameters are in Parameter store or set by the Ansible Roles, and consumed by the Ansible Playbooks via CodeBuild.
Ansible
We will use Red Hat Ansible to provision the network and compute resources by interacting directly with CloudFormation, deploy and configure Apache HTTP Server, and finally, perform final integration tests of the system. In my opinion, the closest equivalent to Ansible on the AWS platform is AWS OpsWorks. OpsWorks lets you use Chef and Puppet (direct competitors to Ansible) to automate how servers are configured, deployed, and managed across Amazon EC2 instances or on-premises compute environments.
Ansible Config
To use Ansible with AWS and CloudFormation, you will first want to customize your project’s ansible.cfg
file to enable the aws_ec2
inventory plugin. Below is part of my configuration file as a reference.
[defaults] gathering = smart fact_caching = jsonfile fact_caching_connection = /tmp fact_caching_timeout = 300 host_key_checking = False roles_path = roles inventory = inventories/hosts remote_user = ec2-user private_key_file = ~/.ssh/ansible [inventory] enable_plugins = host_list, script, yaml, ini, auto, aws_ec2
Ansible Roles
According to Ansible, Roles are ways of automatically loading certain variable files, tasks, and handlers based on a known file structure. Grouping content by roles also allows easy sharing of roles with other users. For the demonstration, I have written four roles, located in the roles
directory, as shown below in the project tree view. The default, common
role is not used in this demonstration.
. ├── LICENSE.md ├── README.md ├── ansible.cfg ├── buildspec_files │ ├── buildspec_compute.yml │ ├── buildspec_integration_tests.yml │ ├── buildspec_network.yml │ └── buildspec_web_config.yml ├── codebuild_projects │ ├── ansible-test.json │ ├── ansible-web-config.json │ ├── build.sh │ ├── cfn-compute.json │ ├── cfn-network.json │ └── notes.md ├── filter_plugins ├── group_vars ├── host_vars ├── inventories │ ├── aws_ec2.yml │ ├── ec2.ini │ ├── ec2.py │ └── hosts ├── library ├── module_utils ├── notes.md ├── parameter_store_values.sh ├── playbooks │ ├── 10_cfn_network.yml │ ├── 20_cfn_compute.yml │ ├── 30_web_config.yml │ └── 40_integration_tests.yml ├── production ├── requirements.txt ├── roles │ ├── cfn_compute │ ├── cfn_network │ ├── common │ ├── httpd │ └── integration_tests ├── site.yml └── staging
The four roles include a role for provisioning the network, the cfn_network
role. A role for configuring the compute resources, the cfn_compute
role. A role for deploying and configuring the Apache servers, the httpd
role. Finally, a role to perform final integration tests of the platform, the integration_tests
role. The individual roles help separate the project’s major parts, network, compute, and middleware, into logical code files. Each role was initially built using Ansible Galaxy (ansible-galaxy init
). They follow Galaxy’s standard file structure, as shown in the tree view below, of the cfn_network
role.
. ├── README.md ├── defaults │ └── main.yml ├── files ├── handlers │ └── main.yml ├── meta │ └── main.yml ├── tasks │ ├── create.yml │ ├── delete.yml │ └── main.yml ├── templates ├── tests │ ├── inventory │ └── test.yml └── vars └── main.yml
Testing Ansible Roles
In addition to checking each role during development and on each code commit with Ansible Lint, each role contains a set of unit tests, in the tests
directory, to confirm the success or failure of the role’s tasks. Below we see a basic set of tests for the cfn_compute
role. First, we gather Facts about the deployed EC2 instances. Facts information Ansible can automatically derive from your remote systems. We check the facts for expected properties of the running EC2 instances, including timezone, Operating System, major OS version, and the UserID. Note the use of the failed_when
conditional. This Ansible playbook error handling conditional is used to confirm the success or failure of tasks.
--- - name: Test cfn_compute Ansible role gather_facts: True hosts: tag_Group_webservers pre_tasks: - name: List all ansible facts debug: msg: "{{ ansible_facts }}" tasks: - name: Check if EC2 instance's timezone is set to 'UTC' debug: msg: Timezone is UTC failed_when: ansible_facts['date_time']['tz'] != 'UTC' - name: Check if EC2 instance's OS is 'Amazon' debug: msg: OS is Amazon failed_when: ansible_facts['distribution_file_variety'] != 'Amazon' - name: Check if EC2 instance's OS major version is '2018' debug: msg: OS major version is 2018 failed_when: ansible_facts['distribution_major_version'] != '2018' - name: Check if EC2 instance's UserID is 'ec2-user' debug: msg: UserID is ec2-user failed_when: ansible_facts['user_id'] != 'ec2-user'
If we were to run the test on their own, against the two correctly provisioned and configured EC2 web servers, we would see results similar to the following.
In the cfn_network
role unit tests, below, note the use of the Ansible cloudformation_facts module. This module allows us to obtain facts about the successfully completed AWS CloudFormation stack. We can then use these facts to drive additional provisioning and configuration, or testing. In the task below, we get the network CloudFormation stack’s Outputs. These are the exact same values we would see in the stack’s Output tab, from the AWS CloudFormation management console.
--- - name: Test cfn_network Ansible role gather_facts: False hosts: localhost pre_tasks: - name: Get facts about the newly created cfn network stack cloudformation_facts: stack_name: "ansible-cfn-demo-network" register: cfn_network_stack_facts - name: List 'stack_outputs' from cached facts debug: msg: "{{ cloudformation['ansible-cfn-demo-network'].stack_outputs }}" tasks: - name: Check if the AWS Region of the VPC is {{ lookup('env','AWS_REGION') }} debug: msg: "AWS Region of the VPC is {{ lookup('env','AWS_REGION') }}" failed_when: cloudformation['ansible-cfn-demo-network'].stack_outputs['VpcRegion'] != lookup('env','AWS_REGION')
Similar to the CloudFormation templates, the Ansible roles require no modifications. Most of the project’s parameters are decoupled from the code and stored in Parameter Store or CodeBuild buildspec files (discussed next). The few parameters found in the roles, in the defaults/main.yml
files are neither account- or environment-specific.
Ansible Playbooks
The roles will be called by our Ansible Playbooks. There is a create
and a delete
set of tasks for the cfn_network
and cfn_compute
roles. Either create
or delete
tasks are accessible through the role, using the main.yml
file and referencing the create
or delete
Ansible Tags.
--- - import_tasks: create.yml tags: - create - import_tasks: delete.yml tags: - delete
Below, we see the create
tasks for the cfn_network
role, create.yml
, referenced above by main.yml
. The use of the cloudcormation module in the first task allows us to create or delete AWS CloudFormation stacks and demonstrates the real power of Ansible—the ability to execute complex AWS resource provisioning, by extending its core functionality via a module. By switching the Cloud module, we could just as easily provision resources on Google Cloud, Azure, AliCloud, OpenStack, or VMWare, to name but a few.
--- - name: create a stack, pass in the template via an S3 URL cloudformation: stack_name: "{{ stack_name }}" state: present region: "{{ lookup('env','AWS_REGION') }}" disable_rollback: false template_url: "{{ lookup('env','TEMPLATE_URL') }}" template_parameters: VpcCIDR: "{{ lookup('env','VPC_CIDR') }}" PublicSubnet1CIDR: "{{ lookup('env','PUBLIC_SUBNET_1_CIDR') }}" PublicSubnet2CIDR: "{{ lookup('env','PUBLIC_SUBNET_2_CIDR') }}" PrivateSubnet1CIDR: "{{ lookup('env','PRIVATE_SUBNET_1_CIDR') }}" PrivateSubnet2CIDR: "{{ lookup('env','PRIVATE_SUBNET_2_CIDR') }}" TagEnv: "{{ lookup('env','TAG_ENVIRONMENT') }}" tags: Stack: "{{ stack_name }}"
The CloudFormation parameters in the above task are mainly derived from environment variables, whose values were retrieved from the Parameter Store by CodeBuild and set in the environment. We obtain these external values using Ansible’s Lookup Plugins. The stack_name
variable’s value is derived from the role’s defaults/main.yml
file. The task variables use the Python Jinja2 templating system style of encoding.
The associated Ansible Playbooks, which call the tasks, are located in the playbooks
directory, as shown previously in the tree view. The playbooks define a few required parameters, like where the list of hosts will be derived and calls the appropriate roles. For our simple demonstration, only a single role is called per playbook. Typically, in a larger project, you would call multiple roles from a single playbook. Below, we see the Network playbook, playbooks/10_cfn_network.yml
, which calls the cfn_network
role.
--- - name: Provision VPC and Subnets hosts: localhost connection: local gather_facts: False roles: - role: cfn_network
Dynamic Inventory
Another principal feature of Ansible is demonstrated in the Web Server Configuration playbook, playbooks/30_web_config.yml
, shown below. Note the hosts to which we want to deploy and configure Apache HTTP Server is based on an AWS tag value, indicated by the reference to tag_Group_webservers
. This indirectly refers to an AWS tag, named Group, with the value of webservers
, which was applied to our EC2 hosts by CloudFormation. The ability to generate a Dynamic Inventory, using a dynamic external inventory system, is a key feature of Ansible.
--- - name: Configure Apache Web Servers hosts: tag_Group_webservers gather_facts: False become: yes become_method: sudo roles: - role: httpd
To generate a dynamic inventory of EC2 hosts, we are using the Ansible AWS EC2 Dynamic Inventory script, inventories/ec2.py
and inventories/ec2.ini
files. The script dynamically queries AWS for all the EC2 hosts containing specific AWS tags, belonging to a particular Security Group, Region, Availability Zone, and so forth.
I have customized the AWS EC2 Dynamic Inventory script’s configuration in the inventories/aws_ec2.yml
file. Amongst other configuration items, the file defines keyed_groups
. This instructs the script to inventory EC2 hosts according to their unique AWS tags and tag values.
plugin: aws_ec2 remote_user: ec2-user private_key_file: ~/.ssh/ansible regions: - us-east-1 keyed_groups: - key: tags.Name prefix: tag_Name_ separator: '' - key: tags.Group prefix: tag_Group_ separator: '' hostnames: - dns-name - ip-address - private-dns-name - private-ip-address compose: ansible_host: ip_address
Once you have built the CloudFormation compute stack in the proceeding section of the demonstration, to build the dynamic EC2 inventory of hosts, you would use the following command.
ansible-inventory -i inventories/aws_ec2.yml --graph
You would then see an inventory of all your EC2 hosts, resembling the following.
@all: |--@aws_ec2: | |--ec2-18-234-137-73.compute-1.amazonaws.com | |--ec2-3-95-215-112.compute-1.amazonaws.com |--@tag_Group_webservers: | |--ec2-18-234-137-73.compute-1.amazonaws.com | |--ec2-3-95-215-112.compute-1.amazonaws.com |--@tag_Name_Apache_Web_Server: | |--ec2-18-234-137-73.compute-1.amazonaws.com | |--ec2-3-95-215-112.compute-1.amazonaws.com |--@ungrouped:
Note the two EC2 web servers instances, listed under tag_Group_webservers
. They represent the target inventory onto which we will install Apache HTTP Server. We could also use the tag, Name, with the value tag_Name_Apache_Web_Server
.
AWS CodeBuild
Recalling our diagram, you will note the use of CodeBuild is a vital part of each of our five DevOps workflows. CodeBuild is used to 1) validate the CloudFormation templates, 2) provision the network resources, 3) provision the compute resources, 4) install and configure the web servers, and 5) run integration tests.
Splitting these processes into separate workflows, we can redeploy the web servers without impacting the compute resources or redeploy the compute resources without affecting the network resources. Often, different teams within a large enterprise are responsible for each of these resources categories—architecture, security (IAM), network, compute, web servers, and code deployments. Separating concerns makes a shared ownership model easier to manage.
Build Specifications
CodeBuild projects rely on a build specification or buildspec file for its configuration, as shown below. CodeBuild’s buildspec file is synonymous to Jenkins’ Jenkinsfile. Each of our five workflows will use CodeBuild. Each CodeBuild project references a separate buildspec file, included in the two GitHub projects, which by now you have pushed to your two CodeCommit repositories.
Below we see an example of the buildspec file for the CodeBuild project that deploys our AWS network resources, buildspec_files/buildspec_network.yml
.
version: 0.2 env: variables: TEMPLATE_URL: "https://s3.amazonaws.com/garystafford_cloud_formation/cf_demo/network-stack.template" AWS_REGION: "us-east-1" TAG_ENVIRONMENT: "ansible-cfn-demo" parameter-store: VPC_CIDR: "/ansible_demo/vpc_cidr" PUBLIC_SUBNET_1_CIDR: "/ansible_demo/public_subnet_1_cidr" PUBLIC_SUBNET_2_CIDR: "/ansible_demo/public_subnet_2_cidr" PRIVATE_SUBNET_1_CIDR: "/ansible_demo/private_subnet_1_cidr" PRIVATE_SUBNET_2_CIDR: "/ansible_demo/private_subnet_2_cidr" phases: install: runtime-versions: python: 3.7 commands: - pip install -r requirements.txt -q build: commands: - ansible-playbook -i inventories/aws_ec2.yml playbooks/10_cfn_network.yml --tags create -v post_build: commands: - ansible-playbook -i inventories/aws_ec2.yml roles/cfn_network/tests/test.yml
There are several distinct sections to the buildspec file. First, in the variables
section, we define our variables. They are a combination of three static variable values and five variable values retrieved from the Parameter Store. Any of these may be overwritten at build-time, using the AWS CLI, SDK, or from the CodeBuild management console. You will need to update some of the variables to match your particular environment, such as the TEMPLATE_URL
to match your S3 bucket path.
Next, the phases
of our build. Again, if you are familiar with Jenkins, think of these as Stages with multiple Steps. The first phase, install
, builds a Docker container, in which the build process is executed. Here we are using Python 3.7. We also run a pip command to install the required Python packages from our requirements.txt
file. Next, we perform our build
phase by executing an Ansible command.
ansible-playbook \ -i inventories/aws_ec2.yml \ playbooks/10_cfn_network.yml --tags create -v
The command calls our playbook, playbooks/10_cfn_network.yml
. The command references the create
tag. This causes the playbook to run to cfn_network
role’s create tasks (roles/cfn_network/tasks/create.yml
), as defined in the main.yml
file (roles/cfn_network/tasks/main.yml
). Lastly, in our post_build
phase, we execute our role’s unit tests (roles/cfn_network/tests/test.yml
), using a second Ansible command.
CodeBuild Projects
Next, we need to create CodeBuild projects. You can do this using the AWS CLI or from the CodeBuild management console (shown below). I have included individual templates and a creation script in each project, in the codebuild_projects
directory, which you could use to build the projects, using the AWS CLI. You would have to modify the JSON templates, replacing all references to my specific, unique AWS resources, with your own. For the demonstration, I suggest creating the five projects manually in the CodeBuild management console, using the supplied CodeBuild project templates as a guide.
CodeBuild IAM Role
To execute our CodeBuild projects, we need an IAM Role or Roles CodeBuild with permission to such resources as CodeCommit, S3, and CloudWatch. For this demonstration, I chose to create a single IAM Role for all workflows. I then allowed CodeBuild to assign the required policies to the Role as needed, which is a feature of CodeBuild.
CodePipeline Pipeline
In addition to CodeBuild, we are using CodePipeline for our first of five workflows. CodePipeline validates the CloudFormation templates and pushes them to our S3 bucket. The pipeline calls the corresponding CodeBuild project to validate each template, then deploys the valid CloudFormation templates to S3.
In true CI/CD fashion, the pipeline is automatically executed every time source code from the CloudFormation project is committed to the CodeCommit repository.
CodePipeline calls CodeBuild, which performs a build, based its buildspec file. This particular CodeBuild buildspec file also demonstrates another ability of CodeBuild, executing an external script. When we have a complex build phase, we may choose to call an external script, such as a Bash or Python script, verses embedding the commands in the buildspec.
version: 0.2 phases: install: runtime-versions: python: 3.7 pre_build: commands: - pip install -r requirements.txt -q - cfn-lint -v build: commands: - sh buildspec_files/build.sh artifacts: files: - '**/*' base-directory: 'cfn_templates' discard-paths: yes
Below, we see the script that is called. Here we are using both the CloudFormation Linter, cfn-lint
, and the cloudformation validate-template
command to validate our templates for comparison. The two tools give slightly different, yet relevant, linting results.
#!/usr/bin/env bash set -e for filename in cfn_templates/*.*; do cfn-lint -t ${filename} aws cloudformation validate-template \ --template-body file://${filename} done
Similar to the CodeBuild project templates, I have included a CodePipeline template, in the codepipeline_pipelines
directory, which you could modify and create using the AWS CLI. Alternatively, I suggest using the CodePipeline management console to create the pipeline for the demo, using the supplied CodePipeline template as a guide.
Below, the stage view of the final CodePipleine pipeline.
Build the Platform
With all the resources, code, and DevOps workflows in place, we should be ready to build our platform on AWS. The CodePipeline project comes first, to validate the CloudFormation templates and place them into your S3 bucket. Since you are probably not committing new code to the CloudFormation file CodeCommit repository, which would trigger the pipeline, you can start the pipeline using the AWS CLI (shown below) or via the management console.
# list names of pipelines aws codepipeline list-pipelines # execute the validation pipeline aws codepipeline start-pipeline-execution --name cfn-validate-s3
The pipeline should complete within a few seconds.
Next, execute each of the four CodeBuild projects in the following order.
# list the names of the projects aws codebuild list-projects # execute the builds in order aws codebuild start-build --project-name cfn-network aws codebuild start-build --project-name cfn-compute # ensure EC2 instance checks are complete before starting # the ansible-web-config build! aws codebuild start-build --project-name ansible-web-config aws codebuild start-build --project-name ansible-test
As the code comment above states, be careful not to start the ansible-web-config build until you have confirmed the EC2 instance Status Checks have completed and have passed, as shown below. The previous, cfn-compute
build will complete when CloudFormation finishes building the new compute stack. However, the fact CloudFormation finished does not indicate that the EC2 instances are fully up and running. Failure to wait will result in a failed build of the ansible-web-config
CodeBuild project, which installs and configures the Apache HTTP Servers.
Below, we see the cfn_network
CodeBuild project first building a Python-based Docker container, within which to perform the build. Each build is executed in a fresh, separate Docker container, something that can trip you up if you are expecting a previous cache of Ansible Facts or previously defined environment variables, persisted across multiple builds.
Below, we see the two completed CloudFormation Stacks, a result of our CodeBuild projects and Ansible.
The fifth and final CodeBuild build tests our platform by attempting to hit the Apache HTTP Server’s default home page, using the Application Load Balancer’s public DNS name.
Below, we see an example of what happens when a build fails. In this case, one of the final integration tests failed to return the expected results from the ALB endpoint.
Below, with the bug is fixed, we rerun the build, which re-executed the tests, successfully.
We can manually confirm the platform is working by hitting the same public DNS name of the ALB as our tests in our browser. The request should load-balance our request to one of the two running web server’s default home page. Normally, at this point, you would deploy your application to Apache, using a software continuous deployment tool, such as Jenkins, CodeDeploy, Travis CI, TeamCity, or Bamboo.
Cleaning Up
To clean up the running AWS resources from the demonstration, first delete the CloudFormation compute stack, then delete the network stack. To do so, execute the following commands, one at a time. The commands call the same playbooks we called to create the stacks, except this time, we use the delete
tag, as opposed to the create
tag.
# first delete cfn compute stack ansible-playbook \ -i inventories/aws_ec2.yml \ playbooks/20_cfn_compute.yml -t delete -v # then delete cfn network stack ansible-playbook \ -i inventories/aws_ec2.yml \ playbooks/10_cfn_network.yml -t delete -v
You should observe the following output, indicating both CloudFormation stacks have been deleted.
Confirm the stacks were deleted from the CloudFormation management console or from the AWS CLI.
All opinions expressed in this post are my own and not necessarily the views of my current or past employers or their clients.