|
1 | 1 | --- |
2 | | -description: 'Brief description of the instruction purpose and scope' |
3 | | -applyTo: 'terraform/**' |
| 2 | +description: 'Comprehensive guidelines for writing, organizing, and maintaining Terraform code in this repository.' |
| 3 | +applyTo: 'terraform/**/*.tf' |
4 | 4 | --- |
5 | 5 |
|
6 | | -# Copilot Authoring Guide for Terraform in this Repository |
| 6 | +# Terraform Development Guidelines |
7 | 7 |
|
8 | | -This guide tunes AI code completions for our Terraform code under `terraform/`. It encodes patterns and constraints we want Copilot to follow. Completions that violate these rules should be discarded or re-asked. |
| 8 | +This document provides best practices and conventions for writing, organizing, and maintaining Terraform code. It is intended for use by developers and GitHub Copilot to ensure consistency, reliability, and maintainability across all Terraform files in the project. |
9 | 9 |
|
10 | | -## Core Principles |
11 | | -- Deterministic, explicit infrastructure: prefer explicit resource blocks over opaque modules unless a shared module already exists in `terraform/modules/`. |
12 | | -- Consistent tagging & naming: all AWS resources must include `default_tags` or explicit `tags` matching our local tags set (see below). |
13 | | -- Environment isolation via Terraform workspaces: never hardcode environment names; derive with `terraform.workspace` in locals. |
14 | | -- Minimise blast radius: use variables for mutable capacity / feature toggles; avoid inline wildcards except where already accepted (some IAM policies intentionally use `*`). |
15 | | -- Security first: prefer least privilege in new policies. Do NOT introduce new broad `"*"` actions unless strongly justified. |
16 | | -- Idempotent & import-friendly: avoid interpolations that create random strings; no `random_*` resources unless approved. |
| 10 | +## General Instructions |
| 11 | + |
| 12 | +- Use Terraform modules to promote code reuse and separation of concerns. |
| 13 | +- Keep resource definitions declarative and avoid imperative logic. |
| 14 | +- Store environment-specific configuration in separate files (e.g., `env/` folders). |
| 15 | +- Use variables and outputs to parameterize and expose configuration. |
| 16 | +- Document resources, modules, and variables with comments. |
| 17 | +- Prefer explicit resource dependencies using `depends_on` when needed. |
| 18 | +- Use remote state for shared resources and outputs. |
| 19 | + |
| 20 | +## Best Practices |
| 21 | + |
| 22 | +- Group related resources in logical subfolders (e.g., `archive/`, `backup-source/`). |
| 23 | +- Use `locals` for computed values and to reduce repetition. |
| 24 | +- Use data sources to reference existing infrastructure. |
| 25 | +- Avoid hardcoding values; use variables and environment files. |
| 26 | +- Use `terraform fmt` to enforce consistent formatting. |
| 27 | +- Use `terraform validate` and `terraform plan` before applying changes. |
| 28 | +- Use `Makefile` targets for common operations (init, plan, apply, destroy). |
| 29 | +- Store secrets and sensitive values in secure locations (e.g., AWS SSM, environment variables), not in code. |
| 30 | +- Use resource tags for traceability and cost management. |
| 31 | +- Prefer resource names that include environment and purpose (e.g., `archive_prod_bucket`). |
| 32 | + |
| 33 | +## Code Standards |
| 34 | + |
| 35 | +### Naming Conventions |
| 36 | + |
| 37 | +- Use snake_case for resource, variable, and output names. |
| 38 | +- Prefix resource names with their type and purpose (e.g., `s3_archive_bucket`). |
| 39 | +- Use clear, descriptive names for modules and files. |
| 40 | +- Use consistent naming for environments (e.g., `dev`, `prod`, `test`). |
| 41 | + |
| 42 | +### File Organization |
| 43 | + |
| 44 | +- Place each environment's configuration in its own file under `env/`. |
| 45 | +- Use a `variables.tf` file for input variables. |
| 46 | +- Use an `outputs.tf` file for outputs. |
| 47 | +- Use a `locals.tf` file for local values. |
| 48 | +- Use a `provider.tf` file for provider configuration. |
| 49 | +- Use a `Makefile` for automation and common tasks. |
| 50 | +- Organize resources by domain (e.g., `archive/`, `infra/`, `storage/`). |
| 51 | + |
| 52 | +## Common Patterns |
| 53 | + |
| 54 | +### Using Variables |
17 | 55 |
|
18 | | -## Standard Locals Pattern |
19 | | -Every stack defines: |
20 | 56 | ```hcl |
21 | | -locals { |
22 | | - environment = terraform.workspace |
23 | | - production = startswith(local.environment, "live") |
24 | | - data_classification = local.production ? "5" : "1" |
25 | | - aws_account_id = data.aws_caller_identity.current.account_id |
26 | | - tags = { |
27 | | - TagVersion = "1" |
28 | | - Programme = "Clinicals" |
29 | | - Project = "EPS" |
30 | | - DataClassification = local.data_classification |
31 | | - Environment = local.environment |
32 | | - ServiceCategory = local.production ? "Platinum" : "N/A" |
33 | | - Tool = "terraform" |
34 | | - Domain = "clinicals" |
35 | | - map-migrated = "mig45780" |
36 | | - } |
| 57 | +variable "bucket_name" { |
| 58 | + description = "Name of the S3 bucket" |
| 59 | + type = string |
37 | 60 | } |
38 | 61 |
|
39 | | -data "aws_caller_identity" "current" {} |
| 62 | +resource "aws_s3_bucket" "archive" { |
| 63 | + bucket = var.bucket_name |
| 64 | + ... |
| 65 | +} |
40 | 66 | ``` |
41 | | -Copilot should replicate this exact structure when creating a new stack folder. |
42 | 67 |
|
43 | | -## Provider & Backend Block |
44 | | -Use the pattern: |
| 68 | +### Using Locals |
| 69 | + |
45 | 70 | ```hcl |
46 | | -terraform { |
47 | | - required_providers { |
48 | | - aws = { |
49 | | - source = "hashicorp/aws" |
50 | | - version = "~> 5.0" |
51 | | - } |
| 71 | +locals { |
| 72 | + tags = { |
| 73 | + Environment = var.environment |
| 74 | + Project = "eps-storage" |
52 | 75 | } |
53 | | - required_version = ">=1.9.4" |
54 | 76 | } |
55 | 77 |
|
56 | | -provider "aws" { |
57 | | - region = "eu-west-2" |
58 | | - allowed_account_ids = var.allowed_account_ids |
59 | | - default_tags { tags = local.tags } |
| 78 | +resource "aws_s3_bucket" "archive" { |
| 79 | + tags = local.tags |
| 80 | + ... |
60 | 81 | } |
| 82 | +``` |
| 83 | + |
| 84 | +### Good Example - Using Modules |
61 | 85 |
|
62 | | -terraform { backend "s3" {} } |
| 86 | +```hcl |
| 87 | +module "archive" { |
| 88 | + source = "../modules/aws-archive" |
| 89 | + environment = var.environment |
| 90 | + ... |
| 91 | +} |
63 | 92 | ``` |
64 | | -Never add backend bucket/key details here; they are injected externally via init. |
65 | | - |
66 | | -## Variables Conventions |
67 | | -- Always define variables with `description` unless trivial and already established (e.g. repeated `allowed_account_ids`). |
68 | | -- Use concrete types (`list(string)`, `map(object({...}))`, `object({...})`); avoid `any`. |
69 | | -- Feature toggles use `bool` with default `false` unless enabling is safer (explicit exceptions: see existing patterns like `deploy_insights = true`). |
70 | | -- Capacity objects: follow `provisioned_capacity` shape from `storage/variables.tf` if adding similar scaling constructs. |
71 | | - |
72 | | -## Resource Naming |
73 | | -- Prefix environment only when conditional: use a `prepend_env` variable or local logic (see `iam.tf`). |
74 | | -- KMS alias pattern: `alias/${local.environment}-<purpose>`. |
75 | | -- S3 bucket pattern: `${local.environment}-spine-eps-datastore-archive` etc. Always avoid uppercase and underscores. |
76 | | -- IAM roles/policies: include environment, service, purpose; keep consistent with existing examples (`eps-storage-${local.environment}-terraform-plan`). |
77 | | - |
78 | | -## Tagging |
79 | | -- Use provider `default_tags` except where AWS requires inline tags (e.g., DynamoDB `tags` or KMS policy-derived resources). Inline tags MUST include a `Name` following existing naming conventions. |
80 | | -- Do not add ad-hoc tag keys. Changes to tagging require team approval. |
81 | | - |
82 | | -## IAM Policies |
83 | | -- Prefer `data "aws_iam_policy_document"` to build JSON, then `aws_iam_policy`. |
84 | | -- Allowed wildcard: inside `Resource` when resource ARNs vary or for already broadly permitted deployment role (`codebuild_apply`). Don't extend wildcard scope in new policies. |
85 | | -- When referencing DynamoDB stream or table ARNs lists: use for-expressions as in `iam.tf`. |
86 | | - |
87 | | -## KMS Policies |
88 | | -- Keep the minimal root policy block pattern used in `kms.tf`; don't invent complex grants unless required. |
89 | | -- Reuse `aws_account_id` from locals; never hardcode account IDs. |
90 | | - |
91 | | -## DynamoDB Tables |
92 | | -- Table names come from `var.ddb_table_names`; don't hardcode names. |
93 | | -- Support both PAY_PER_REQUEST and PROVISIONED with the existing capacity structure. |
94 | | -- Enable encryption: always specify `server_side_encryption { enabled = true kms_key_arn = <key>.arn }`. |
95 | | -- Conditional features (TTL, PITR, streams) are controlled by variables; replicate this approach for new features. |
96 | | - |
97 | | -## Autoscaling |
98 | | -- Use `for_each` with `toset(var.ddb_table_names)` or derived maps; replicate naming pattern for policies. |
99 | | -- Target tracking metric type should match read/write capacity utilization; keep target at `70` unless data-driven change requested. |
100 | | - |
101 | | -## Event Source Mappings |
102 | | -- Derive lambda ARN via locals; avoid duplicating account ID retrieval. |
103 | | -- Set batching window conditional by environment (`dev` = 0 else 60). |
104 | | -- Use filter criteria with jsonencode pattern like existing `event_source.tf`. |
105 | | - |
106 | | -## S3 Buckets |
107 | | -- Must set: versioning, encryption (KMS), public access block, lifecycle only for non-production (count trick `local.production ? 0 : 1`). |
108 | | -- Enforce SSL with a bucket policy using the `AllowSSLRequestsOnly` statement pattern. |
109 | | - |
110 | | -## CodeBuild / CodePipeline |
111 | | -- Environment variable injection should use interpolation of pipeline variables like shown (`#{variables.TerraformStack}`). |
112 | | -- Buildspec paths live under `terraform/deployment/scripts/`. |
113 | | -- Log groups names follow `/eps-storage-${local.environment}-codebuild/<purpose>`. |
114 | | -- Use `execution_mode = "PARALLEL"` and include dynamic Approval stage only for non-dev envs. |
115 | | - |
116 | | -## Module Usage |
117 | | -- If adding a reusable construct, place it under `terraform/modules/<module-name>/` and accept inputs rather than hardcoding environment. Provide example usage in a stack. |
118 | | -- Keep modules small, single responsibility (e.g., backup-destination vs backup-source separation). |
119 | | - |
120 | | -## Workspaces & Environments |
121 | | -- Never assume workspace names beyond matching `live*` for production detection. |
122 | | -- Place per-env config in `env/*.tfvars.json` files; do not create new env-specific `.tf` logic. |
123 | | - |
124 | | -## Formatting & Style |
125 | | -- Indent with two spaces. |
126 | | -- Keep attribute ordering: identifiers (name/bucket/etc) first, then settings, then nested blocks, then `tags` last. |
127 | | -- Use snake_case for variable names; hyphen-separated for resource names. |
128 | | -- Use `jsonencode({...})` for inline JSON to prevent quoting mistakes. |
129 | | - |
130 | | -## tfsec & Security Exceptions |
131 | | -- Annotate intentional rule suppressions with inline comments directly above the resource (`# tfsec:ignore:<rule-id>`). |
132 | | -- Don't add new ignores without reason; if required, add short justification after the rule id. |
133 | | - |
134 | | -## Avoid |
135 | | -- Randomness (`random_id`, etc.) unless collision avoidance is critical and approved. |
136 | | -- Hardcoding account IDs, environment names, or ARNs (derive via data sources / locals). |
137 | | -- Unbounded resource growth (autoscaling caps required via variables). |
138 | | - |
139 | | -## When Copilot Generates Code |
140 | | -1. Include the standard locals and provider blocks for new stack folders. |
141 | | -2. Reuse existing variable patterns; if unsure, look at the closest analogous file. |
142 | | -3. Prefer `for_each` over `count` when keys are meaningful (use `count` only for conditional single resources). |
143 | | -4. Suggest feature toggles (bool vars) for optional capabilities. |
144 | | -5. Ensure encryption and logging defaults are present. |
145 | | - |
146 | | -## Review Checklist (Copilot Internal) |
147 | | -Before finalizing a completion: |
148 | | -- Are all new resources tagged? (Either via default_tags or inline Name) |
149 | | -- Are environment/account references dynamic? |
150 | | -- Are optional features behind variables? |
151 | | -- Are IAM/KMS policies least-privilege following patterns? |
152 | | -- Is formatting consistent (2 spaces, block ordering)? |
153 | | - |
154 | | -## Example Minimal New Stack Skeleton |
| 93 | + |
| 94 | +### Bad Example - Hardcoding Values |
| 95 | + |
155 | 96 | ```hcl |
156 | | -# terraform/newstack/locals.tf |
157 | | -locals { /* standard locals as above */ } |
| 97 | +resource "aws_s3_bucket" "archive" { |
| 98 | + bucket = "my-hardcoded-bucket-name" |
| 99 | + ... |
| 100 | +} |
| 101 | +``` |
158 | 102 |
|
159 | | -data "aws_caller_identity" "current" {} |
| 103 | +## Security |
160 | 104 |
|
161 | | -# terraform/newstack/provider.tf |
162 | | -terraform { /* standard required_providers and required_version */ } |
163 | | -provider "aws" { /* region + allowed_account_ids + default_tags */ } |
164 | | -terraform { backend "s3" {} } |
| 105 | +- Never commit secrets or credentials to version control. |
| 106 | +- Use IAM roles and policies with least privilege. |
| 107 | +- Enable encryption for all supported resources (e.g., S3, KMS, DynamoDB). |
| 108 | +- Use secure remote state backends (e.g., S3 with encryption and locking). |
| 109 | +- Validate input variables for expected values and types. |
165 | 110 |
|
166 | | -# terraform/newstack/variables.tf |
167 | | -variable "allowed_account_ids" { type = list(string) } |
168 | | -``` |
| 111 | +## Performance |
169 | 112 |
|
170 | | -## Extending This Guide |
171 | | -Raise a PR to modify this file for any new patterns. Don't drift without updating instructions. Keep the frontmatter `languages` array if adding multi-language guidance. |
| 113 | +- Use resource lifecycle rules to manage retention and cleanup. |
| 114 | +- Use data sources to avoid duplicating resources. |
| 115 | +- Minimize resource drift by keeping code and infrastructure in sync. |
| 116 | +- Use `terraform plan` to preview changes and avoid unnecessary updates. |
172 | 117 |
|
173 | | ---- |
| 118 | +## Testing |
| 119 | + |
| 120 | +- Use `terraform validate` to check syntax and configuration. |
| 121 | +- Use `terraform plan` to preview changes before applying. |
| 122 | +- Use `tfsec` for static security analysis (`tfsec.yml` config). |
| 123 | +- Use automated CI/CD pipelines for deployment and testing. |
| 124 | + |
| 125 | +## Validation and Verification |
| 126 | + |
| 127 | +- Format code: `terraform fmt` (run in each Terraform folder) |
| 128 | +- Validate code: `terraform validate` |
| 129 | +- Security scan: `tfsec .` |
| 130 | +- Plan changes: `terraform plan -var-file=env/dev.tfvars.json` |
| 131 | +- Apply changes: `terraform apply -var-file=env/dev.tfvars.json` |
| 132 | + |
| 133 | +## Maintenance |
| 134 | + |
| 135 | +- Review and update modules and dependencies regularly. |
| 136 | +- Remove unused resources and variables. |
| 137 | +- Update environment files as infrastructure evolves. |
| 138 | +- Keep documentation up to date. |
| 139 | +- Refactor code to improve readability and maintainability. |
| 140 | + |
| 141 | +## Additional Resources |
| 142 | + |
| 143 | +- [Terraform Documentation](https://www.terraform.io/docs) |
| 144 | +- [Terraform AWS Provider](https://registry.terraform.io/providers/hashicorp/aws/latest/docs) |
| 145 | +- [tfsec Security Scanner](https://tfsec.dev/) |
0 commit comments