Provisioning AWS solutions in minutes with Infra as Github Actions

Alonso Suarez - May 31 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'}}
Enter fullscreen mode Exit fullscreen mode

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?

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .