Terraform Remote Execution
Remote execution runs terraform plan and terraform apply on a managed infrastructure (Terraform Cloud, HCP Terraform, or CI/CD runners) rather than on a developer’s local machine. The benefits: consistent environment, centralized audit logs, no dependency on who has provider credentials on their laptop, and native integration with policy-as-code.
Why Remote Execution
| Local Execution | Remote Execution |
|---|---|
| Runs in developer’s shell | Runs in a consistent, clean environment |
| Developer needs provider credentials | Centralized credential management |
| No visibility when someone runs apply | Centralized run history and logs |
| Difficult to enforce policies | Policy-as-code enforced before apply |
| ”Works on my machine” variability | Same Terraform version, same env for all |
Terraform Cloud Remote Execution
HCP Terraform (formerly Terraform Cloud) provides remote execution as a managed service:
terraform { cloud { organization = "mycompany"
workspaces { name = "production-aws" } }}# Authenticate with Terraform Cloudterraform login
# Initialize — downloads modules, sets up remote executionterraform init
# This plan runs remotely in Terraform Cloudterraform plan
# This apply runs remotely with approval workflowterraform applyWhen you run terraform plan locally, the plan actually executes in Terraform Cloud’s infrastructure. Streaming output appears in your terminal, but the execution happens remotely.
Execution Modes
Terraform Cloud supports two execution modes:
Remote (Default)
Plans and applies run in Terraform Cloud’s managed infrastructure:
# Workspace settings in Terraform Cloud UI:# Settings > General > Execution Mode: RemoteLocal
State is stored remotely in Terraform Cloud, but plan/apply runs locally:
# Useful when you need local provider access or custom environment# Settings > General > Execution Mode: LocalAgent
Plans and applies run on your own infrastructure via Terraform Cloud Agents — useful for private network resources:
# Install and register a Terraform Cloud Agentdocker run -d \ -e TFC_AGENT_TOKEN="your-agent-token" \ -e TFC_AGENT_NAME="private-network-agent" \ hashicorp/tfc-agent:latestAPI-Driven Workflow
Trigger Terraform runs from CI/CD without the Terraform CLI:
# Create and start a run via Terraform Cloud APIWORKSPACE_ID="ws-abc123"TFC_TOKEN="your-token"
# Queue a plancurl \ --header "Authorization: Bearer $TFC_TOKEN" \ --header "Content-Type: application/vnd.api+json" \ --request POST \ --data '{ "data": { "attributes": { "message": "Queued from GitHub Actions", "is-destroy": false }, "type": "runs", "relationships": { "workspace": { "data": { "type": "workspaces", "id": "'$WORKSPACE_ID'" } } } } }' \ https://app.terraform.io/api/v2/runsCLI-Driven Workflow in CI/CD
The most common pattern — run the Terraform CLI in a CI/CD job that streams output back:
name: Terraform
on: push: branches: [main] pull_request:
jobs: plan: name: Terraform Plan runs-on: ubuntu-latest permissions: id-token: write contents: read pull-requests: write
steps: - uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3 with: terraform_version: "1.9.5" cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}
- name: Terraform Init run: terraform init -input=false
- name: Terraform Plan id: plan run: | terraform plan -no-color -input=false -out=tfplan 2>&1 | tee plan-output.txt
- name: Comment Plan on PR if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const fs = require('fs') const plan = fs.readFileSync('plan-output.txt', 'utf8') github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: `## Terraform Plan\n\`\`\`terraform\n${plan.slice(-60000)}\n\`\`\`` })
apply: name: Terraform Apply runs-on: ubuntu-latest needs: plan if: github.ref == 'refs/heads/main' environment: production # Requires manual approval in GitHub Environments
steps: - uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3 with: terraform_version: "1.9.5"
- uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} aws-region: us-east-1
- run: terraform init -input=false - run: terraform apply -auto-approve -input=falseAtlantis: Pull Request Automation
Atlantis runs Terraform inside your own infrastructure and comments plan/apply output on GitHub/GitLab PRs:
# atlantis.yaml — project configurationversion: 3projects: - name: production dir: environments/production workspace: production autoplan: when_modified: - "*.tf" - "modules/**/*.tf" apply_requirements: - approved # Require PR approval before apply - mergeable # Require branch to be mergeable
- name: staging dir: environments/staging workspace: staging autoplan: when_modified: ["*.tf"]Atlantis workflow in pull requests:
- Push changes to a branch
- Atlantis comments
terraform planoutput on the PR - Reviewer approves the PR and comments
atlantis apply - Atlantis runs apply and reports results
- Merge the PR
Securing Remote Execution
# Use OIDC to avoid static credential management# CI runner proves identity to AWS without stored keys
resource "aws_iam_role" "ci_runner" { name = "TerraformCIRole"
assume_role_policy = jsonencode({ Statement = [{ Effect = "Allow" Principal = { Federated = "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com" } Action = "sts:AssumeRoleWithWebIdentity" Condition = { StringLike = { "token.actions.githubusercontent.com:sub" = "repo:myorg/*:*" } } }] })}