Integrating AWS CDK into GitHub Actions
We want to be able to have changes to our infrastructure-as-code be deployed automatically. Specifically, we want changes to our repository to result in the following:
- Build and test code our code.
- Build our website.
- Deploy our website infrastructure, including updating our website content.
Choice of GitHub Actions as CI/CD
Now, one might be forgiven for thinking that if we’re going to be deploying code to AWS that we might make use of AWS features, such as CodePipeline and CodeBuild. However, GitHub Actions are completely free, whereas the AWS services are not. Let’s go with the free option…
AWS Credentials
Create an IAM user with programmatic to AWS and store the access key and secret access key as secrets in the GitHub repository. The permissions associated with the user will be determined by the resources you define within your CDK stack. Let’s assume these secrets are called AWS_ACCESS_KEY_ID
and AWS_SECRET_ACCESS_KEY
. GitHub Actions will make these secrets available to the actions and we don’t need to store them in source code anywhere. We’ll also assume that there is another secret called AWS_REGION
, but clearly neither this nor AWS_ACCESS_KEY_ID
actually need to be secret - we’ll just store them in GitHub as secrets to completely decouple our AWS deployment details from the source code.
GitHub Actions Workflow
At its simplest, a GitHub Action comprises a single workflow, which is a sequence of steps to carry out. You can refer to other actions as part of these steps, where those actions may be defined by you or others. For our purposes, we’ll just define a single workflow.
Workflow
Our workflow definition file is inside .github/workflows/build.yml
(you’re free to choose whatever workflow name you wish). We define the name of the workflow and when we want it to be triggered (on every push to the repository). We want it to run on a Ubuntu runner and perform a series of steps when it is triggered.
1name: Build23on: [push]45jobs:6 build:7 runs-on: ubuntu-latest8 steps:9 ... steps go here
Our first steps involve checking out the code from the repository and then configuring Node.js. I need to install the AWS CDK and Gatsby - you may not need Gatsby if you’re building your website some other way. We define some caching of build dependencies to reduce build times for subsequent runs.
1- uses: actions/checkout@v223 - name: Use Node.js4 uses: actions/setup-node@v15 with:6 node-version: '14.x'78 - name: Cache Node.js modules9 uses: actions/cache@v210 with:11 path: ~/.npm12 key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}13 restore-keys: |14 ${{ runner.OS }}-node-15 ${{ runner.OS }}-1617 - name: Install CDK and Gatsby18 run: |19 npm install -g aws-cdk@1.62.0 gatsby-cli
We then configure Java and Gradle with caching of Gradle dependencies:
1- name: Set up JDK 1.112 uses: actions/setup-java@v13 with:4 java-version: 1.1156 - name: Cache Gradle packages7 uses: actions/cache@v28 with:9 path: ~/.gradle/caches10 key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}11 restore-keys: ${{ runner.os }}-gradle
Now we’re ready to define some steps which will build our project’s CDK and generate the website. Clearly, you can put in whatever unit tests you wish for your application code: remember, CDK is just developing in your normal coding language.
1- name: Build with Gradle2 run: ./gradlew build34 - name: Build Gatsby5 run: |6 pushd web7 npm ci8 gatsby clean9 gatsby build10 popd
Next step is to synthesize the CDK stack, which, as you’ll recall, requires AWS credentials to perform a Route53 HostedZone lookup. We want to synthesize the CDK stack every time we push a change, as this is a good way of determining if there’s an issue with our CDK: some errors won’t be apparent until you try to synthesize them.
1- name: Synth CDK2 run: |3 ./gradlew cdkPrepare4 cdk synth \5 --app 'java -jar ./infrastructure/build/cdk/infrastructure-all.jar -apiLambdaPath ./infrastructure/build/cdk/api-lambdas.zip -webAssets ./infrastructure/build/cdk/web -domainName johntipper.org -region ${{ secrets.AWS_REGION }} -targetAccount ${{ secrets.AWS_TARGET_ACCOUNT }}' \6 --output build/cdk.out7 env:8 AWS_REGION: ${{ secrets.AWS_REGION }}9 AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}10 AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}11 AWS_TARGET_ACCOUNT: ${{ secrets.AWS_TARGET_ACCOUNT }}
Finally, we want to deploy the synthesized CDK, if we’re on the master branch:
1- name: Deploy CDK2 run: |3 cdk deploy --app ./build/cdk.out --require-approval never "*"4 if: github.ref == 'refs/heads/master'5 env:6 AWS_REGION: ${{ secrets.AWS_REGION }}7 AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}8 AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Status
In our repository README.md, we’ll define an image, which GitHub will update whenever a build occurs. That image will (hopefully) show a green badge showing that our build works. We can refer to that image using this syntax:
1![Build status](https://github.com/<org>/<repo>/workflows/Build/badge.svg "GitHub Actions Build Status")
where Build
is the name of the workflow that we define in our workflow file.