Secure cloud provisioning pipeline with GitHub automation

Rishav DharRishav Dhar
5 min read

As a member of the Platforms engineering team, we understand that security is a shared responsibility throughout the DevSecOps lifecycle for provisioning infrastructure. As a result, we set about championing best practices across the organization, with a focus on:

  1. Configuring short-lived credentials

  2. Automating cloud-provisioning pipelines

  3. Comparing infrastructure-as-code tooling

  4. Securing deployments from code-to-delivery

Figure: How to provision infrastructure-as-code.


Short-lived credentials

GitHub Actions form the basis of our continuous integration/continuous deployment (CI/CD) pipeline as it integrates seamlessly with GitOps: the framework by which we ship peer-reviewed code early and often. It enables us to extend our workflow with Actions from verified creators, such as aws-actions (other cloud computing platforms are available).

This streamlines the process of configuring AWS credentials via OpenID Connect (OIDC): our favorite “keyless” authentication method. OIDC only permits access from federated providers using short-lived credentials, which pairs perfectly with GitHub Actions’ ephemeral runners. What’s more, we can grant least-privilege provisioning permissions to the assumed-role (and include both thumbprints). Let’s add it to our workflow.

Tip While the following pseudo-code is for legibility, we will build up to the complete workflow by the end of this post.

jobs:
  provision:
    runs-on: ubuntu

    steps:
      - name: Authenticate AWS
        uses: aws-actions/configure-aws-credentials

Figure: Security hardening GitHub Action workflow with OpenID Connect (source).


Automated provisioning pipeline

With authentication sorted and access to infrastructure secured, we turned to Test Double for inspiration on a “roll-your-own” deployment workflow.

It goes on to link DevSecTop/TF-via-PR: another open-source project which combines the outlined concepts into a reusable Action. In support of trunk-based Pull Request (PR) automation, this workflow:

  1. Runs init > workspace > plan with optional arguments when a PR is opened, returning the plan output in a PR comment and storing the encrypted plan file as an artifact.

  2. Reruns plan when the PR is updated with new commits, returning the latest output and replacing the old PR comment to reduce noise.

  3. Runs init > workspace > apply with the previous plan file artifact, along with optional arguments when the PR is approved and merged.

Tip Passing in the previously-generated plan file when applying prevents any configuration drift from stale plans.

By default, each PR and associated workflow runs hold a complete log of infrastructure change/proposals for ease of collaboration and audit compliance within a set retention period. The use of GitHub Actions also removes the overhead of maintaining dedicated containers or self-hosted compute instances: lending itself to empower development teams to self-service scalably. Time to add another step to our workflow.

on:
  pull_request:

jobs:
  provision:
    runs-on: ubuntu

    steps:
      - name: Authenticate AWS
        uses: aws-actions/configure-aws-credentials

      - name: Checkout repository
        uses: actions/checkout

      - name: Provision infrastructure
        uses: devsectop/tf-via-pr

Figure: Flow chart of provisioning pipeline steps.


Infrastructure-as-code tooling

Although plan file encryption is one of the options provided by the Action, it’s also a feature native to OpenTofu: a project backed by The Linux Foundation as an open-source alternative to Terraform.

More recently, OpenTofu released support for early static evaluation of variables/locals, enabling the likes of module versions and backend inputs to be loosely coupled with don’t-repeat-yourself (DRY) configuration. While a number of teams have successfully experimented by replacing terraform with tofu, many others are holding out for dynamically configured provider support as well as Dependabot automation before making the switch.

Tip For anyone else experimenting between the two, whole-heartedly recommend tofuutils/tenv as the go-to package manager of choice with broad compatibility.


Secured deployments workflow

With the GitHub Action dependencies accounted for, all that’s left is to bring them together in a workflow that is reactive to the provisioning result. In other words, if apply fails for any reason, then reject the PR from merge and return the error output in a PR comment for follow-up. This happens to be the ideal use-case for a merge queue of size and concurrency one.

Now that we have all the pieces to form the complete workflow below, can you spot what else has been done to shore up security? Answers below!

on:
  pull_request:
    branches: [main]
  merge_group:
    types: [checks_requested]

jobs:
  provision:
    runs-on: ubuntu-24.04

    permissions:
      actions: read        # Required to download artifact.
      checks: write        # Required to add status summary.
      contents: read       # Required to checkout repository.
      id-token: write      # Required to authenticate via OIDC.
      pull-requests: write # Required to add PR comment and label.

    env:
      AWS_ACCOUNT: platforms-dev
      AWS_REGION: us-east-1
      AWS_ROLE: arn:aws:iam::123456789012:role/provision-cicd

    environment: ${{ github.event_name == 'merge_group' && env.AWS_ACCOUNT || '' }}

    steps:
      - name: Authenticate AWS
        uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
        with:
          aws-region: ${{ env.AWS_REGION }}
          role-to-assume: ${{ env.AWS_ROLE }}

     - name: Checkout repository
       uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
        with:
          persist-credentials: false

      - name: Setup TF
        uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
        with:
          terraform_version: "1.8.5"

      - name: Provision infrastructure
        uses: devsectop/tf-via-pr@f1acaae1d94826457fa57bc65f1df318fd81b3bc # v12.0.0
        with:
          command: ${{ github.event_name == 'merge_group' && 'apply' || 'plan' }}
          arg-lock: ${{ github.event_name == 'merge_group' }}
          working-directory: path/to/${{ env.AWS_ACCOUNT }}
          plan-encrypt: ${{ secrets.TF_ENCRYPTION }}

Next steps

There’s plenty of potential with this workflow: from interpolating workspaces with dynamic backends to bulk-provisioning multiple accounts in matrix strategy concurrently. Is there any particular setup or tooling you’d like us to explore next?

0
Subscribe to my newsletter

Read articles from Rishav Dhar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rishav Dhar
Rishav Dhar

Based in Edinburgh UK, where I’m engaged in a remote role as a Senior DevOps Platform Engineer at Wavelo: https://linkedin.com/in/RishavDhar