Terraform state management is one of the areas where government programs most commonly make mistakes that compound into operational problems. In a commercial cloud environment, state management mistakes cost time. In a FedRAMP-authorized or DoD IL-environment, they can create compliance gaps, unauthorized configuration drift, and audit findings.
Here's how Rutagon structures Terraform state for government cloud programs — including the specific GovCloud configuration and the patterns that prevent the most common production failures.
Why State Management Is More Critical in Government Programs
Terraform state tracks what resources exist in the target infrastructure and how they map to your configuration. In a government program, state has additional significance:
Audit trail requirements: FedRAMP ConMon and FISMA require documentation of infrastructure configuration changes. State stored in a versioned S3 bucket creates an immutable history of infrastructure changes — who applied what, when, and what changed. This history directly satisfies AU-9 (Protection of Audit Information) and CM-3 (Configuration Change Control).
Multi-person delivery teams: Government programs almost always involve multiple engineers with access to infrastructure. Without centralized remote state + locking, two engineers applying simultaneously can corrupt state or create resource conflicts.
Long-lived environments: Government programs have long contract periods — 5, 10, even 20 years. Infrastructure must be manageable across team rotations. State stored locally on individual developer machines is incompatible with team continuity.
S3 Backend Configuration for AWS GovCloud
The standard production pattern for Terraform state in AWS GovCloud uses S3 for state storage, DynamoDB for state locking, and a dedicated state management account in your AWS Organizations structure.
# backend.tf — GovCloud remote state configuration
terraform {
backend "s3" {
bucket = "rutagon-tfstate-govcloud-prod"
key = "services/api-gateway/terraform.tfstate"
region = "us-gov-west-1"
encrypt = true
kms_key_id = "arn:aws-us-gov:kms:us-gov-west-1:ACCOUNT_ID:key/KEY_ID"
dynamodb_table = "terraform-state-lock"
# GovCloud requires explicit endpoint in some toolchain configurations
# endpoint = "s3.us-gov-west-1.amazonaws.com"
}
} Key elements of this configuration:
encrypt = true + kms_key_id: State files contain resource IDs, ARNs, and potentially sensitive configuration values. In a FedRAMP environment, state must be encrypted at rest using a customer-managed KMS key (CMKM). Using a CMK satisfies SC-28 (Protection of Information at Rest) for the state store and provides the audit trail showing you control the key rotation.
DynamoDB locking: The dynamodb_table parameter enables state locking. Any terraform apply or terraform destroy acquires a lock before executing. Locks prevent concurrent applies — a critical safety mechanism when multiple engineers work in the same environment.
Separate state account: In a multi-account AWS Organization, the state S3 bucket and DynamoDB table should live in a dedicated infrastructure/shared-services account, not the workload account. This allows engineers to access state without direct workload account access — a least-privilege principle (AC-6) that reduces the blast radius of credential compromise.
Setting Up the State Bootstrap
The S3 bucket and DynamoDB table themselves must be created before Terraform can use them as a backend. This is the classic "chicken and egg" problem — you need Terraform to create infrastructure, but Terraform state requires that infrastructure to exist first.
The solution is a separate bootstrap module, applied once using a local backend, then never changed:
# bootstrap/main.tf — applied once with local state, then never touched
resource "aws_s3_bucket" "tfstate" {
bucket = "rutagon-tfstate-govcloud-prod"
# Prevent accidental deletion of state store
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.tfstate.arn
}
}
}
resource "aws_s3_bucket_public_access_block" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_dynamodb_table" "state_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
point_in_time_recovery {
enabled = true
}
server_side_encryption {
enabled = true
kms_key_arn = aws_kms_key.tfstate.arn
}
} The prevent_destroy = true lifecycle is critical. Accidentally running terraform destroy on the bootstrap stack would delete all state — an unrecoverable disaster in a production environment.
Workspace Strategy for Government Programs
Terraform workspaces allow multiple state files within the same backend, useful for environment isolation. The pattern we use:
services/
api-service/
terraform.tfstate # workspace: default (unused)
env:/
dev/terraform.tfstate
staging/terraform.tfstate
prod/terraform.tfstate Each environment gets its own state file in the same S3 bucket, separated by workspace. The key convention:
# S3 key becomes: env:/prod/services/api-service/terraform.tfstate
terraform {
backend "s3" {
bucket = "rutagon-tfstate-govcloud-prod"
key = "services/api-service/terraform.tfstate"
# workspace_key_prefix defaults to "env:"
}
} Workspace vs. separate backends: For production government programs, we generally recommend separate backends (separate S3 buckets) for production vs. lower environments rather than workspaces-only isolation. This provides:
- IAM-enforced access control — developers have backend access in dev/staging, not production
- Cleaner audit trail — production state changes are in a separate location with separate logging
- Reduced blast radius — a state corruption in staging doesn't affect production backend
CI/CD Integration: Who Can Apply
In a government program, uncontrolled terraform apply from developer laptops is an access control risk and creates an unaudited change pathway. The approved pattern:
- Developers:
terraform planlocally (read-only state access) - Merge to main: CI/CD pipeline runs
terraform plan— output reviewed as part of merge request - After approval gate: CI/CD pipeline runs
terraform applyusing an OIDC-federated role with write access to the production state and workload accounts - Humans: Never have IAM credentials with production
terraform applyrights — only the pipeline role does
# .gitlab-ci.yml — production apply with OIDC
terraform-apply-prod:
stage: deploy
environment: production
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual # Requires explicit approval in GitLab
before_script:
- export AWS_ROLE_ARN="arn:aws-us-gov:iam::PROD_ACCOUNT:role/TerraformApplyRole"
- export AWS_WEB_IDENTITY_TOKEN_FILE="${CI_JOB_JWT_V2_FILE}"
script:
- terraform init -backend-config="key=services/api-service/terraform.tfstate"
- terraform workspace select prod
- terraform apply -auto-approve -var-file=envs/prod.tfvars This pipeline pattern — OIDC identity exchange, no stored AWS credentials, manual approval gate for production — satisfies CM-3 (Configuration Change Control), AC-6 (Least Privilege), and AU-2/AU-12 (Audit Events) for the infrastructure change pathway.
Discuss your government cloud architecture with Rutagon →
Frequently Asked Questions
Why can't I store Terraform state in my Git repository?
Git is not designed for Terraform state storage: state files contain sensitive values (resource ARNs, potentially secrets), are modified concurrently (causing merge conflicts), and grow continuously (a large environment's state can exceed 10MB). More importantly, state in Git doesn't provide DynamoDB-style locking, meaning concurrent applies can corrupt state. Remote S3 backend with DynamoDB locking is the production-standard pattern.
How do I handle state migration when moving to GovCloud?
State migration from commercial AWS to GovCloud requires a terraform state mv or re-importing resources into a new GovCloud-backed state. Most government programs start fresh on GovCloud rather than migrating from commercial. If migrating an existing system, the process involves: exporting current state, updating provider configuration for GovCloud endpoints, initializing the new GovCloud backend, and running terraform import for resources that exist in GovCloud. This is manageable but requires careful planning to avoid downtime.
What IAM permissions does the Terraform role need?
The minimum permissions follow least-privilege: only the specific service actions your Terraform code actually uses. In practice, a full Terraform execution role often needs broad permissions across the services it manages — EC2, RDS, IAM, S3, etc. The safeguard is that only the CI/CD pipeline holds this role (via OIDC), not individual developers. For production, separate the plan role (read-only) from the apply role (read-write) and scope each to the specific accounts and services they touch.
How should secrets be handled in Terraform for government programs?
Secrets should never appear in Terraform state as plain text values. The pattern: sensitive values are stored in AWS Secrets Manager or Parameter Store (SecureString type), and Terraform references them by ARN/path using data sources — not by value. If a secret value must exist in state temporarily (e.g., a generated RDS password), mark it as sensitive = true in the output to prevent console logging, and ensure the S3 bucket encryption with CMK is in place.
What does Terraform state look like for FedRAMP ConMon purposes?
S3 bucket versioning on the state bucket creates an immutable history of every state file version — every terraform apply generates a new version. Combined with S3 server access logging and CloudTrail on the S3 bucket API calls, you have: who accessed the state, when they accessed it, what version was current, and what changed after each apply. This directly satisfies AU-9 (Protection of Audit Information) and CM-3 (Configuration Change Control) evidence requirements.
Discuss your project with Rutagon
Contact Us →