Cloud  /  Terraform

IaC Terraform 50 guides · updated 2026

Infrastructure as code done right — providers, state, reusable modules, and the workflow patterns that keep multi-cloud deployments sane in 2026.

Secrets Management in Terraform

Terraform configurations often need secrets — database passwords, API keys, TLS certificates. The challenge: secrets must be available at plan/apply time without being committed to version control, visible in CI logs, or stored in unencrypted state. This guide covers the most effective patterns used in production infrastructure.


The Core Problem

# NEVER DO THIS
resource "aws_db_instance" "main" {
password = "SuperSecret123!" # Committed to git, leaked in state, shown in plan
}
# ALSO WRONG
variable "db_password" {
default = "SuperSecret123!" # Hardcoded default = hardcoded secret
}

The problems: version control history, CI/CD logs, and unencrypted state files can all expose the value.


Secrets live in AWS Secrets Manager, not in Terraform. Terraform reads them at runtime using a data source:

# Read a secret — value is never written to .tf files or tfvars
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "production/rds/master-password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
# For JSON-encoded secrets (most common pattern)
data "aws_secretsmanager_secret_version" "app_secrets" {
secret_id = "production/app/credentials"
}
locals {
secrets = jsondecode(data.aws_secretsmanager_secret_version.app_secrets.secret_string)
}
resource "aws_ecs_task_definition" "app" {
container_definitions = jsonencode([{
name = "app"
secrets = [
{ name = "DATABASE_URL", valueFrom = "arn:aws:secretsmanager:..." },
{ name = "API_KEY", valueFrom = "arn:aws:secretsmanager:..." }
]
}])
}

Creating a secret with Terraform (rotation-friendly pattern):

resource "aws_secretsmanager_secret" "db_password" {
name = "production/rds/master-password"
recovery_window_in_days = 7
}
resource "random_password" "db" {
length = 32
special = true
override_special = "!#$%&*()-_=+[]{}<>:?"
}
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
secret_string = random_password.db.result
}
resource "aws_db_instance" "main" {
password = random_password.db.result
lifecycle {
ignore_changes = [password] # Let rotation handle updates
}
}

Pattern 2: HashiCorp Vault

For multi-cloud or existing Vault infrastructure:

provider "vault" {
address = "https://vault.mycompany.internal"
# Auth via environment: VAULT_TOKEN or VAULT_ROLE_ID + VAULT_SECRET_ID
}
# Read from KV v2
data "vault_kv_secret_v2" "db_credentials" {
mount = "secret"
name = "production/database"
}
resource "aws_db_instance" "main" {
username = data.vault_kv_secret_v2.db_credentials.data["username"]
password = data.vault_kv_secret_v2.db_credentials.data["password"]
}
# Dynamic database credentials (Vault generates short-lived creds)
data "vault_database_secret_backend_creds" "app" {
backend = "database"
role = "app-readonly"
}
# Vault AWS secrets engine — generates temporary AWS credentials
data "vault_aws_access_credentials" "deploy" {
backend = "aws"
role = "terraform-role"
type = "sts"
}

Pattern 3: SOPS + Encrypted Files

SOPS encrypts secrets files that can be safely committed to version control. The file is encrypted with KMS, PGP, or age keys:

Terminal window
# Encrypt a secrets file with AWS KMS
sops --kms "arn:aws:kms:us-east-1:123456789012:key/mrk-..." \
--encrypt secrets.yaml > secrets.enc.yaml
# Edit the encrypted file (SOPS decrypts in-place, re-encrypts on save)
sops secrets.enc.yaml
# Using the SOPS provider
terraform {
required_providers {
sops = {
source = "carlpett/sops"
version = "~> 1.0"
}
}
}
provider "sops" {}
data "sops_file" "production_secrets" {
source_file = "secrets.enc.yaml"
}
locals {
db_password = data.sops_file.production_secrets.data["database_password"]
api_key = data.sops_file.production_secrets.data["api_key"]
}
resource "aws_db_instance" "main" {
password = local.db_password
}

Pattern 4: OIDC — No Credentials at All

The best pattern for CI/CD: Terraform never receives a static secret. The CI runner proves its identity via OIDC and gets temporary credentials:

# GitHub Actions with OIDC (no AWS_ACCESS_KEY_ID needed)
jobs:
terraform:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
# OIDC — no static secrets needed
- run: terraform apply -auto-approve
# AWS IAM role that trusts GitHub Actions OIDC
resource "aws_iam_role" "github_actions" {
name = "GitHubActionsRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = "arn:aws:iam::${data.aws_caller_identity.current.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:myorg/myrepo:*"
}
}
}]
})
}

Protect State — Secrets Still Land There

Even with the best secret sourcing patterns, secret values often end up in state (e.g., random_password.db.result). Encrypt and restrict access:

terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
encrypt = true
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/mrk-..."
dynamodb_table = "terraform-lock"
}
}
# Restrict state bucket access to only Terraform IAM role
resource "aws_s3_bucket_policy" "state" {
bucket = aws_s3_bucket.terraform_state.id
policy = jsonencode({
Statement = [{
Effect = "Deny"
Principal = "*"
Action = ["s3:GetObject", "s3:PutObject"]
Resource = "${aws_s3_bucket.terraform_state.arn}/*"
Condition = {
ArnNotEquals = {
"aws:PrincipalArn" = [
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/TerraformRole"
]
}
}
}]
})
}

Quick Decision Guide

SituationBest Pattern
AWS infrastructure, secrets already in AWSAWS Secrets Manager data source
Multi-cloud, existing Vault deploymentHashiCorp Vault provider
Secrets need to be in git (encrypted)SOPS + terraform-sops provider
CI/CD runner identity (GitHub Actions, GitLab CI)OIDC — no static credentials
Local dev only.env file + TF_VAR_* environment variables
Quick prototype-var flag (never commit the command)