19

Automated Terraform Deployments with GitHub

Managing infrastructure through Terraform is powerful — but doing it securely and automatically is where real efficiency begins. In this…

Managing infrastructure through Terraform is powerful — but doing it securely and automatically is where real efficiency begins.

In this guide, we’ll set up GitHub Actions workflows that plan and apply Terraform configurations to Azure using OpenID Connect (OIDC) — completely without storing secrets.

We’ll also highlight a critical but often underestimated piece: proper Terraform state storage in Azure Blob Storage.

Feel free: https://github.com/simon-vedder/deploy-terraform-template

🧭 Architecture Overview

Our setup consists of the following components:

  • GitHub Actions Workflows:
    • terraform-plan.yml – runs on pull requests, performs validation, security checks, and generates a plan.
    • terraform-apply.yml – runs on merge/push, applies the configuration to Azure environments (Dev & Prod).
  • Azure App Registration (Service Principal):
    • Authenticates via Federated Credentials (OIDC) from GitHub – no secrets required.
    • Assigned roles:
      • Contributor at the subscription level (to create resources)
      • Storage Blob Data Contributor on the storage account (to write Terraform state)
  • Terraform Remote State:
    • Stored in an Azure Storage Account to ensure consistent, shared state between workflows.

⚙️ Prerequisites

Before automation begins, prepare the following:

  1. Azure Storage Account
    • Create a container (e.g., tfstatefiles) to hold your Terraform state files.
  2. Azure App Registration
    • Create an App Registration in Microsoft Entra ID.
    • Add a Federated Credential tied to your GitHub repository and branch (e.g., main).
    • Note the following values:
      • Client ID
      • Tenant ID
      • Subscription ID
  3. Assign Roles
    • Contributor on the target Subscription
    • Storage Blob Data Contributor on the Storage Account
  4. GitHub Secrets Store only the IDs (no client secret!):
    • ARM_CLIENT_ID
    • ARM_TENANT_ID
    • ARM_SUBSCRIPTION_ID

🧩 Terraform Configuration

Terraform itself is configured to use OIDC and store its state securely in Azure Blob Storage.

backend.tf – configure the location of your statefiles:

terraform {
  backend "azurerm" {
    use_oidc             = true 
    resource_group_name  = "rg" #resource group which is associated with the storage account
    storage_account_name = "thisisatestsaacc" #storage account name
    container_name       = "tfstatefiles" #container name
    key                  = "dev/terraform.tfstate" #path and file name 
  }
}

💡 Why this matters:

Keeping your Terraform state in a shared, remote backend ensures team consistency and prevents state corruption.

Storing it in Azure Blob Storage allows your pipelines to work concurrently and recover easily if a local runner fails.

main.tf (excerpt):

provider "azurerm" {
  features {}
  use_oidc = true
}

resource "azurerm_resource_group" "main" {
  name     = "demo-dev-rg"
  location = "West Europe"
}

resource "azurerm_virtual_network" "main" {
  name                = "vnet-demo-dev"
  address_space       = ["10.0.0.0/16"]
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
}

🚀 GitHub Actions Workflows

Your automation is split into two workflows: Plan and Apply.

Both use OIDC for authentication and share the same Terraform backend.

1. Terraform Plan Workflow

📄 File: .github/workflows/terraform-plan.yml (Full code: Link)

Triggered by pull requests to the main branch.

Key highlights:

  • Runs terraform init, validate, and plan for each environment.
  • Performs security scans using Checkov and tfsec.
  • Comments plan results directly in the pull request.
permissions:
  id-token: write   # Required for OIDC
  contents: read
  pull-requests: write
- name: Azure Login via OIDC
  uses: azure/login@v1
  with:
    client-id: ${{ secrets.ARM_CLIENT_ID }}
    tenant-id: ${{ secrets.ARM_TENANT_ID }}
    subscription-id: ${{ secrets.ARM_SUBSCRIPTION_ID }}

2. Terraform Apply Workflow

📄 File: .github/workflows/terraform-apply.yml (Full code: Link)

Triggered on push to the main branch or manually via workflow_dispatch.

Key features:

  • Applies Terraform to Dev first, then Prod.
  • Includes a manual approval step before deploying to production.
  • Uses the same OIDC login, no secrets required.
manual-approval:
  name: Manual Approval
  uses: trstringer/manual-approval@v1
  with:
    approvers: simon-vedder #user account of approvers
    minimum-approvals: 1
    issue-title: "Manual Approval Required for Terraform Apply"

What happens when an error occurs:

How it should look like:

🔐 Secure Authentication with OIDC

This setup uses Federated Credentials (OIDC) between GitHub and Azure — which means:

  • No static secrets (no ARM_CLIENT_SECRET)
  • Short-lived tokens automatically managed by Azure
  • Fine-grained access scoped to your repository and branch

💡 Pro tip:

Federated credentials can be configured per branch or environment, letting you enforce least privilege and safer multi-environment pipelines.

GitHub Environsment Configuration:

Azure App Registration Federated Credentials Trust with GitHub:

🛡️ Security Checks for Terraform Code

Before applying any infrastructure changes, each workflow runs automated security scans to detect misconfigurations and policy violations early.

Tools used:

  • Checkov – focuses on Terraform best practices and compliance checks
  • tfsec (optional alternative) – static analysis for Terraform code security

Both scan your .tf files directly in the pipeline and flag potential risks such as:

  • Publicly exposed network ports
  • Unencrypted storage or disks
  • Missing tags or resource policies
  • Use of deprecated Terraform resources or Azure SKUs

Implementation:

    - name: Terraform Validate
      run: terraform validate
      working-directory: terraform/environments/${{ matrix.environment }}

    - name: Terraform Format Check
      run: terraform fmt -check -recursive
      working-directory: terraform/environments/${{ matrix.environment }}

    - name: Security Scan - Checkov
      run: checkov -d . --framework terraform --output cli --skip-check CKV_AZURE_50
      continue-on-error: true
      working-directory: terraform/environments/${{ matrix.environment }}

    - name: Security Scan - TFSec
      run: tfsec .
      continue-on-error: true
      working-directory: terraform/environments/${{ matrix.environment }}

💡 Tip:

You can and should set continue-on-error: false for production to fail the pipeline if a critical issue is found.

This approach enforces security gates before deployments and keeps your IaC compliant without manual reviews.

🧱 Terraform State Management (the underrated part)

Many CI/CD guides forget to mention where Terraform’s state files live — yet it’s crucial.

To achieve this you just have to prepare the backend.tf like in the example above.

With a remote backend in Azure Blob Storage:

  • You get consistent state across all runs.
  • GitHub Actions can safely read/write the same state concurrently.
  • The App Registration’s Storage Blob Data Contributor role ensures secure, limited access.
  • If one pipeline fails, state recovery is trivial — no local corruption, no drift.

🔒 Best practice:

Never store terraform.tfstate locally or in your repository.

Always rely on a secure, shared backend like Azure Blob Storage.

✅ Results

After a successful terraform apply

  • Azure Portal shows your created infrastructure:
    • Resource Group
    • Virtual Network
    • Network Security Group
    • Windows VM

🧩 Benefits of This Setup

AspectBenefit
SecurityNo secrets stored in GitHub; OIDC provides temporary credentials
ConsistencyCentralized remote state in Azure Blob Storage
AutomationFull CI/CD flow from PR to production
SafetyManual approval before production
ComplianceBuilt-in Terraform validation and security scanning

🧠 Final Thoughts

By combining GitHub Actions, Azure OIDC, and Terraform remote state, you achieve a workflow that is:

  • Secure by design
  • Fully automated
  • Transparent and auditable
  • Easy to extend for multi-environment or multi-subscription setups

This pattern should be your default baseline whenever you deploy Terraform to Azure from GitHub — simple, maintainable, and entirely secret-free.

Full Repository

https://github.com/simon-vedder/deploy-terraform-template

Simon

Cloud Engineer focused on Azure, Terraform & PowerShell. Passionate about automation, efficient solutions, and sharing real-world cloud projects and insights. ⸻ Let me know if you’d like to make it more casual, technical, or personalized.

Leave a Reply

Your email address will not be published. Required fields are marked *