Ever needed to do CI on a centralised CloudFormation repository, and struggled to get template validation done quickly? Here's how you can do it.
Why?
Ideally, Infrastructure as Code lives in the repo alongside the code that gets deployed on to it. Sometimes reality is different, and you have a lot of CloudFormation templates all in one repository.
Amazon provide a CloudFormation template validation tool using the AWS CLI (aws cloudformation validate-template --template-body file://myfile.yml
). This method has some limitations:
- Requires you to be authenticated
- Is slow due to network latency
- Running in parallel risks API throttling
- Raises error on long (but still valid) templates
- Misses non-fatal errors (duplicate keys, unused parameters, etc.)
I wanted a tool that could be run both on our CI platform and locally, and required no internet access. For a while I tried running cfn-lint. It struggled to cope with the large library of questionably maintained CloudFormation templates that I needed to validate. One day, I stumbled across cfn-python-lint.
Files
docker-compose.yml
version: '3'
services:
cfn-python-lint:
image: amaysim/cfn-python-lint:0.3.3
network_mode: "none"
entrypoint: ''
working_dir: /srv/app
volumes:
- .:/srv/app:Z
Makefile
FILES := $(shell find . \( -name "*.y*ml" -o -name "*.json" \) -not -name "docker-compose.yml")
test:
docker-compose run cfn-python-lint make -j 8 _test
_test: $(FILES)
$(FILES):
cfn-lint -t "$@"
.PHONY: $(FILES)
How To Run
Make sure you have Make, docker-compose and Docker installed, then run:
make test
Explanation
cfn-python-lint is run by callingcfn-lint -t "<FILENAME>"
. On failure it outputs the failing lines and exits with a non-0 exit code, making it perfect for CI pipelines.
However, cfn-python-lint can only run against a single template at a time, which is far too slow. We will take advantage of the capabilities of Make to parallelise it.
First, we use the find
command to build a list of any YAML or JSON files under the working directory. If this was picking up unrelated files, we can exclude them by appending -not -name ".gitlab-ci.yml"
to the find command.
The list of generated files is stored in the $(FILES)
variable. For each entry in the $(FILES)
variable, we call cfn-lint
. Normally this would execute sequentially, so we use the -j
flag to run in parallel. This drastically increases the speed at which we can validate templates. .PHONY
is used to work around the incremental build feature of Make.
This all runs inside the amaysim/cfn-python-lint:0.3.3 container which has all the dependencies to run cfn-python-lint preinstalled. docker-compose is used to manage the various flags needed to run the image such as mount points and the entrypoint.
The docker-compose command itself is stored as a Make target call locally or in our CI tool:
make test