Securely Deploy to AWS

With OIDC and IAM Roles and Github Action

token.actions.githubusercontent.com

sts.amazonaws.com

Create a bucket

Create an S3 bucket to store our Terraform states.

Create your AWS IAM Role and policies

For permission set create poliicy

1. Create Policy for access bucket for state file

Copy{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::YOUR_BUCKET/*",
                "arn:aws:s3:::YOUR_BUCKET"
            ]
        }
    ]
}
  1. Create on more policy with requied permission to iam role to create infra on aws

Add this both policy to role

Step 2: Select trusted entity

Copy{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::YOUR_ACCOUNT_NUMBER:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:YOUR_GITHUB_USERNAME/YOUR_REPO_NAME:*"
                }
            }
        }
    ]
}

You will need to modify:

  1. YOUR_ACCOUNT_NUMBER (AWS)

  2. YOUR_GITHUB_USERNAME

  3. YOUR_REPO_NAME

    Go to ssh to get “YOUR_GITHUB_USERNAME/YOUR_REPO_NAME”

Terraform code

create your terraform code and also provider block

Provider Block

terraform {
  required_version = ">= 1.5.3, < 1.10.0"  # Supports Terraform v1.5.3 and higher, but under 1.10.0
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      #version = ">= 5.73.0"  # Use AWS provider version 5.73.0 or newer
      version = ">= 5.0"
    }
  }


  # Adding Backend as S3 for Remote State Storage
  backend "s3" {
    bucket = "BUCKETNAME"
    key    = "terraform/BUCKET-FOLDER/terraform.tfstate"
    region = "ap-south-1"

    # For State Locking
    dynamodb_table = "TABLE-NAME"
  }    
}

# Provider Block
provider "aws" {
  region  = var.aws_region
  #profile = "default"
}

GitHub Secrets

Before creating the GitHub Actions, let’s add some Secrets inside our repository.

Left side » Secrets » Actions

Click on: New repository secret

secreat for aws role

  • AWS_ROLE : ARN OF YOUR ROLE

GitHub Action

Let’s create our GitHub Action, for this, we need to create a file in our GitHub repository :

.github/workflows/deploy.yml

Copyname: "Terraform Action" # Name of the workflow 

# Trigger the workflow 
on:
  push:
    paths:
      - 'FOLDER/**'  # Trigger only if files in FOLDER folder are modified
    branches:
        [ "BRANCH-NAME" ]
    #   "clem-devops/v1.1"   # Trigger only on pushes to the devops v1.1 branch
  pull_request:
    paths:
      - 'FOLDER/**'  # Trigger only if PR modifies files in FOLDER folder

# Set permissions required for the workflow
permissions:
  id-token: write  # Required for AWS OIDC connection
  contents: read   # Required to checkout the repository
  pull-requests: write # Required to comment on PRs

# Environment variables shared across the workflow
env:
  TF_LOG: INFO         # Enable Terraform logging
  AWS_REGION: ap-south-1 # Specify AWS region

jobs:
  deploy:
    runs-on: ubuntu-latest # Run the job on the latest Ubuntu environment
    defaults:
      run:
        shell: bash
        working-directory: ./FOLDER  # Change working directory for FOLDER module

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3  # Check out the code from the repository

      - name: Configure AWS credentials using OIDC
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_DEV }}  # Role to assume for AWS access
          aws-region: ${{ env.AWS_REGION }}            # AWS region
          role-session-name: GitHub-OIDC-TERRAFORM     # Session name for identification

      - name: Clear Terraform Cache
        run: |

          rm -rf ~/.terraform.d/plugin-cache  # Clear Terraform plugin cache
          rm -rf .terraform
          rm -f .terraform.lock.hcl

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.5.3  # Specify Terraform version

      - name: Terraform Format Check
        id: fmt
        run: terraform fmt -check   # Check if Terraform files are formatted
        continue-on-error: true     # Continue even if formatting errors are found

      - name: Terraform Init
        id: init
        run: terraform init         # Initialize Terraform configuration

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color  # Validate Terraform files

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color      # Generate execution plan
        if: github.event_name == 'pull_request'
        continue-on-error: true

      - uses: actions/github-script@v6
        if: github.event_name == 'pull_request'
        env:
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
            #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
            #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
            <details><summary>Validation Output</summary>

            \`\`\`\n
            ${{ steps.validate.outputs.stdout }}
            \`\`\`

            </details>

            #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`

            <details><summary>Show Plan</summary>

            \`\`\`\n
            ${process.env.PLAN}
            \`\`\`

            </details>

            *Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })

      - name: Terraform Plan Status
        if: steps.plan.outcome == 'failure'
        run: exit 1   # Fail the workflow if the plan step fails

      - name: Terraform Apply
        if: github.ref == 'refs/heads/BRANCH-NAME' && github.event_name == 'push'
        run: terraform apply -auto-approve -input=false  # Apply changes if on BRANCH-NAME branch