Skip to main content

Command Palette

Search for a command to run...

How to automate Terraform deployments with GitHub Actions.

Updated
4 min read
How to automate Terraform deployments with GitHub Actions.

I know… There is lots of articles on this rather basic topic. I’m not consider myself an expert. Maybe someone find helpful a HOWTO on this particular combination of technologies.

TL;DR

I’m going to create a simple workflow for deploying a Terraform stack on AWS using GitHub Action. I will setup OIDC authentication between GitHub and AWS.

Prerequisites

We will need:

  • a simple, working Terraform project,

  • an AWS account.

  • a GitHub account.

AWS identity provider

Please, create it according to the guide: https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-aws

It needs to be done once per AWS account.

S3 backend

It’s not going to work with a local state file. Let’s prepare all required items. I’m going to use Terraform code. Please mind it is not a part of the project you’re going to deploy.

Bucket

Just create a bucket with permissions.

resource "aws_s3_bucket" "state-bucket" {
  bucket = local.bucket_name
}

resource "aws_s3_bucket_versioning" "vers" {
  bucket = aws_s3_bucket.state-bucket.id
  versioning_configuration {
    status = "Enabled"
  }
}

I have a local variable describing the bucket name.

IAM

I’m going to use the same permissions for backend and for AWS provider. You probably should separate these roles for non-lab environments.

Local user

Despite - we’re going to use OIDC - I still want and user to run it locally.

data "aws_iam_policy_document" "project_policy" {
  statement {
    actions = [
      "s3:ListBucket"
    ]
    resources = [aws_s3_bucket.state-bucket.arn]
  }

  statement {
    actions = [
      "s3:GetObject",
      "s3:PutObject",
      "s3:DeleteObject"
    ]
    resources = [
      "${aws_s3_bucket.state-bucket.arn}/*/terraform.tfstate",
      "${aws_s3_bucket.state-bucket.arn}/*/terraform.tfstate.tflock"
    ]
  }
}

resource "aws_iam_user" "project_user" {
  name = "${var.project_name}-user"
}

resource "aws_iam_user_policy" "project_user_policy" {
  user   = aws_iam_user.project_user.id
  policy = data.aws_iam_policy_document.project_policy.json
}

Now - add the permissions which allow to build you infra. They are a little bit too wide. Please adjust them to your needs.

resource "aws_iam_user_policy_attachment" "ec2-full" {
  user       = aws_iam_user.project_user.id
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
}

resource "aws_iam_user_policy_attachment" "vpc-full" {
  user       = aws_iam_user.project_user.id
  policy_arn = "arn:aws:iam::aws:policy/AmazonVPCFullAccess"
}

resource "aws_iam_user_policy_attachment" "ssm-ro" {
  user       = aws_iam_user.project_user.id
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess"
}

resource "aws_iam_user_policy_attachment" "iam-ro" {
  user       = aws_iam_user.project_user.id
  policy_arn = "arn:aws:iam::aws:policy/IAMReadOnlyAccess"
}

You will need an access key for the user to use it locally

Role

OIDC needs an IAM role.

data "aws_iam_policy_document" "sts_policy" {
  statement {
    effect = "Allow"
    principals {
      type        = "Federated"
      identifiers = ["arn:aws:iam::<AWS account ID>:oidc-provider/token.actions.githubusercontent.com"]
    }
    actions = ["sts:AssumeRoleWithWebIdentity"]
    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values = [
        "repo:<GitHub account>/<Repo>:ref:refs/heads/main"
      ]
    }
  }
}


resource "aws_iam_role" "project_role" {
  assume_role_policy = data.aws_iam_policy_document.sts_policy.json
  name               = "${var.project_name}-role"
}

resource "aws_iam_role_policy" "project_role_attach" {
  role   = aws_iam_role.project_role.id
  policy = data.aws_iam_policy_document.project_policy.json
}

You need to adjust the following parameters:

  • AWS account ID,

  • GitHub user or organization,

  • The repo name.

The last resource allows the role to access the bucket. You still need the resources access. You can copy the code from user changing aws_iam_user_policy_attachment to aws_iam_role_policy_attachment.

That’s it. You can deploy the resources manually.

Setup the S3 backend for you project

Now create or update the backend setup for your project.

terraform {
  backend "s3" {
    bucket       = "<bucket-name>"
    key          = "<stack>/terraform.tfstate"
    use_lockfile = true
    region       = "eu-central-1"
  }
}

Replace bucket name, and “folder” in the bucket. I have multiple stacks in my project - each with a separate state. If your project is live already, use terraform init -migrate-state. If i’s a fresh one - just terraform init'

Workflow setup

Crate a YAML file in .github/workflows/

---
name: Net
on:
  push:
    branches: ["main"]
    paths:
      - net/**

permissions:
  id-token: write
  contents: read

jobs:
  make_key:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: net/
    steps:
      - uses: actions/checkout@v6
      - uses: aws-actions/configure-aws-credentials@v5
        with:
          role-to-assume: arn:aws:iam::<AWS account id>:role/tf-infra-role
          aws-region: eu-central-1
      - uses: hashicorp/setup-terraform@v3

      - run: terraform init -input=false
      - run: terraform plan
      - run: terraform apply -auto-approve

A quick explanation:

  • the workflow runs when you push to the ‘main’ and something in net/ subdir is changed. As I said - I have multiple stacks with multiple workflows.

  • “permissions” - required to handle OIDC.

  • “working-directory” - skip it if you run your project in the root.

  • “uses: aws-actions/configure-aws-credentials” - setups AWS connectivity. Use your actual account id.

  • Then terraform stuff goes. I know. I should have an approval after plan…

If everything went right - after next push you infra should built. You always can run terraform destroy locally.

References

https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp.html

https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-aws