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

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.