Terraform Operations¶
All AWS infrastructure changes must go through Terraform. Never create or modify resources in the AWS Console. Always operate at the module level, never at the parent environment directory.
Repository: terraform-plan/
Module-Level Operations¶
The terraform-plan/ repository is organized into independent root modules. Each module under environments/{account}/ has its own backend.tf and provider.tf for independent state management.
Always run Terraform at the module level
Run terraform init, terraform plan, and terraform apply inside a specific module directory (e.g., environments/dev/eks-cluster/), NOT at the parent environments/dev/ directory. The parent directory does not contain a unified root module — each child is a standalone root module with its own state.
terraform-plan/
organization/
environments/
shared-services/
ec2-instances/
gitlab/
nexus/
sonarqube/ <- run terraform here
dev/
eks-cluster/ <- run terraform here
ec2-instances/
orchestrator/ <- run terraform here
Standard Workflow¶
# 1. Navigate to the module directory
cd terraform-plan/organization/environments/dev/eks-cluster/
# 2. Initialize (downloads providers, configures backend)
terraform init
# 3. Plan — review all proposed changes before applying
terraform plan \
-var="tailscale_auth_key=$TAILSCALE_AUTH_KEY" \
-var="created_by=$CREATED_BY"
# 4. Apply — only after reviewing the plan output
terraform apply \
-var="tailscale_auth_key=$TAILSCALE_AUTH_KEY" \
-var="created_by=$CREATED_BY"
Required Variables¶
| Variable | Source | Required | Purpose |
|---|---|---|---|
tailscale_auth_key |
.claude-env or Vault |
Yes (for modules with Tailscale resources) | Tailscale provider authentication for ACL changes |
created_by |
.claude-env |
Always mandatory | Tags every AWS resource with the operator's identity |
CREATED_BY is mandatory for all Terraform operations
The created_by variable tags every AWS resource with the operator's identity (e.g., ateodoro). This is required for accountability and cost attribution. If CREATED_BY is not set in your environment, set it before running any Terraform command:
Loading Variables from .claude-env¶
# Load environment variables from .claude-env
source .claude-env
# Verify they are set
echo $TAILSCALE_AUTH_KEY
echo $CREATED_BY
# Then run Terraform
terraform plan \
-var="tailscale_auth_key=$TAILSCALE_AUTH_KEY" \
-var="created_by=$CREATED_BY"
State Backend¶
All Terraform state is stored in S3 in the shared-services account, regardless of which account the resources are created in:
# backend.tf (example from dev/eks-cluster)
terraform {
backend "s3" {
bucket = "cwiq-terraform-states"
key = "cwiq-io/dev/eks-cluster/terraform.tfstate"
region = "us-west-2"
profile = "shared-services"
}
}
State is always in shared-services, resources may be in dev
The profile = "shared-services" in backend.tf controls where state is stored. The profile in provider.tf controls where resources are created. A module can store state in shared-services while creating EC2 instances in the dev account.
Adding a New EC2 Instance¶
When adding a new EC2 instance via Terraform, you must follow these requirements:
1. Use AlmaLinux 9 as the Default AMI¶
All new EC2 instances must use AlmaLinux 9 unless there is a documented reason for a different OS:
| AMI ID | OS | Region | Use For |
|---|---|---|---|
ami-02169c46e1cfcd5e7 |
AlmaLinux 9.7.20251118 x86_64 | us-west-2 | Default — all new instances |
ami-08952be33a4ff3495 |
AlmaLinux 10.1.20251124 x86_64 | us-west-2 | Only if AL10 is explicitly required |
AlmaLinux 10 compatibility
AlmaLinux 10 has known incompatibilities with SSSD proxy mode (PAM preauth task 249 bug in SSSD 2.11/PAM 1.6). Use AlmaLinux 9 for all new instances unless you have explicitly tested AL10 compatibility for your use case.
Copy the AMI variable from an existing module rather than hardcoding it:
# variables.tf
variable "ami_id" {
description = "AlmaLinux 9 AMI ID (us-west-2)"
type = string
default = "ami-02169c46e1cfcd5e7"
}
2. Include a Cost Projection in Your MR¶
Every MR that adds a new EC2 instance must include a cost projection:
| Cost Component | Calculation |
|---|---|
| Instance (On-Demand) | Monthly rate for the selected instance type in us-west-2 |
| Root EBS (gp3) | GB x $0.08/month |
| Data EBS (gp3) | GB x $0.08/month |
| Compute Savings Plan | ~47% discount on instance cost (3yr No Upfront) |
| Total monthly | Instance + storage (after Savings Plan) |
Quick reference — us-west-2 On-Demand rates:
| Instance Type | Monthly (On-Demand) | With 3yr Savings Plan |
|---|---|---|
| t3.micro | $7.59 | ~$4.02 |
| t3.small | $15.18 | ~$8.05 |
| t3.medium | $30.37 | ~$16.10 |
| t3.large | $60.74 | ~$32.19 |
| t3.xlarge | $121.47 | ~$64.38 |
Full pricing analysis: terraform-plan/docs/cost/COST_OPTIMIZATION.md
3. Tag All Resources¶
The created_by variable must flow through to resource tags:
# main.tf
resource "aws_instance" "example" {
ami = var.ami_id
instance_type = var.instance_type
tags = {
Name = var.instance_name
Environment = var.environment
CreatedBy = var.created_by
ManagedBy = "terraform"
}
}
Provider Configuration¶
Each module's provider.tf specifies which AWS account it targets:
# provider.tf — dev resources
provider "aws" {
region = "us-west-2"
profile = "dev"
}
# provider.tf — shared-services resources
provider "aws" {
region = "us-west-2"
profile = "shared-services"
}
If you find a module without a profile in its provider configuration, do not run it. Ask the infrastructure team which account it should target before proceeding.
Reviewing a Plan Output¶
Before applying any Terraform plan, verify:
- No unexpected destroys — Look for
# module.x will be destroyed. A destroy of a running service (EC2, RDS, EKS node group) requires explicit sign-off. - Correct account — Confirm the provider profile matches the account where you expect changes.
- Resource count — The plan summary line (
Plan: X to add, Y to change, Z to destroy) should match your intended scope. - Tag coverage — Spot-check a resource's tags block to confirm
created_byandManagedByare present.
# Save a plan to a file for review
terraform plan \
-var="tailscale_auth_key=$TAILSCALE_AUTH_KEY" \
-var="created_by=$CREATED_BY" \
-out=tfplan
# Apply the saved plan (no re-evaluation)
terraform apply tfplan
What Terraform Manages¶
| Resource Type | Terraform Module Location |
|---|---|
| VPC, subnets, routing | environments/{account}/vpc/ |
| EC2 instances | environments/{account}/ec2-instances/{service}/ |
| IAM roles and policies | environments/{account}/iam/ |
| EKS cluster and node groups | environments/dev/eks-cluster/ |
| Route53 records | environments/shared-services/route53/ |
| Tailscale ACLs | environments/shared-services/tailscale/ |
| Security groups | Co-located with the service module that owns them |
Related Documentation¶
- AWS Accounts — Account IDs, purposes, and which account owns which resources
- CLI Profiles — How to configure and verify AWS CLI profiles before running Terraform