I remember those days when I created infra by clicking in the console 😬 eventually that became a nightmare to manage and infra as code came to save the day 🦸 but with that, it also started an awkward phase of coupling infra code and app code
Some platform teams decided to move infra code to their own repos, that worked, specially for access control, but that required exceptions like:
- Ignoring the image version since it was going to be managed by the app pipeline
- The awkward environment variables setup
I've been searching for ways to bring infra back infra code to app repos
The first experiment was to keep network and LBs and move ECS services with Fargate to the app repo, this continuous to work surprisingly well after 3 years, environment variables changes are in the same PR as app that depended on it, another advantage was that terraform itself managed the newly built docker image tag. However, app developers rarely touched most of the terraform code, as terraform requires significant effort to learn, there has to be a better way 🤔
Entering Infra as GitHub Actions
With just a few line of YML, we can create pretty complex depended workflows, what if we can use Github Actions to compose infra?
I write a couple of actions that will provision the S3, CloudFront and Route53 with fairly simple steps:
- Login to AWS
- Setup the Backend
- Provision the website
The whole workflow relies on only 3 inputs- Instance name: an identifier for the infra- Domain: DNS for the website, you need to own it on Route53- Path: content to publish as root on the website
permissions:
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out repo
uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: us-east-1
role-to-assume: ${{ secrets.ROLE_ARN }}
role-session-name: ${{ github.actor }}
- uses: alonch/actions-aws-backend-setup@main
with:
instance: demo
- uses: alonch/actions-aws-website@main
with:
domain: ${{ env.DOMAIN }}
content-path: public
How it works
When a GitHub Action job is initialized, it checks out its source code from the repo, this unlocks interesting capabilities, for example: an action could apply terraform as if the terraform code is part of the current repo
actions-aws-backend-setup (repo)
- uses: alonch/actions-aws-backend-setup@main
with:
instance: demo
This action requires a custom instance name to query AWS by tag, we need to find the S3 and Dynamodb to setup the terraform backend
In case where it doesn’t exist, it will provision it and setup the environment variables:
- TF_BACKEND_s3: bucket name
- TF_BACKEND_dynamodb: table name
actions-aws-website (repo)
- uses: alonch/actions-aws-website@main
with:
domain: ${{ env.DOMAIN }}
content-path: public
This action assumes the backend setup has run, and it requires a domain and the path to the content that needs to be publish as the website
Similar to the backend action, GitHub checks the source code which includes the terraform code to provision a Bucket, Cloudfront, Certificate and route53 routes
Using Github Actions is incredible flexible as the only dependency is on the AWS role and the backend instance name, in the case were we need to upgrade the infra, we just need to tag the new action version and terraform will sync the resources to the latest desired state
New degree of freedom
With Infra as Github Actions we basically can achieve ephemeral environments with a 31 lines of YML
name: Deploy Ephemeral Environment
on:
pull_request:
types: [opened, synchronize, reopened, closed]
env:
DOMAIN: ${{github.head_ref}}.test.realsense.ca
permissions:
id-token: write
jobs:
deploy:
environment:
url: "https://${{ env.DOMAIN }}"
name: ${{github.head_ref}}
runs-on: ubuntu-latest
steps:
- name: Check out repo
uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: us-east-1
role-to-assume: ${{ secrets.ROLE_ARN }}
role-session-name: ${{ github.actor }}
- uses: alonch/actions-aws-backend-setup@main
with:
instance: demo
- uses: alonch/actions-aws-website@main
with:
domain: ${{ env.DOMAIN }}
content-path: public
# destroy when PR closed
action: ${{github.event.action == 'closed' && 'destroy' || 'apply'}}
This will create a new infra for that PR, update it in sync and destroy it when the PR is closed
What’s next?
This is just the beginning, I believe GitHub actions dependency could be use to compose and orchestre complex infra with simple interfaces
I'm considering building:
- actions-aws-http-lambda: Serverless API from folder path
- actions-aws-edge-auth: Website behind social media login from client secrets
- actions-aws-http-server: Web hosting from docker image
What do you think? What should I focus on next?