Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- .woodpecker.yaml: image paths -> library/autojanet-{agent,dispatcher}
- .woodpecker.yaml: secret names RS_HARBOR_USER / RS_HARBOR_PASS (global)
- container/Dockerfile: restore COPY skills/, skills/ populated from opencode config
- skills/: 84 opencode skills bundled into image
- k8s/manifests: update image refs to library/
166 lines
4.6 KiB
Markdown
166 lines
4.6 KiB
Markdown
---
|
|
name: opentofu-module
|
|
description: Use when writing, editing, or reviewing OpenTofu/Terraform modules or configurations across AWS, Azure, OCI, or homelab environments. Triggers on tasks involving HCL, tofu commands, state management, IAM, EKS, IRSA, backends, or AFT.
|
|
---
|
|
|
|
# OpenTofu Module Skill
|
|
|
|
## Overview
|
|
|
|
Write and maintain OpenTofu modules for Zoe's infrastructure. Use `tofu` commands (not `terraform`). Providers span AWS (primary), Azure, and OCI free tier.
|
|
|
|
## Workflow
|
|
|
|
1. Define variables + outputs **before** writing resources — prevents refactoring
|
|
2. Write `versions.tf` first — sets provider constraints
|
|
3. Write resources in `main.tf`
|
|
4. `tofu fmt -recursive` and `tflint` before plan
|
|
5. `checkov -d .` — fix HIGH/CRITICAL before applying
|
|
6. `tofu plan -out=plan.out` → review → `tofu apply plan.out`
|
|
7. **Never `tofu apply` without a plan file in production**
|
|
|
|
## Standard Module Structure
|
|
|
|
```
|
|
modules/<name>/
|
|
main.tf # resources
|
|
variables.tf # input variables with descriptions and types
|
|
outputs.tf # exported values
|
|
versions.tf # required_providers with version constraints
|
|
README.md # auto-generated by terraform-docs — do NOT write manually
|
|
```
|
|
|
|
## Key Patterns
|
|
|
|
### versions.tf
|
|
|
|
```hcl
|
|
terraform {
|
|
required_version = ">= 1.6"
|
|
required_providers {
|
|
aws = {
|
|
source = "hashicorp/aws"
|
|
version = "~> 5.0"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### variables.tf
|
|
|
|
```hcl
|
|
variable "cluster_name" {
|
|
description = "Name of the EKS cluster"
|
|
type = string
|
|
}
|
|
|
|
variable "tags" {
|
|
description = "Tags to apply to all resources"
|
|
type = map(string)
|
|
default = {}
|
|
}
|
|
```
|
|
|
|
### S3 Backend
|
|
|
|
```hcl
|
|
terraform {
|
|
backend "s3" {
|
|
bucket = "company-tofu-state"
|
|
key = "env/production/cluster/terraform.tfstate"
|
|
region = "us-west-2"
|
|
dynamodb_table = "terraform-state-lock"
|
|
encrypt = true
|
|
}
|
|
}
|
|
```
|
|
|
|
### IRSA (IAM Roles for Service Accounts) — EKS
|
|
|
|
Comes up constantly. Full pattern:
|
|
|
|
```hcl
|
|
data "aws_eks_cluster" "cluster" {
|
|
name = var.cluster_name
|
|
}
|
|
|
|
data "aws_iam_openid_connect_provider" "cluster" {
|
|
url = data.aws_eks_cluster.cluster.identity[0].oidc[0].issuer
|
|
}
|
|
|
|
data "aws_iam_policy_document" "assume_role" {
|
|
statement {
|
|
effect = "Allow"
|
|
actions = ["sts:AssumeRoleWithWebIdentity"]
|
|
principals {
|
|
type = "Federated"
|
|
identifiers = [data.aws_iam_openid_connect_provider.cluster.arn]
|
|
}
|
|
condition {
|
|
test = "StringEquals"
|
|
variable = "${replace(data.aws_iam_openid_connect_provider.cluster.url, "https://", "")}:sub"
|
|
values = ["system:serviceaccount:${var.namespace}:${var.service_account_name}"]
|
|
}
|
|
}
|
|
}
|
|
|
|
resource "aws_iam_role" "irsa" {
|
|
name = "${var.cluster_name}-${var.name}-irsa"
|
|
assume_role_policy = data.aws_iam_policy_document.assume_role.json
|
|
tags = var.tags
|
|
}
|
|
```
|
|
|
|
### Cross-Account AssumeRole (OrganizationAccountAccessRole)
|
|
|
|
```hcl
|
|
data "aws_iam_policy_document" "trust" {
|
|
statement {
|
|
effect = "Allow"
|
|
actions = ["sts:AssumeRole"]
|
|
principals {
|
|
type = "AWS"
|
|
# Use the role ARN pattern — NOT AWSReservedSSO_* (causes MalformedPolicyDocument)
|
|
identifiers = ["arn:aws:iam::${var.management_account_id}:role/${var.deployer_role_name}"]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Critical Gotcha: AWSReservedSSO_* Roles
|
|
|
|
**NEVER use `AWSReservedSSO_*` ARNs as IAM principals in trust policies.**
|
|
|
|
- Error: `MalformedPolicyDocument: Invalid principal in policy`
|
|
- Fix: Use the underlying permission set role name pattern, or use `aws:PrincipalOrgID` condition instead
|
|
|
|
## State Operations
|
|
|
|
```bash
|
|
# Push local state to remote after manual work
|
|
tofu state push terraform.tfstate --force
|
|
|
|
# Import existing resource
|
|
tofu import aws_s3_bucket.example my-bucket-name
|
|
|
|
# Move resource between state paths (refactoring)
|
|
tofu state mv module.old.aws_instance.web module.new.aws_instance.web
|
|
```
|
|
|
|
## Toolchain Quick Reference
|
|
|
|
| Tool | Command | Purpose |
|
|
|------|---------|---------|
|
|
| Format | `tofu fmt -recursive` | Before every commit |
|
|
| Lint | `tflint` | Catch provider-specific issues |
|
|
| Security | `checkov -d .` | Fix HIGH/CRITICAL before apply |
|
|
| Docs | `terraform-docs markdown . > README.md` | README generation only |
|
|
| Plan | `tofu plan -out=plan.out` | Always use plan files in prod |
|
|
|
|
## Environment Notes
|
|
|
|
- **AWS**: EKS, EBS, EFS, IAM, S3, Lambda, CodePipeline, GuardDuty, RDS Aurora, Organizations/AFT
|
|
- **Azure**: AKS, Azure DevOps — state backend uses Azure Blob Storage
|
|
- **OCI**: Free tier, budgets, some compute
|
|
- **AFT**: Used for AWS org account provisioning via Terraform
|
|
- **State backends**: S3+DynamoDB (AWS), Azure Blob (Azure)
|