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



