Skip to content

Commit 251f215

Browse files
authored
🤖 chore: move terraform state to S3 with lockfile locking (#32)
## Summary Move the `terraform/` root module from local state to **AWS S3 remote state** with native lockfile-based locking (no DynamoDB). Bootstrap the S3 bucket, import all 25 existing AWS resources into the new remote state, and align the Terraform config with the deployed infrastructure. ## Background The `terraform/` directory provisions a sandbox EKS cluster in `eu-central-1`. It was using the default local backend, meaning state lived wherever `terraform apply` was last run. This is fragile for team use and risks state loss. ## Implementation ### S3 backend configuration - `terraform/versions.tf`: Added `backend "s3"` block with `use_lockfile = true`, `encrypt = true`, region `eu-central-1`. Bucket/key are supplied at init time via `-backend-config` (not hardcoded). - Bumped `required_version` from `>= 1.5` to `>= 1.11` (minimum for S3 lockfile support). ### S3 bucket bootstrap (out-of-band) - Created bucket `coder-k8s-tfstate-112158171837` in `eu-central-1` with versioning, default AES256 encryption, and all public access blocked. ### Resource import - Discovered all 25 existing AWS resources (VPC, subnets, IGW, NAT GW, EIP, route tables, RT associations, IAM roles, policy attachments, EKS cluster, node group, 3 addons). - Used `import {}` blocks to import everything in a single `terraform apply`. - `terraform plan` is a clean no-op after import. ### Config drift fixes - `terraform/vpc.tf`: Replaced `cidrsubnets()` (which generated `/20` blocks) with `cidrsubnet()` per-subnet (generating `/24` blocks matching deployed infra). CIDRs are still derived from `var.vpc_cidr`. - `terraform/eks.tf`: Added `bootstrap_self_managed_addons = false` to match the deployed cluster. ### Repo housekeeping - `.gitignore`: Added standard Terraform ignores (state files, `.terraform/`, crash logs, plan files, override files, `terraform/backend.hcl`). Does **not** ignore `.terraform.lock.hcl`. - `flake.nix`: Added `terraform` to the Nix dev shell, with `allowUnfreePredicate` scoped to just `terraform` (BSL 1.1 license). ### Documentation - `terraform/README.md`: Updated to reflect S3 remote state, Terraform `>= 1.11`, added "Remote state backend" section with bucket bootstrap, IAM, auth, and migration docs. Updated Usage section with `-backend-config` workflow. ## Validation - `terraform plan` → "No changes. Infrastructure matches the configuration." - State file confirmed in S3 (50KB, AES256 encrypted) - `make build` ✅ - `make test` ✅ - `make lint` ✅ - `nix-instantiate --parse flake.nix` ✅ - `terraform fmt -check` ✅ ## Risks - **Low**: The `cidrsubnet()` change produces identical CIDRs to deployed infra, confirmed by clean plan. If `var.vpc_cidr` were ever changed, subnet CIDRs would shift — but that's a VPC rebuild regardless. - **Low**: BSL license allowance in flake is scoped to `terraform` only. --- <details> <summary>📋 Implementation Plan</summary> # Plan: Move `terraform/` state to AWS S3 (S3 lockfile, no DynamoDB) ## Context / Why We have a Terraform root module under `terraform/` that provisions a sandbox EKS cluster in `eu-central-1`. Today it uses the default **local** backend (state lives on whoever ran `terraform apply`). Goal: - Store Terraform state in **AWS S3** (in `eu-central-1`). - Use **S3-native state locking** (Terraform’s `use_lockfile`), **no DynamoDB**. - Add repo housekeeping so state/working dirs don’t get committed. - Ensure the Nix flake dev shell includes the Terraform CLI. - After the S3 bucket exists, **discover any already-existing AWS resources** for this stack and **import them into the new remote state**, so we don’t “orphan” resources. ## Evidence (what I checked) - `terraform/versions.tf`: no backend configured; `required_version = ">= 1.5"`. - `terraform/*.tf`: root module resources are all plain `aws_*` resources (no external modules), so imports are addressable directly (e.g., `aws_vpc.this`, `aws_eks_cluster.this`). - `.gitignore`: currently **no Terraform ignores**. - `flake.nix`: dev shell includes `awscli2` but **does not** include `terraform`. - Terraform docs (v1.11.0) for S3 backend show: - Locking via `use_lockfile = true`. - DynamoDB locking is deprecated and will be removed in a future minor version. - Lock file uses `<key>.tflock` in the same bucket/prefix. - AWS S3 docs indicate strong read-after-write consistency and atomic updates per key, but **S3 itself does not provide a built-in “object locking for concurrent writers” mechanism**, so Terraform’s lockfile is still required. --- ## Implementation Plan ### 1) Repo changes (backend config, docs, housekeeping, Nix) #### 1.1. Update Terraform version + S3 backend config **File:** `terraform/versions.tf` - Bump Terraform minimum version so the repo’s documented expectations match S3 lockfile support. - Recommend: `required_version = ">= 1.11"` (lockfile is no longer described as experimental; DynamoDB locking is deprecated). - Add an S3 backend block with **lockfile enabled** and **encryption enabled**. There are two workable patterns; pick one: **Option A (recommended): keep bucket/key out of git; commit only the backend type + safety settings** - Commit: - backend type `s3` - `use_lockfile = true` - `encrypt = true` - `region = "eu-central-1"` - Provide `bucket` + `key` via `-backend-config` at init time (see §2). ```hcl terraform { required_version = ">= 1.11" backend "s3" { region = "eu-central-1" encrypt = true use_lockfile = true # bucket/key supplied via -backend-config to avoid hardcoding a globally-unique name } required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } ``` **Option B: commit the full backend config (bucket + key) into the repo** - Only do this if there is exactly one shared AWS account/environment for this Terraform root module. - Still set `use_lockfile = true` and `encrypt = true`. > Plan assumes **Option A** to avoid bikeshedding the globally unique bucket name in git. #### 1.2. Document remote state + locking + bootstrap steps **File:** `terraform/README.md` Update: - Prereqs: Terraform `>= 1.11`. - Replace “Local Terraform state” note with “Remote state in S3”. - Add a “Remote state backend (S3 + lockfile)” section including: - Bucket bootstrap commands (create bucket, enable versioning, block public access, enable default encryption). - The backend config approach (Option A) and the `terraform init` command to supply bucket/key. - AWS auth note: the **S3 backend authenticates separately** from the AWS provider. If you’re using `TF_VAR_aws_profile`, also set `AWS_PROFILE` (or set `profile = ...` in the backend config file) so `terraform init` can access the S3 bucket. - Explain lockfile behavior: lock stored at `<key>.tflock`; why `s3:DeleteObject` is needed for the lock object. #### 1.3. Housekeeping: ignore local Terraform artifacts (but commit dependency lock file) **File:** `.gitignore` Add standard Terraform ignores: - State files: - `*.tfstate` - `*.tfstate.*` - Working directories: - `.terraform/` - Crash logs: - `crash.log` - `crash.*.log` - Plan files: - `*.tfplan` - `*.tfplan.*` - Override files: - `override.tf`, `override.tf.json`, `*_override.tf`, `*_override.tf.json` - CLI config files: - `.terraformrc`, `terraform.rc` - Local backend config (for `terraform init -backend-config=...`; do not commit): - `terraform/backend.hcl` Important nuance: - **Do not** ignore `.terraform.lock.hcl` (provider dependency lock). Best practice is to commit it. Suggested block: ```gitignore # Terraform *.tfstate *.tfstate.* .terraform/ crash.log crash.*.log *.tfplan *.tfplan.* override.tf override.tf.json *_override.tf *_override.tf.json .terraformrc terraform.rc # Backend config (local-only) terraform/backend.hcl ``` #### 1.4. Add Terraform CLI to the Nix flake dev shell **File:** `flake.nix` Add `terraform` to `devShells.default.packages` near `awscli2`: ```nix # Cloud tooling awscli2 terraform ``` Follow-up (verification step): - Run `nix develop` and confirm `terraform version` is `>= 1.11`. If nixpkgs’ `terraform` is older for any reason, switch to a versioned package (e.g., `terraform_1` / `terraform_1_11` depending on nixpkgs naming). --- ### 2) Bootstrap the S3 backend bucket (eu-central-1) Terraform can’t create its own backend bucket (chicken-and-egg), so create it out-of-band. #### 2.1 Choose names - **Bucket name**: must be globally unique. Recommended pattern: `coder-k8s-tfstate-<account-id>` or `coder-k8s-tfstate-<org>-<env>`. - **State key**: keep it stable and descriptive, e.g. `terraform-ncp3/sandbox-eks/terraform.tfstate`. #### 2.2 Create + harden the bucket Example AWS CLI (adjust bucket name): ```bash REGION=eu-central-1 BUCKET=<globally-unique-bucket> aws s3api create-bucket \ --bucket "$BUCKET" \ --region "$REGION" \ --create-bucket-configuration LocationConstraint="$REGION" # Strongly recommended for state recovery aws s3api put-bucket-versioning \ --bucket "$BUCKET" \ --versioning-configuration Status=Enabled # Block all public access aws s3api put-public-access-block \ --bucket "$BUCKET" \ --public-access-block-configuration \ BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true # Default encryption (SSE-S3). If you prefer SSE-KMS, use KMS instead. aws s3api put-bucket-encryption \ --bucket "$BUCKET" \ --server-side-encryption-configuration '{ "Rules": [{ "ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"} }] }' ``` Optional but recommended hardening: - Bucket policy enforcing TLS (`aws:SecureTransport`) and encryption headers. - Object Ownership = Bucket owner enforced (disables ACLs). #### 2.3 IAM permissions for Terraform backend access Whoever runs Terraform needs: - Read/write the state object: `s3:GetObject`, `s3:PutObject`. - Read/write/delete the lock object `<key>.tflock`: `s3:GetObject`, `s3:PutObject`, `s3:DeleteObject`. - List the bucket (ideally prefix-restricted): `s3:ListBucket`. Add/update an IAM policy (illustrative; scope down to your bucket + prefix): ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::<bucket>", "Condition": { "StringLike": { "s3:prefix": [ "terraform-ncp3/sandbox-eks/*" ] } } }, { "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject"], "Resource": "arn:aws:s3:::<bucket>/terraform-ncp3/sandbox-eks/terraform.tfstate" }, { "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"], "Resource": "arn:aws:s3:::<bucket>/terraform-ncp3/sandbox-eks/terraform.tfstate.tflock" } ] } ``` --- ### 3) Initialize the backend (and migrate state if it exists) **Directory:** `terraform/` #### 3.1 Create a backend config file (recommended for Option A) Create `terraform/backend.hcl` (do not commit): ```hcl bucket = "<bucket>" key = "terraform-ncp3/sandbox-eks/terraform.tfstate" # Optional: if you use AWS shared config profiles profile = "<profile>" # e.g. terraform ``` Then initialize (from within `terraform/`): ```bash terraform init -reconfigure -backend-config=backend.hcl ``` > If you rely on `TF_VAR_aws_profile` for the provider, remember the backend won’t read that; set `AWS_PROFILE` too (or set `profile = ...` above). After the first successful init: - Verify `terraform/.terraform.lock.hcl` exists and commit it (it pins provider selections for reproducible runs). #### 3.2 If an existing local state file exists, migrate it instead of importing If someone already has the correct local state file from previous applies, the safest path is to **migrate** it: ```bash terraform init -reconfigure -migrate-state -backend-config=backend.hcl ``` This preserves all resource addresses and avoids manual `terraform import` for most cases. --- ### 4) Final step: discover existing resources and import into the new remote state This section applies if: - The old local `.tfstate` is missing / not trustworthy, **or** - You want to double-check that the remote state fully captures what already exists. #### 4.1 Guardrails before importing - **Do not run `terraform apply`** until imports are complete and `terraform plan` looks sane. - Ensure the Terraform variables match what was used to create the infra: - `aws_region` should be `eu-central-1` - `cluster_name` default is `sandbox-eks` - Node group name is `${cluster_name}-nodes` (default `sandbox-eks-nodes`) - Subnet resources use `count` and depend on the first two AZ names returned by AWS. Import indices must match the AZ suffix in the subnet `Name` tag. #### 4.2 Discover IDs in AWS (examples) Use tags from the config (notably `Name = "${var.cluster_name}-..."`) to locate resource IDs: <details> <summary>🔎 Example AWS CLI discovery commands (eu-central-1)</summary> ```bash REGION=eu-central-1 CLUSTER=sandbox-eks # VPC aws ec2 describe-vpcs --region "$REGION" \ --filters "Name=tag:Name,Values=${CLUSTER}-vpc" \ --query 'Vpcs[0].VpcId' --output text # Internet Gateway aws ec2 describe-internet-gateways --region "$REGION" \ --filters "Name=tag:Name,Values=${CLUSTER}-igw" \ --query 'InternetGateways[0].InternetGatewayId' --output text # Subnets aws ec2 describe-subnets --region "$REGION" \ --filters "Name=tag:Name,Values=${CLUSTER}-public-*" \ --query 'Subnets[].{id:SubnetId,az:AvailabilityZone,name:Tags[?Key==`Name`]|[0].Value}' --output table aws ec2 describe-subnets --region "$REGION" \ --filters "Name=tag:Name,Values=${CLUSTER}-private-*" \ --query 'Subnets[].{id:SubnetId,az:AvailabilityZone,name:Tags[?Key==`Name`]|[0].Value}' --output table # EIP allocation id (tagged) aws ec2 describe-addresses --region "$REGION" \ --filters "Name=tag:Name,Values=${CLUSTER}-nat-eip" \ --query 'Addresses[0].AllocationId' --output text # NAT gateway aws ec2 describe-nat-gateways --region "$REGION" \ --filter "Name=tag:Name,Values=${CLUSTER}-nat" \ --query 'NatGateways[0].NatGatewayId' --output text # Route tables aws ec2 describe-route-tables --region "$REGION" \ --filters "Name=tag:Name,Values=${CLUSTER}-public-rt" \ --query 'RouteTables[0].RouteTableId' --output text aws ec2 describe-route-tables --region "$REGION" \ --filters "Name=tag:Name,Values=${CLUSTER}-private-rt" \ --query 'RouteTables[0].RouteTableId' --output text # EKS aws eks describe-cluster --region "$REGION" --name "$CLUSTER" \ --query 'cluster.name' --output text aws eks list-nodegroups --region "$REGION" --cluster-name "$CLUSTER" aws eks list-addons --region "$REGION" --cluster-name "$CLUSTER" # IAM roles aws iam get-role --role-name "${CLUSTER}-cluster-role" --query 'Role.RoleName' --output text aws iam get-role --role-name "${CLUSTER}-node-role" --query 'Role.RoleName' --output text ``` </details> #### 4.3 Import commands (addresses + ID formats) Import order is mostly for human sanity; Terraform doesn’t always require strict ordering, but this tends to minimize confusing errors. <details> <summary>📥 Import checklist for this repo’s current resources</summary> > Replace IDs with the discovered values. **Networking (`terraform/vpc.tf`)** ```bash terraform import aws_vpc.this vpc-xxxxxxxx terraform import aws_internet_gateway.this igw-xxxxxxxx # Public subnets (map [0]/[1] to the AZ suffix in the Name tag) terraform import 'aws_subnet.public[0]' subnet-aaaaaaaa terraform import 'aws_subnet.public[1]' subnet-bbbbbbbb # Private subnets terraform import 'aws_subnet.private[0]' subnet-cccccccc terraform import 'aws_subnet.private[1]' subnet-dddddddd terraform import aws_eip.nat eipalloc-xxxxxxxx terraform import aws_nat_gateway.this nat-xxxxxxxx terraform import aws_route_table.public rtb-xxxxxxxx terraform import aws_route_table.private rtb-yyyyyyyy # Route table associations use: <subnet-id>/<route-table-id> terraform import 'aws_route_table_association.public[0]' subnet-aaaaaaaa/rtb-xxxxxxxx terraform import 'aws_route_table_association.public[1]' subnet-bbbbbbbb/rtb-xxxxxxxx terraform import 'aws_route_table_association.private[0]' subnet-cccccccc/rtb-yyyyyyyy terraform import 'aws_route_table_association.private[1]' subnet-dddddddd/rtb-yyyyyyyy ``` **IAM (`terraform/iam.tf`)** ```bash terraform import aws_iam_role.eks_cluster sandbox-eks-cluster-role terraform import aws_iam_role.node_group sandbox-eks-node-role terraform import aws_iam_role_policy_attachment.eks_cluster_policy \ 'sandbox-eks-cluster-role/arn:aws:iam::aws:policy/AmazonEKSClusterPolicy' terraform import aws_iam_role_policy_attachment.node_group_worker_policy \ 'sandbox-eks-node-role/arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy' terraform import aws_iam_role_policy_attachment.node_group_cni_policy \ 'sandbox-eks-node-role/arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy' terraform import aws_iam_role_policy_attachment.node_group_ecr_readonly \ 'sandbox-eks-node-role/arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly' ``` **EKS (`terraform/eks.tf`)** ```bash terraform import aws_eks_cluster.this sandbox-eks # Node group import id format: <cluster-name>:<node-group-name> terraform import aws_eks_node_group.this sandbox-eks:sandbox-eks-nodes # Addons import id format: <cluster-name>:<addon-name> terraform import aws_eks_addon.coredns sandbox-eks:coredns terraform import aws_eks_addon.kube_proxy sandbox-eks:kube-proxy terraform import aws_eks_addon.vpc_cni sandbox-eks:vpc-cni ``` </details> #### 4.4 Verify state matches reality After imports: - `terraform plan` should ideally be a no-op. - If there are diffs, decide whether: - The config should change to match reality, or - Reality should be changed via `terraform apply`. Common import-related issues to watch for: - Subnet indices swapped (AZ order mismatch) → Terraform will want to recreate subnets. - EKS add-on settings drifted (Terraform will propose updates). --- ### 5) Validation / acceptance criteria - `terraform init` succeeds with the S3 backend and `use_lockfile = true`. - State file exists in S3 at the expected key, and a `.tflock` object appears briefly during a plan/apply. - `.gitignore` prevents state and `.terraform/` from being committed (including `terraform/backend.hcl`). - `terraform/.terraform.lock.hcl` is committed. - `nix develop` provides `terraform` and its version is `>= 1.11`. - After import/migration, `terraform plan` does not propose recreating the existing cluster/VPC (only expected drift). </details> --- _Generated with `mux` • Model: `anthropic:claude-opus-4-6` • Thinking: `xhigh` • Cost: `$4.83`_ <!-- mux-attribution: model=anthropic:claude-opus-4-6 thinking=xhigh costs=4.83 -->
1 parent 3771f49 commit 251f215

7 files changed

Lines changed: 120 additions & 10 deletions

File tree

.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,21 @@
1616
# MkDocs
1717
/site/
1818
/.cache/
19+
20+
# Terraform
21+
*.tfstate
22+
*.tfstate.*
23+
.terraform/
24+
crash.log
25+
crash.*.log
26+
*.tfplan
27+
*.tfplan.*
28+
override.tf
29+
override.tf.json
30+
*_override.tf
31+
*_override.tf.json
32+
.terraformrc
33+
terraform.rc
34+
35+
# Backend config (local-only, not committed)
36+
terraform/backend.hcl

flake.nix

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
in {
1616
devShells = forAllSystems (system:
1717
let
18-
pkgs = import nixpkgs { inherit system; };
18+
pkgs = import nixpkgs {
19+
inherit system;
20+
config.allowUnfreePredicate = pkg:
21+
builtins.elem (nixpkgs.lib.getName pkg) [ "terraform" ];
22+
};
1923
docsPython = pkgs.python3.withPackages (ps: [
2024
ps.mkdocs
2125
ps."mkdocs-material"
@@ -35,6 +39,7 @@
3539

3640
# Cloud tooling
3741
awscli2
42+
terraform
3843

3944
goreleaser
4045
actionlint

terraform/README.md

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ This directory provisions a cost-optimized Amazon EKS sandbox cluster in region
1717
- Disk size: `20 GiB`
1818
- AMI type: `AL2023_x86_64_STANDARD`
1919
- EKS managed add-ons: `coredns`, `kube-proxy`, `vpc-cni`
20-
- Local Terraform state (no remote backend configured)
20+
- Remote Terraform state in AWS S3 with lockfile-based locking (no DynamoDB)
2121

2222
## Prerequisites
2323

24-
- Terraform `>= 1.5`
24+
- Terraform `>= 1.11`
2525
- AWS CLI v2 installed
2626
- AWS identity with permissions to create VPC, IAM, EKS, and EC2 resources in your target account
2727

@@ -71,10 +71,78 @@ If you prefer not to use `TF_VAR_aws_profile`, pass `-var="aws_profile=<your-ter
7171

7272
> Security note: do not commit `~/.aws/config`, `~/.aws/credentials`, or any copied credential values to git.
7373
74+
## Remote state backend (S3 + lockfile)
75+
76+
Terraform state is stored in an S3 bucket in `eu-central-1`. Locking uses Terraform's native S3 lockfile (`use_lockfile = true` in the backend block), so no DynamoDB lock table is required. The backend configuration committed in this repo only includes shared settings (`region`, `encrypt`, and `use_lockfile`); the bucket name and state key are supplied at init time via `-backend-config`.
77+
78+
### Bootstrap the S3 bucket (one-time)
79+
80+
Terraform cannot create its own backend bucket before backend initialization, so create and secure the bucket once with AWS CLI:
81+
82+
```bash
83+
REGION=eu-central-1
84+
BUCKET=<globally-unique-bucket-name>
85+
86+
# Create bucket
87+
aws s3api create-bucket \
88+
--bucket "$BUCKET" \
89+
--region "$REGION" \
90+
--create-bucket-configuration LocationConstraint="$REGION"
91+
92+
# Enable versioning (for state recovery)
93+
aws s3api put-bucket-versioning \
94+
--bucket "$BUCKET" \
95+
--versioning-configuration Status=Enabled
96+
97+
# Block all public access
98+
aws s3api put-public-access-block \
99+
--bucket "$BUCKET" \
100+
--public-access-block-configuration \
101+
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
102+
103+
# Default encryption (SSE-S3)
104+
aws s3api put-bucket-encryption \
105+
--bucket "$BUCKET" \
106+
--server-side-encryption-configuration '{
107+
"Rules": [{
108+
"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}
109+
}]
110+
}'
111+
```
112+
113+
### Required IAM permissions
114+
115+
The identity used for Terraform backend access needs S3 permissions for `s3:GetObject`, `s3:PutObject`, `s3:DeleteObject` (lockfile lifecycle), and `s3:ListBucket` on the state bucket path.
116+
117+
### Authentication note
118+
119+
The S3 backend authenticates separately from the AWS provider configuration. If you use `TF_VAR_aws_profile` for provider auth, also set `AWS_PROFILE` or add `profile` to `backend.hcl` so `terraform init` can authenticate to S3.
120+
121+
### Migrate existing local state
122+
123+
If you already have a local `.tfstate` file, migrate it into S3 during init:
124+
125+
```bash
126+
terraform init -migrate-state -backend-config=backend.hcl
127+
```
128+
74129
## Usage
75130

131+
Create a local backend config file `backend.hcl` in this directory (it is
132+
git-ignored):
133+
134+
```hcl
135+
bucket = "<your-tfstate-bucket>"
136+
key = "terraform-ncp3/sandbox-eks/terraform.tfstate"
137+
138+
# Optional: set if you use an AWS CLI profile for backend access
139+
# profile = "terraform"
140+
```
141+
142+
Then initialize and apply:
143+
76144
```bash
77-
terraform init
145+
terraform init -backend-config=backend.hcl
78146
terraform plan
79147
terraform apply
80148
```

terraform/eks.tf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ resource "aws_eks_cluster" "this" {
33
role_arn = aws_iam_role.eks_cluster.arn
44
version = var.cluster_version
55

6+
bootstrap_self_managed_addons = false
7+
68
vpc_config {
79
subnet_ids = aws_subnet.private[*].id
810
endpoint_public_access = true

terraform/variables.tf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ variable "vpc_cidr" {
2626
description = "CIDR block for the VPC that hosts the EKS cluster."
2727
type = string
2828
default = "10.0.0.0/16"
29+
30+
validation {
31+
# Subnets use netnum up to 20, so we need at least 5 bits (2^5 = 32 > 20).
32+
# That means VPC prefix + 5 bits <= 24, i.e. prefix <= 19.
33+
condition = tonumber(split("/", var.vpc_cidr)[1]) <= 19
34+
error_message = "VPC CIDR prefix must be /19 or shorter so that /24 subnets with network numbers up to 20 can be allocated."
35+
}
2936
}
3037

3138
variable "node_instance_types" {

terraform/versions.tf

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
terraform {
2-
required_version = ">= 1.5"
2+
required_version = ">= 1.11"
3+
4+
backend "s3" {
5+
region = "eu-central-1"
6+
encrypt = true
7+
use_lockfile = true
8+
# bucket and key supplied via -backend-config at init time.
9+
}
310

411
required_providers {
512
aws = {

terraform/vpc.tf

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ locals {
1414

1515
azs = slice(data.aws_availability_zones.available.names, 0, local.az_count)
1616

17-
# Derive subnet ranges from the configurable VPC CIDR.
18-
subnet_cidrs = cidrsubnets(var.vpc_cidr, 4, 4, 4, 4)
19-
20-
public_subnet_cidrs = slice(local.subnet_cidrs, 0, local.az_count)
21-
private_subnet_cidrs = slice(local.subnet_cidrs, local.az_count, local.az_count * 2)
17+
# Derive /24 subnet ranges from the VPC CIDR.
18+
# Compute new-bits dynamically so this works for any VPC prefix, not just /16.
19+
subnet_newbits = 24 - tonumber(split("/", var.vpc_cidr)[1])
20+
21+
# Network numbers are intentionally spaced so public (1,2) and private (10,20)
22+
# blocks don't collide and are easy to identify at a glance.
23+
public_subnet_cidrs = [for i in [1, 2] : cidrsubnet(var.vpc_cidr, local.subnet_newbits, i)]
24+
private_subnet_cidrs = [for i in [10, 20] : cidrsubnet(var.vpc_cidr, local.subnet_newbits, i)]
2225
}
2326

2427
resource "aws_vpc" "this" {

0 commit comments

Comments
 (0)