Engineering Automated Infrastructure: Orchestrating Azure Deployments with Terraform and GitHub Actions

Modern infrastructure management has evolved from manual portal clicks and static scripts to a philosophy of Infrastructure as Code (IaC). When leveraging Terraform for Azure resource management, the true realization of efficiency is found not in the code itself, but in the automation of its deployment. Integrating Terraform with GitHub Actions allows organizations to implement a robust Continuous Integration and Continuous Deployment (CI/CD) pipeline that ensures consistency, security, and scalability across various environments.

Central to a professional-grade automation strategy is the elimination of long-lived secrets. Historically, deploying Terraform from a CI tool required storing a Service Principal's client secret within the CI tool's secret store. This approach introduced security risks associated with secret rotation and potential leakage. The modern standard is the implementation of OpenID Connect (OIDC), which facilitates a trust relationship between GitHub and Microsoft Entra ID (formerly Azure Active Directory), enabling short-lived, token-based authentication.

The Architecture of Automated Terraform Deployments

A production-ready Terraform pipeline is composed of several interdependent components. The goal is to create a loop where code changes in GitHub trigger a series of validations, leading to a predictable deployment in Azure.

The structural components of this architecture include:

  • GitHub Actions Workflows: These are the automation engines. Typically, these are split into a "Plan" workflow for validation and a "Apply" workflow for execution.
  • Azure App Registration (Service Principal): This serves as the identity for the automation. By using Federated Credentials, the Service Principal trusts the GitHub repository to request access tokens without needing a password.
  • Azure Blob Storage: Because Terraform maintains a state file to map real-world resources to the configuration, a centralized remote backend is required to prevent state corruption and allow team collaboration.

Infrastructure Component Overview

Component Primary Function Key Security/Operational Detail
GitHub Actions CI/CD Orchestration Uses OIDC for secretless authentication
Microsoft Entra ID Identity Management Manages Federated Credentials for GitHub
Azure Blob Storage Remote State Storage Requires Storage Blob Data Contributor role
Terraform Infrastructure as Code Uses .tf files to define Azure resources
Checkov/tfsec Static Analysis Scans for security misconfigurations before deployment

Implementing OpenID Connect (OIDC) for Secretless Auth

OpenID Connect (OIDC) is an identity authentication protocol that extends OAuth 2.0. In the context of GitHub Actions and Azure, OIDC allows the GitHub workflow to request a short-lived access token directly from Azure. This removes the need to create and duplicate credentials as secrets in GitHub, thereby eliminating the overhead of rotating client secrets.

When a federated credential is created, a trust relationship is established between Microsoft Entra ID and GitHub. This trust is defined by the "subject," which specifies which GitHub entity is allowed to request a token.

Federated Credential Subject Configurations

The subject allows for granular control over which workflows can access Azure. For example:

  • Environment-based: repo:${var.github_organization_target}/${var.github_repository}:environment:${var.environment}. This ensures that only workflows running in a specific GitHub environment (e.g., "dev") can authenticate.
  • Pull Request-based: repo:${var.github_organization_target}/${var.github_repository}:pull_request. This allows the "Plan" workflow to authenticate to Azure to check the current state during a PR, without granting permission to change the infrastructure.
  • Branch-based: repo:my-github-user/my-repo:ref:refs/heads/main. This restricts authentication to only the main branch, typically reserved for the "Apply" or production workflow.

Managing Terraform Remote State in Azure

Terraform must store state about the managed infrastructure to map real-world resources to the configuration. This state file is used to track metadata and determine which changes are necessary during the next execution. By default, Terraform stores this in a local terraform.tfstate file. However, committing this file to source control is strictly forbidden due to security risks (it may contain sensitive data) and the risk of state collision in team environments.

The solution is to use an Azure Storage Account as a remote backend. This ensures that the state is shared across all workflow runs and stored securely.

Remote Backend Configuration

To implement this, an Azure Storage Account and a specific container (e.g., tfstatefiles) must be created. The GitHub Actions workflow then initializes Terraform by passing these backend configurations dynamically.

The initialization command often looks like this:

bash terraform init -backend-config="resource_group_name=${{secrets.BACKEND_AZURE_RESOURCE_GROUP_NAME}}" -backend-config="storage_account_name=${{secrets.BACKEND_AZURE_STORAGE_ACCOUNT_NAME}}" -backend-config="container_name=${{secrets.BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME}}"

This method keeps the backend configuration flexible, allowing different secrets to be injected based on the environment being deployed.

Designing the CI/CD Workflow Pipeline

A professional pipeline is split into two distinct phases: the Plan phase (CI) and the Apply phase (CD).

The Terraform Plan Workflow

The terraform-plan.yml workflow is triggered by pull requests to the main branch. Its primary goal is validation and visibility. It ensures that the code is syntactically correct, secure, and that the resulting changes are documented for the reviewer.

Key steps in the Plan workflow include:

  1. Azure Login via OIDC: The workflow requests a token using the client ID, tenant ID, and subscription ID.
  2. Terraform Init: Initializes the backend and downloads required providers.
  3. Terraform Validate: Ensures the configuration is internally consistent.
  4. Security Scanning: Tools like Checkov and tfsec perform static code analysis to spot misconfigurations (e.g., open SSH ports or unencrypted disks) before they reach Azure.
  5. Terraform Plan: Generates a plan showing exactly what resources will be added, changed, or destroyed. This output is often commented directly back into the pull request for ease of review.

Plan Workflow Permissions

For OIDC to function, the GitHub Action requires specific permissions in the YAML file:

yaml permissions: id-token: write # Required for requesting the OIDC token contents: read # Required to checkout the code pull-requests: write # Required to comment the plan back to the PR

The Terraform Apply Workflow

The terraform-apply.yml workflow is triggered only after a merge or a push to the main branch. While the Plan workflow is about "checking," the Apply workflow is about "executing."

This workflow typically runs the terraform apply command, utilizing the same OIDC authentication and remote state backend. Because this workflow modifies live infrastructure, it is often gated by environment protections in GitHub (such as mandatory manual approvals).

Technical Implementation: Azure App Registration and Roles

For the automation to succeed, the Azure App Registration (Service Principal) must be granted sufficient permissions. Permissions should follow the principle of least privilege, but for general infrastructure management, two primary roles are required:

  • Contributor: Assigned at the subscription level. This allows Terraform to create, delete, and modify resources across the subscription.
  • Storage Blob Data Contributor: Assigned specifically to the Storage Account holding the state file. This is necessary because the "Contributor" role does not grant direct data-plane access to blobs, which Terraform requires to read and write the .tfstate file.

Role Assignment via Terraform

In a sophisticated setup, the role assignments can be handled by Terraform itself using data blocks to reference the subscription ID dynamically. For instance, using the azurerm_subscription data source allows the code to remain portable across different subscriptions.

```hcl
data "azurerm_subscription" "sub" {}

Use the ID from the data source in a role assignment

resource "azurermroleassignment" "example" {
scope = data.azurermsubscription.sub.id
role
definitionname = "Contributor"
principal
id = var.serviceprincipalid
}
```

Structuring the Terraform Project

Organization of the directory structure is critical for maintainability. A common pattern is to separate the "bootstrap" code from the "application" code.

Recommended Folder Structure

Folder/File Purpose
/bootstrap Terraform code to set up the initial OIDC, App Registration, and Storage Account.
/cluster-deployment The primary logic for deploying resources (e.g., AKS clusters).
.github/workflows/ YAML definitions for terraform-plan.yml and terraform-apply.yml.
.gitignore Ensures terraform.tfstate and local .terraform folders are not committed.

By separating the bootstrap process, you avoid a "chicken and egg" problem where you need a storage account to store the state of the storage account creation. The bootstrap process is typically run once manually or via a separate minimal pipeline.

Advanced Pipeline Integration and Static Analysis

Integrating static analysis tools like Checkov and tfsec into the pipeline transforms the workflow from simple deployment to "Policy as Code." Instead of relying on a human reviewer to notice a security flaw, the pipeline automatically fails if a security policy is violated.

For example, if a developer attempts to deploy an Azure Kubernetes Service (AKS) cluster with a public load balancer that is open to the entire internet, Checkov can detect this during the terraform-plan phase and block the merge. This shifts security "left" in the development lifecycle.

Conclusion

Automating Azure deployments with Terraform and GitHub Actions represents a significant leap in operational maturity. By transitioning from secret-based authentication to OpenID Connect (OIDC), organizations eliminate the risk of credential leakage and reduce the operational burden of secret management. The implementation of a dual-workflow system—splitting the process into a validation-heavy "Plan" phase and an execution-focused "Apply" phase—ensures that infrastructure changes are scrutinized before they are realized.

Furthermore, the use of Azure Blob Storage for remote state management is non-negotiable for any team environment, providing the necessary locking and consistency mechanisms to prevent state corruption. When combined with static analysis tools and a clean directory structure, this ecosystem creates a scalable, secure, and highly predictable path for delivering cloud infrastructure. The synergy between Terraform's declarative nature and GitHub Actions' orchestration capabilities allows engineers to treat their data center as a software project, complete with version control, automated testing, and peer-reviewed deployments.

Sources

  1. Automated Terraform Deployments with GitHub
  2. Using GitHub Actions Workload identity federation (OIDC) with Azure for Terraform Deployments
  3. Deploying to Azure with Terraform and GitHub Actions

Related Posts