Infrastructure as code
Skill MARUCIE/openclaw-foundry/web/public/packs/spellbook-platform-engineer/skills/infrastructure-as-code
Use when writing Terraform for cloud resources, setting up remote state, structuring modules for reuse, managing multiple environments, reviewing a plan before apply, or importing and resolving state drift.From its SKILL.md
npx -y skills add MARUCIE/openclaw-foundry --skill infrastructure-as-codeAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
13.7 KB, ~3.6k tokens by cl100k_base, as published. Nobody here has run it
是什么
这是一份 IaC(基础设施即代码)规范,覆盖 Terraform 模块结构、远端 state(状态文件)配置、多环境分离、plan 评审、状态漂移修复,让基础设施变更和业务代码一样走 PR 评审,告别手工点云控制台。
怎么用
- 新项目立项时,按本文档的目录骨架建好 modules、envs、shared 三层结构,避免后期重构成本。
- 远端 state 必须配 backend(远端存储)和锁机制,防止两个人同时 apply 把状态弄坏。
- 提 PR 时附上 terraform plan 输出,Review 时重点看 destroy 资源和 replace 资源两类高危改动。
- 状态漂移时按文档的 import 与 refresh 流程修复,不要直接 apply 覆盖,避免误删生产资源。
- 多环境用 workspace 或目录隔离,按规范配 variables,让 dev/staging/prod 走同一份代码不同参数。
架构图
flowchart LR
A[Terraform 代码] --> B[PR 评审]
B --> C[Plan 输出]
C --> D[审批通过]
D --> E[Apply 部署]
E --> F[远端 State]
Infrastructure as Code
Terraform lets you define, provision, and version cloud infrastructure as declarative HCL code, enabling repeatable and reviewable infrastructure changes.
When to Activate
- Writing Terraform for cloud resources (VPC, RDS, EKS, IAM, etc.)
- Setting up a Terraform state backend
- Creating a reusable Terraform module
- Managing multiple environments (dev/staging/prod) with Terraform
- Reviewing a
terraform planbefore applying - Dealing with state drift or importing existing resources
Core Building Blocks
# Provider — connects Terraform to a cloud API
terraform {
required_version = ">= 1.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# Variable — parameterise configuration
variable "aws_region" {
type = string
description = "AWS region to deploy into"
default = "us-east-1"
validation {
condition = can(regex("^[a-z]{2}-[a-z]+-[0-9]$", var.aws_region))
error_message = "Must be a valid AWS region code."
}
}
# Local — computed values used inside the module
locals {
name_prefix = "${var.environment}-${var.app_name}"
common_tags = {
Environment = var.environment
ManagedBy = "terraform"
App = var.app_name
}
}
# Resource — a cloud resource
resource "aws_s3_bucket" "app_data" {
bucket = "${local.name_prefix}-app-data"
tags = local.common_tags
}
# Data source — read existing resource without managing it
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
# Output — export values for other modules or humans
output "s3_bucket_arn" {
value = aws_s3_bucket.app_data.arn
description = "ARN of the application data bucket"
}
State Management
Remote State Backend
State must be remote and locked — never store terraform.tfstate in git.
AWS (S3 + DynamoDB lock):
terraform {
backend "s3" {
bucket = "my-org-terraform-state"
key = "services/payment-service/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks" # partition key: LockID (String)
}
}
GCP (GCS):
terraform {
backend "gcs" {
bucket = "my-org-terraform-state"
prefix = "services/payment-service"
}
}
State Commands
terraform state list # list all managed resources
terraform state show aws_s3_bucket.data # inspect a resource's state
terraform state mv OLD_ADDR NEW_ADDR # rename without destroying
terraform state rm aws_s3_bucket.old # remove from state (doesn't destroy)
terraform force-unlock LOCK_ID # release a stuck lock
State file security: The state file contains sensitive values (RDS passwords, private keys). Ensure S3 bucket has:
- Versioning enabled (recover from bad apply)
- Server-side encryption
- Block public access
- Access restricted to CI role + team IAM role only
Module Structure
modules/
└── rds-postgres/
├── main.tf # resources
├── variables.tf # inputs
├── outputs.tf # outputs
└── README.md # usage docs (required for shared modules)
Module Example
# modules/rds-postgres/variables.tf
variable "instance_class" {
type = string
default = "db.t3.medium"
}
variable "db_name" { type = string }
variable "subnet_ids" { type = list(string) }
variable "vpc_id" { type = string }
variable "tags" { type = map(string); default = {} }
# modules/rds-postgres/outputs.tf
output "endpoint" { value = aws_db_instance.this.endpoint }
output "db_name" { value = aws_db_instance.this.db_name }
output "secret_arn" { value = aws_secretsmanager_secret.db_password.arn }
# Consuming the module
module "payment_db" {
source = "../../modules/rds-postgres"
db_name = "payments"
instance_class = "db.t3.large"
subnet_ids = module.vpc.private_subnet_ids
vpc_id = module.vpc.vpc_id
tags = local.common_tags
}
Module Versioning
# Pin to a Git tag (preferred for shared modules)
module "rds" {
source = "git::https://github.com/my-org/tf-modules.git//rds-postgres?ref=v2.1.0"
}
# Terraform Registry
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
}
Environment Strategy
Directory-per-Environment (recommended)
infra/
├── modules/
│ ├── vpc/
│ └── rds-postgres/
└── environments/
├── dev/
│ ├── main.tf # calls modules
│ ├── terraform.tfvars # dev-specific values
│ └── backend.tf # dev state backend
├── staging/
│ └── ...
└── prod/
└── ...
Pros: complete isolation, different providers per env, easy to cd into.
Cons: some code duplication across environments.
Workspace (alternative)
terraform workspace new dev
terraform workspace select staging
terraform workspace list
Use terraform.workspace in HCL:
locals {
instance_type = terraform.workspace == "prod" ? "db.r6g.xlarge" : "db.t3.medium"
}
Decision: Use directory-per-environment for significant infrastructure differences between envs. Use workspaces only for identical infrastructure with minor variable differences.
tfvars per Environment
# environments/prod/terraform.tfvars
aws_region = "us-east-1"
environment = "prod"
instance_class = "db.r6g.xlarge"
min_capacity = 3
max_capacity = 20
Plan/Apply Workflow
# 1. Init (first time, or after source changes)
terraform init
# 2. Format and validate
terraform fmt -recursive
terraform validate
# 3. Plan — save output for reproducible apply
terraform plan -out=tfplan -var-file=terraform.tfvars
# 4. Policy check (optional, using OPA/Conftest)
terraform show -json tfplan | conftest test -
# 5. Apply from the saved plan (no re-planning)
terraform apply tfplan
# 6. Verify
terraform state list
CI Pipeline Integration
# .github/workflows/terraform.yml
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.7.0"
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/terraform-plan
aws-region: us-east-1
- run: terraform init
- run: terraform plan -out=tfplan
- run: terraform show -json tfplan > tfplan.json
- name: Policy check
run: conftest test tfplan.json --policy policies/
- uses: actions/upload-artifact@v4
with:
name: tfplan
path: tfplan
apply:
needs: plan
if: github.ref == 'refs/heads/main'
environment: production # requires approval in GitHub
steps:
- uses: actions/download-artifact@v4
with: { name: tfplan }
- run: terraform apply tfplan
Common Resource Patterns
VPC Networking (AWS)
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "${local.name_prefix}-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = var.environment != "prod" # save cost in non-prod
tags = local.common_tags
}
IAM Role + Policy (least privilege)
resource "aws_iam_role" "app_role" {
name = "${local.name_prefix}-app"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ec2.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
tags = local.common_tags
}
resource "aws_iam_role_policy" "app_policy" {
name = "app-policy"
role = aws_iam_role.app_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject"]
Resource = "${aws_s3_bucket.app_data.arn}/*"
},
{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = aws_secretsmanager_secret.db_password.arn
}
]
})
}
RDS Instance
resource "aws_db_instance" "postgres" {
identifier = "${local.name_prefix}-postgres"
engine = "postgres"
engine_version = "16.2"
instance_class = var.db_instance_class
allocated_storage = 100
storage_encrypted = true
db_name = var.db_name
username = "app"
password = random_password.db.result
db_subnet_group_name = aws_db_subnet_group.this.name
vpc_security_group_ids = [aws_security_group.rds.id]
backup_retention_period = var.environment == "prod" ? 30 : 7
deletion_protection = var.environment == "prod"
skip_final_snapshot = var.environment != "prod"
tags = local.common_tags
}
Drift Detection and Import
Detect Drift
terraform plan # shows resources that diverged from state
Any ~ (update) or -/+ (replace) on a resource you haven't changed = drift.
Import Existing Resources
# Import by resource address and cloud ID
terraform import aws_s3_bucket.legacy my-existing-bucket
# After import, write matching HCL or Terraform will show a diff
moved Block (safe refactoring)
# Rename a resource without destroying it
moved {
from = aws_s3_bucket.data
to = aws_s3_bucket.app_data
}
See also:
ci-cd,containerization,security
Red Flags
- Storing
terraform.tfstatein git — state files contain plaintext secrets (RDS passwords, private keys); use an S3+DynamoDB or GCS backend with server-side encryption from day one - Running
terraform applydirectly without a saved plan —terraform applywithout-out=tfplanre-plans at apply time; what was reviewed in the PR and what actually runs can differ if state changed between plan and apply - Using
terraform apply -auto-approvein CI on the production environment — auto-approve bypasses the human gate; production applies must require explicit approval via a GitHub environment protection rule - Module pinned to
mainor with no version constraint —source = "git::...?ref=main"means any upstream commit silently changes your infrastructure; pin to a specific git tag or Terraform registry version - IAM policy with
"Action": "*"or"Resource": "*"— wildcard actions on all resources violates least privilege; scope to the exact actions and resource ARNs the role actually needs terraform state rmused to "fix" a drift problem — removing a resource from state without destroying it creates orphaned cloud resources that accumulate cost and may introduce security gaps; usemovedblocks orterraform importinstead- Deleting a Terraform resource block to decommission a resource — removing the block from HCL causes
terraform planto show a destroy; validate intent withterraform planand addlifecycle { prevent_destroy = true }on stateful resources - Sharing a single state file across all environments — one bad apply in staging can corrupt or lock the production state; each environment must have its own state file with its own backend key
Checklist
- Remote state backend configured (S3+DynamoDB or GCS) — state never committed to git
- State S3 bucket has versioning, encryption, and public access blocked
- All resources tagged with
environment,app, andmanaged_by = "terraform" - Infrastructure split into reusable modules with
variables.tfandoutputs.tf - Modules pinned to specific versions (git tag or registry version constraint)
-
terraform plan -out=tfplanused — apply from saved plan, not a re-plan - Policy checks (OPA/Conftest) run on plan JSON before apply
- Production apply requires human approval (GitHub environment protection)
- IAM roles follow least privilege — no
"*"actions or resources in policy - Deletion protection enabled on RDS and other stateful prod resources
-
movedblocks used for resource renames — never destroy-and-recreate -
terraform planrun after every manual change to detect drift
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.