avatar Post
🇬🇧 🇪🇸

Automating AWS Resource Deployment with GitHub Actions and Terraform

Automating AWS Resource Deployment with GitHub Actions and Terraform

Updated September 2025. This article is an updated version of my previous guide, now including the recommended OIDC approach to replace long-lived AWS access keys.

1. Introduction

In previous articles, we have discussed Terraform and explored various DevOps strategies for automating deployments on AWS.

Now, we’ll automate the deployment of Terraform projects using GitHub Actions. Regardless of what you need to deploy, this approach enables efficient, automated Terraform-based infrastructure deployments.

Before diving into details, I want to mention that I’ll explain two approaches to set up authentication with AWS: using OIDC or using secrets.

And I’ve created a repository for each approach:

Feel free to customize the examples. You can use them to deploy any AWS resource.

1.1. What is GitHub Actions?

GitHub Actions is an automation tool within GitHub that allows you to create workflows triggered by GitHub events, such as pushes or pull requests. It’s commonly used for CI/CD pipelines to automate tasks like testing, building, and deploying applications.

1.2. How GitHub Actions works with Terraform

When using GitHub Actions to deploy Terraform resources, the typical workflow involves:

  • Triggering workflows based on changes in the repository.
  • Setting up AWS credentials using GitHub Secrets (or, preferably, using OIDC for better security).
  • Running Terraform commands (e.g., terraform plan, terraform apply) to manage infrastructure.

1.3. Prerequisites

Before getting started, make sure you have:

Authentication with AWS will be configured later.

There are two approaches to configure authentication with AWS:

    1. OIDC (recommended) – secure and modern. You will use an IAM Role.
    1. Legacy – static access keys in GitHub Secrets. You will use AWS Access Keys on IAM.

2. Step 1: Prepare the Terraform Code

Create your Terraform code. In this example, we will deploy a simple AWS Budget resource, but you can deploy anything using Terraform.

First, create a main.tf file with the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
provider "aws" {
  region = "eu-west-1"
}

# Customize with your own S3 bucket and DynamoDB table if you want to use a Remote Backend for State
terraform {
  backend "s3" {
    bucket         = "terraform-tfstate-playingaws-poc"     # Update it 
    key            = "poc/terraform-github-actions.tfstate" # Update it
    region         = "eu-west-1"                            # Update it
    dynamodb_table = "terraform-lock"                       # Update it
    encrypt        = true
  }
}

resource "aws_budgets_budget" "zero_spend_budget" {
  name         = "ZeroSpendBudget"
  budget_type  = "COST"
  limit_amount = "0.1"
  limit_unit   = "USD"
  time_unit    = "MONTHLY"
  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 0
    threshold_type             = "ABSOLUTE_VALUE"
    notification_type          = "ACTUAL"
    subscriber_email_addresses = ["your_email@domain.com"]
  }
}

If you don’t include the backend configuration, by default, Terraform state is stored locally in the GitHub runner. This means that in subsequent executions, you could encounter state inconsistencies, so it’s recommended to use a remote backend.

If you include the backend configuration but you have not created the S3 bucket and the DynamoDB table in the first place, you will receive an error in the pipeline execution.

3. Step 2: Configuring AWS Credentials in GitHub

To allow GitHub Actions to deploy resources in your AWS account, you must provide AWS credentials to the runner. There are two ways to do this:

  • OIDC (recommended): short-lived credentials issued at runtime via OpenID Connect, with a restrictive trust policy bound to your repo/branch (no static secrets).
  • Legacy (less secure): long-lived Access Keys stored as GitHub Secrets.

Below you will find both approaches. We will start with OIDC and then show the legacy method for completeness and migration scenarios.

While using AWS Access Keys stored in GitHub Secrets works, it’s no longer the best practice. A more secure approach is to use GitHub OpenID Connect (OIDC) to obtain temporary credentials directly from AWS, without long-lived secrets.

Why OIDC is better

  • No static secrets stored in GitHub → reduces the risk of leaks.
  • Ephemeral credentials → valid only for a short time.
  • Granular trust policy → restrict access to specific repos/branches.

How it works

  1. Configure GitHub as an OIDC provider in your AWS account.
  2. Create an IAM Role with a trust policy restricted to your GitHub repository and branch.
  3. Attach the least-privilege policy needed for Terraform (S3, DynamoDB, Budgets in this example).
  4. Update your workflow to use OIDC.

Trust policy example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Federated": "arn:aws:iam::<YOUR_ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com" },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:<YOUR_ORG_OR_USER>/<YOUR_REPO>:ref:refs/heads/main"
        }
      }
    }
  ]
}

Workflow change (key section):

1
2
3
4
5
6
7
8
9
permissions:
  id-token: write
  contents: read

- name: Configure AWS credentials (OIDC)
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::<YOUR_ACCOUNT_ID>:role/<ROLE_FOR_GITHUB>
    aws-region: ${{ env.AWS_REGION }} 

By using OIDC, GitHub Actions retrieves short-lived credentials on each run, making the setup more secure and more aligned with AWS best practices.

OIDC is now the industry standard for connecting GitHub Actions with AWS. By adopting it, you reduce the risk of compromised credentials and align with AWS security best practices.

3.2 Using GitHub Secrets (Legacy)

If you cannot use OIDC yet (for example, due to organizational restrictions), you can fall back to storing AWS Access Keys in GitHub Secrets.

Add the following secrets in your GitHub repository:

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY

How?

  • Go to your repository’s settings.
    • 1-access-settings
  • Navigate to Secrets and Variables > Actions.
    • 2-access-secrets
    • 3-create-new-secret
  • Add the following secrets:
    • AWS_ACCESS_KEY_ID: Your AWS Access Key ID.
    • AWS_SECRET_ACCESS_KEY: Your AWS Secret Access Key.
    • 4-secrets-created

This ensures that your AWS credentials are securely available to the GitHub Actions workflow.

Then configure the workflow like this:

1
2
3
4
5
6
7
- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
   
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: ${{ env.AWS_REGION }}

While your repository might be public, your AWS credentials remain secure by being stored in the encrypted Secrets section.

GitHub also has built-in Secrets Detection to prevent accidental exposure.

4. Step 3: Configure GitHub Actions for Terraform Deployment

Now that we have the Terraform code, let’s set up GitHub Actions to automate its deployment.

Create the workflow file in your GitHub repository at .github/workflows/terraform-deploy.yml.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
name: Terraform Deploy (OIDC)

on:
  push:
    branches:
      - main
    paths:
      - 'main.tf'
      - '.github/workflows/terraform-deploy.yml'
  pull_request:
    branches:
      - main
    paths:
      - 'main.tf'
      - '.github/workflows/terraform-deploy.yml'
  workflow_dispatch:

permissions:
  id-token: write   # required for OIDC
  contents: read    # required for checkout

env:
  AWS_REGION: ${{ env.AWS_REGION }} 
  TF_IN_AUTOMATION: true

concurrency:
  group: terraform-${{ github.ref }}
  cancel-in-progress: false

jobs:
  plan:
    name: Terraform Plan
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::<YOUR_ACCOUNT_ID>:role/<ROLE_FOR_GITHUB>
          aws-region: ${{ env.AWS_REGION }}

      - name: Terraform Init
        run: terraform init -input=false

      - name: Terraform Format Check
        run: terraform fmt -check

      - name: Terraform Validate
        run: terraform validate

      - name: Terraform Plan
        run: terraform plan -input=false -out=tfplan

      - name: Upload plan artifact
        uses: actions/upload-artifact@v4
        with:
          name: tfplan
          path: tfplan

  apply:
    name: Terraform Apply (requires approval)
    needs: plan
    runs-on: ubuntu-latest
    environment:
      name: production   # configure Environment protections (required reviewers) in GitHub -> Settings -> Environments
    if: github.event_name != 'pull_request'  # don't apply on PRs

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::<YOUR_ACCOUNT_ID>:role/<ROLE_FOR_GITHUB>
          aws-region: ${{ env.AWS_REGION }}

      - name: Terraform Init
        run: terraform init -input=false

      - name: Download plan artifact
        uses: actions/download-artifact@v4
        with:
          name: tfplan
          path: .

      - name: Terraform Apply
        run: terraform apply -input=false tfplan

In the apply job we specify environment: production.

This allows you to configure Environment Protection Rules in GitHub (Settings → Environments → production), so that terraform apply requires manual approval before execution.

If you don’t need this behavior, you can safely remove the environment: line.

Or you can also define multiple environments (e.g., staging, production) and require different reviewers per environment.

4.2 Using GitHub Secrets (Legacy)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
name: Deploy Terraform

on:
  push:
    branches:
      - main

jobs:
  terraform:
    name: Deploy Terraform Job
    runs-on: ubuntu-latest

    steps:
      # Pulls the latest version of your code from the GitHub repository
      - name: Checkout code
        uses: actions/checkout@v4

      # Installs Terraform on the GitHub runner
      - name: Set up Terraform
        uses: hashicorp/setup-terraform@v3

      # Configures your AWS access by using the credentials stored in GitHub Secrets
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
         
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      # Initialize terraform
      - name: Initialize Terraform
        run: terraform init

      # Execute the terraform plan
      - name: Terraform Plan
        run: terraform plan

      # Deploy terraform changes
      - name: Terraform Apply
        run: terraform apply -auto-approve

      # Destroy terraform resources (uncomment lines below)
      # - name: Terraform Destroy
      #   run: terraform destroy -auto-approve

5. Step 4: Testing the Setup

Push your Terraform code and GitHub Actions configuration to the main branch.

5-commit-code

GitHub Actions will trigger automatically, and you can monitor the deployment under the Actions tab of your repository.

6-automatic-deployment

If the deployment succeeds, your AWS Budget (or whatever Terraform resource you define) will be applied automatically.

7-deployment-successful

8-aws-resource-created

Now, each time you push changes to the repository, the pipeline will be triggered, and if you haven’t made any changes, the pipeline will finish with this message:

9-new-execution-without-changes

6. Conclusion

By using GitHub Actions to deploy Terraform resources, you can fully automate your infrastructure management processes. The combination of Terraform and GitHub Actions creates a powerful CI/CD pipeline, allowing you to deploy infrastructure changes automatically.

Although we used an AWS Budget resource in this example, you can apply the same process to any other AWS resource supported by Terraform.

While this article shows both methods, the recommended approach is using OIDC to avoid static credentials and improve security.

7. Next steps

  • Manual Approval for terraform apply: Add a manual approval step to review the terraform plan output
  • Include Security Tools: Integrate tools like tflint and checkov to lint and check for security issues
  • Automate Testing: Use unit testing frameworks like terratest to validate your Terraform code before deployment
  • If you are still using AWS Access Keys stored in GitHub Secrets, migrate to the OIDC approach as soon as possible to reduce risk and align with AWS security best practices.
This post is licensed under CC BY 4.0 by the author.

Subscribe to my newsletter!

Receive my latest articles, tutorials, and tips on AWS and cloud computing by subscribing to my newsletter. No spam, I promise!