Skip to main content
INS // Insights

IaC Terraform for DoD Cloud Programs: Production Patterns

Updated May 2026 · 6 min read

Infrastructure as Code on DoD cloud programs isn't just about automation — it's about auditability. Every infrastructure resource must be traceable to a reviewed-and-approved Terraform change. Every configuration value must be in version control. Every deployment must leave an evidence trail that the ISSO and AO can review.

Rutagon's Terraform patterns for DoD programs are built around these requirements from the ground up — not bolted on as compliance theater at the end of a project.

Repository Structure: Mono-Repo vs. Multi-Repo

For DoD programs with multiple environments (dev, staging, production) and potentially multiple IL levels, the repository structure determines how well the IaC scales:

gov-cloud-infra/
├── modules/                    # Reusable modules (internal or tested external)
│   ├── vpc/
│   ├── eks/
│   ├── rds/
│   └── security/
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   ├── staging/
│   └── prod/
├── policy/                     # OPA/Sentinel policies for compliance checks
│   ├── stig-baseline.rego
│   └── il2-controls.rego
└── scripts/
    ├── plan-with-checks.sh     # Plan + OPA evaluation
    └── collect-evidence.sh     # Post-apply evidence collection

Modules: Internal modules for security-sensitive components (VPC, EKS) — external modules from public registries aren't appropriate for IL programs where module provenance and security review can't be verified. Modules are pinned to specific versions (tagged releases) and reviewed before version bumps.

Environment-specific tfvars: Variable values that differ by environment (CIDR blocks, instance sizes, replication settings) live in terraform.tfvarsnot in code. Secrets never appear in tfvars files (those go in SSM Parameter Store or Secrets Manager and are fetched dynamically in Terraform using data sources).

Remote state with locking: Terraform state files in S3 with DynamoDB state locking and KMS encryption. State files contain resource configurations (though not secrets) — they're stored with appropriate access controls and versioning.

Plan-Before-Apply with Compliance Pre-Check

Every infrastructure change follows a plan → review → apply workflow. The compliance pre-check runs between plan and apply:

#!/bin/bash
# plan-with-checks.sh
set -e

ENVIRONMENT=$1
PLAN_FILE="terraform-plan.json"

echo "=== Terraform Plan ==="
terraform -chdir=environments/${ENVIRONMENT} plan \
  -out=tfplan \
  -var-file="terraform.tfvars" \
  -lock=true

# Export plan as JSON for OPA evaluation
terraform -chdir=environments/${ENVIRONMENT} show \
  -json tfplan > ${PLAN_FILE}

echo "=== OPA Compliance Check ==="
opa eval \
  --input ${PLAN_FILE} \
  --data policy/stig-baseline.rego \
  --format pretty \
  "data.stig.deny" | tee opa-results.txt

# Fail pipeline if OPA finds violations
if grep -q '"result":' opa-results.txt && ! grep -q '"result": \[\]' opa-results.txt; then
  echo "COMPLIANCE VIOLATION: Review OPA results before applying"
  exit 1
fi

echo "=== Plan passed compliance checks ==="
echo "Plan file: ${PLAN_FILE}"
echo "OPA results: opa-results.txt"

OPA (Open Policy Agent) evaluates the plan JSON against STIG-derived policies before allowing apply. Example OPA policy for encryption:

# stig-baseline.rego
package stig

# Deny EBS volumes without encryption
deny[msg] {
  resource := input.planned_values.root_module.resources[_]
  resource.type == "aws_ebs_volume"
  resource.values.encrypted != true
  msg := sprintf("EBS volume %v must be encrypted", [resource.address])
}

# Deny RDS instances without encryption
deny[msg] {
  resource := input.planned_values.root_module.resources[_]
  resource.type == "aws_db_instance"
  resource.values.storage_encrypted != true
  msg := sprintf("RDS instance %v must have storage_encrypted = true", [resource.address])
}

# Deny security groups with unrestricted SSH
deny[msg] {
  resource := input.planned_values.root_module.resources[_]
  resource.type == "aws_security_group_rule"
  resource.values.type == "ingress"
  resource.values.from_port <= 22
  resource.values.to_port >= 22
  resource.values.cidr_blocks[_] == "0.0.0.0/0"
  msg := sprintf("Security group rule %v allows SSH from 0.0.0.0/0", [resource.address])
}

This approach catches STIG violations before they're deployed — preventing findings that require remediation and evidence of the remediation for the ATO package.

Drift Detection

Configuration drift — infrastructure state diverging from the Terraform state — is a STIG and ATO evidence problem. Manual changes to infrastructure outside of Terraform can introduce misconfigurations that aren't captured in the evidence record.

Rutagon runs scheduled terraform plan checks against the running infrastructure:

# GitLab CI — scheduled drift detection
drift-detection:
  stage: check
  script:
    - terraform -chdir=environments/${ENVIRONMENT} plan \
        -detailed-exitcode \
        -lock=false \
        -var-file="terraform.tfvars" \
        -no-color \
        2>&1 | tee drift-report.txt

    # Exit code 2 = there are changes (drift)
    - |
      exit_code=${PIPESTATUS[0]}
      if [ $exit_code -eq 2 ]; then
        echo "DRIFT DETECTED — infrastructure differs from Terraform state"
        # Alert goes to ISSO dashboard
        python3 scripts/alert-drift.py drift-report.txt
        exit 1
      fi
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'

Drift detection runs nightly. Detected drift triggers an alert to the ISSO dashboard and blocks the next planned deployment until the drift is resolved (either by applying the Terraform baseline or justifying the manual change and updating the Terraform code).

View Rutagon's cloud engineering approach → rutagon.com/government

Frequently Asked Questions

What version of Terraform do you use on DoD programs?

Rutagon uses OpenTofu (the CNCF-hosted open-source fork of Terraform) or Terraform pre-1.6 on programs with licensing constraints. For programs without licensing constraints, Terraform 1.x (currently 1.7/1.8 range) is standard. The IaC runtime is pinned to a specific version in the CI/CD environment (Docker image tag) to prevent version drift across the team.

How do you handle Terraform secrets on GovCloud?

Secrets (database passwords, API keys, certificates) are never in Terraform code or tfvars files. Values stored in AWS SSM Parameter Store (SecureString with KMS encryption) or Secrets Manager are referenced in Terraform using aws_ssm_parameter or aws_secretsmanager_secret_version data sources — fetched at plan/apply time using the pipeline's IAM role. The secret value is used directly (referenced as an attribute value in the resource) without appearing in code.

What happens when a STIG baseline update requires changing existing infrastructure?

DISA releases STIG updates periodically. When a new control requires changing an existing Terraform configuration, Rutagon's process: (1) identify impacted resources via Terraform state query, (2) update module code to incorporate the new control requirement, (3) run the full plan → OPA check → apply workflow, (4) collect post-apply evidence artifacts, and (5) update the ATO package to reflect the new control implementation. The change is tracked in the PR as a compliance improvement.

Can you use Terraform modules from the public Terraform Registry on DoD programs?

For programs at IL2 and above, public registry modules require review before use — Rutagon reviews module source code for security issues, hardcoded credentials, and behavior that could violate program security requirements. Approved external modules are forked into the program's internal registry or vendored into the IaC repository. Unreviewed public modules aren't used directly in production IL environments.

How is the Terraform state file secured?

Remote state is stored in S3 with: versioning enabled (state file history preserved), server-side encryption (KMS-managed key specific to the program), access restricted to the pipeline IAM role and designated operators via IAM policies, and CloudTrail logging on the S3 bucket. State files don't contain secret values (assuming secrets are handled through data sources as described above), but they do contain resource IDs and configuration values — they're treated as sensitive infrastructure documentation.

Ready to discuss your project?

We deliver production-grade software for government, defense, and commercial clients. Let's talk about what you need.

Initiate Contact