From d0b2072596dd36b94e193a9045262a49d63f50bd Mon Sep 17 00:00:00 2001 From: John Clark Date: Fri, 24 Jul 2026 15:34:37 -0700 Subject: [PATCH 1/3] Various updates to modernize the Terraform stack. Details in the upgrade notes. --- .DS_Store | Bin 0 -> 6148 bytes UPGRADE_NOTES.md | 107 ++++++++++++++++++++ aws/eks.tf | 254 +++++++++++++---------------------------------- aws/iam.tf | 3 +- aws/main.tf | 78 +++++++++++---- aws/outputs.tf | 24 +++-- aws/rds.tf | 73 +++++++++++--- aws/variables.tf | 30 +++++- aws/vpc.tf | 9 +- 9 files changed, 342 insertions(+), 236 deletions(-) create mode 100644 .DS_Store create mode 100644 UPGRADE_NOTES.md diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ed3fdb6eba76adbd3ffbe4843a06fce9f744ab92 GIT binary patch literal 6148 zcmeHK-AcnS6i&A3GKSC#1-%P+J8*x9;!UaZ1+3_W%53S-Vr|B{*^4pgy}pny;`4Y; zl7hoti@0+j`Oa_Be9-(b#<;hLdW<=YF#!#cqf#O0t_;;|G9t$@vT2y85Uh`IYGZ#L z@Z0MwU^&ae{QLJOaW?OEKY6X**xGK2rf7?M|4HTkJeV&sFPPn;b){4imU<9g$CG^Q z?4GN19>nQnrVHX|0x5SlaT=-IQ;Rgpbggd!q9s~m=U}-U_6Hr=8=mx59l1R1cRO-8 zI9{z~j2^yrk;Q@X3L0Ejt!Vcn8HU=hdI3iAo>AQ)ZW0gv0n z!~iky&lup1kw5ZaQTA;8u{=C$CA0@)WENA`8l#3kyG=UOZb;wb3X Q<$!b%(1cJ&4EzEEUp1IXzW@LL literal 0 HcmV?d00001 diff --git a/UPGRADE_NOTES.md b/UPGRADE_NOTES.md new file mode 100644 index 0000000..ea16ceb --- /dev/null +++ b/UPGRADE_NOTES.md @@ -0,0 +1,107 @@ +# Modernization notes (branch: modernize-terraform-stack) + +This branch responds to a customer review that flagged this repo as badly out +of date. Summary of changes, by original comment: + +1. **RDS Postgres 15.10 (EOL May 2026)** - replaced by moving to Aurora + PostgreSQL (see #7); if you stay on plain RDS Postgres instead, bump + `engine_version` to a current supported release (17.x as of mid-2026 - + check https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL.Concepts.General.DBVersions.html). +2. **k8s 1.32 (standard support ended Mar 2026, extended support only)** - + `variable.cluster_version` default bumped to 1.35. This ages fast; check + https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html + before every deploy. +3. **AWS provider 4.67 -> 6.43+** - `main.tf` now requires `>= 6.43, < 7.0`. + This is a 2 major-version jump; review the v5 and v6 upgrade guides on the + registry for resource-level breaking changes beyond what's covered here. +4. **EKS module 18.31.0 -> ~> 21.0** - also a multi-major jump. v21 removed + aws-auth configmap management entirely, so this branch switches cluster + auth to EKS access entries (`authentication_mode = "API"`, + `access_entries` block in `eks.tf`). +5. **EKS Auto Mode** - enabled via `cluster_compute_config` in `eks.tf`. This + replaces the hand-rolled VPC CNI / EBS CSI / AWS Load Balancer Controller + IRSA roles and Helm release, and the `eks_managed_node_group*` config, all + of which have been deleted. `main.tf` no longer configures the + `kubernetes`/`helm` providers since nothing in this repo uses them anymore. + + **Post-merge finding:** `module.eks.node_security_group_id`, which + `eks.tf` previously used for the node-to-RDS security group rule, does + not correspond to the security group Auto Mode nodes actually use - Auto + Mode nodes attach to AWS's auto-created cluster security group instead. + This was silently broken (the rule attached to an unused SG, causing RDS + connection timeouts with no obvious error) until fixed by switching to + `module.eks.cluster_primary_security_group_id`. If you see connection + timeouts to RDS from cluster workloads after a fresh apply, check this + first. +6. **Write-only password + ephemeral Secrets Manager** - `rds.tf` generates + the master password with an `ephemeral "random_password"` resource, + stores it in Secrets Manager via `secret_string_wo`, and feeds it to the + database via `master_password_wo` / `master_password_wo_version`. The + plaintext password is never written to state or plan files, and the old + `rds_password` sensitive output (which held the cleartext value) has been + removed from `outputs.tf` in favor of `rds_password_secret_arn`. + Requires Terraform >= 1.11. +7. **Aurora PostgreSQL vs RDS Postgres** - `rds.tf` now provisions an + `aws_rds_cluster` (1 writer + `var.db_reader_count` readers) instead of a + single `aws_db_instance`. Aurora fails over to a reader in ~30s; + RDS Postgres Multi-AZ has no automatic failover between instances. + +## Manual steps required after `terraform apply` + +EKS Auto Mode provides the underlying capability for two things but does not +auto-configure them - both require a one-time manual `kubectl apply` against +the live cluster before deploying any PVC- or Ingress-using workload (e.g. +before installing Opal via KOTS): + +1. **Default StorageClass** - Auto Mode registers the EBS CSI driver but + creates no StorageClass pointing at it. Without one, PVC-backed + workloads (KOTS's own datastore, Opal's app) hang `Pending` indefinitely. +2. **IngressClass / IngressClassParams** - Auto Mode won't provision an ALB + for any Ingress until these exist; AWS's own docs confirm this isn't + automatic even under Auto Mode. + +Both are intentionally *not* managed in this Terraform: doing so requires +the `kubernetes` provider, which needs network access to the cluster's +private API endpoint (`endpoint_public_access = false`), meaning every +future `apply` - even ones unrelated to these two resources - would only +work from inside the VPC. See the comment block in `main.tf` for the exact +manifests and rationale. + +## Things that still need a human before you `terraform apply` + +- **This is not a safe in-place upgrade for an existing deployed cluster/DB.** + These are suggested changes for review, not a tested migration plan. +- **RDS -> Aurora is a data migration, not a version bump.** Terraform will + want to destroy the `aws_db_instance` and create a new `aws_rds_cluster`. + Plan an actual migration (snapshot restore into Aurora, or logical + dump/restore) and cut Opal over during a maintenance window. +- **VPC module bumped 3.2.0 -> ~> 6.0** (not one of the 7 items, but the old + version isn't validated against AWS provider 6.x and would likely break + planning). This also jumps several majors - diff the plan carefully. +- **EKS module 18 -> 21 + Auto Mode + access entries is three changes at + once** on the cluster's control plane and auth model. Consider sequencing + these as separate applies against a non-prod cluster first. +- Version numbers here (provider/module versions, k8s version, Aurora + version) were current as of mid-2026 research for this branch - re-check + the registry/AWS docs for anything newer before applying. + +## Follow-ups (not urgent, tracked for later) + +- **Opal's setup doc is now out of date for the `kubectl get pods -A` step.** + It documents the old self-managed node group output (`aws-node`, + `ebs-csi-controller`, `ebs-csi-node`, `kube-proxy` DaemonSets across 3 + nodes). With Auto Mode enabled, none of those run as visible workloads - + Auto Mode manages networking and storage internally - so the real output + is just `coredns` pods (2 here) and nothing else. This is expected, not a + bug, but the doc should be updated so it doesn't look like a failed + deployment to the next person following it. +- **Opal's KOTS-packaged Ingress is incompatible with EKS Auto Mode.** Its + Helm chart configures the ALB via the legacy + `kubernetes.io/ingress.class: alb` annotation and + `alb.ingress.kubernetes.io/*` annotations - both silently ignored/ + unrecognized by Auto Mode, which requires `spec.ingressClassName` plus a + separate `IngressClassParams` resource for scheme/certificate config. + Workaround in use: disable Opal's "pre-packaged K8s ingress" in the KOTS + config and manage the Ingress independently (see runbook / ask John for + the manifest). Worth reporting to Opal as a packaging gap - likely + affects GKE Autopilot similarly. \ No newline at end of file diff --git a/aws/eks.tf b/aws/eks.tf index cec5d1f..3fa765d 100644 --- a/aws/eks.tf +++ b/aws/eks.tf @@ -1,198 +1,84 @@ -data "aws_eks_cluster" "cluster" { - name = module.eks.cluster_id -} - -data "aws_eks_cluster_auth" "cluster" { - name = module.eks.cluster_id -} - -# irsa - vpc cni -module "vpc_cni_irsa_role" { - source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" - - role_name = "${module.eks.cluster_name}-vpc-cni" - - attach_vpc_cni_policy = true - vpc_cni_enable_ipv4 = true - - oidc_providers = { - main = { - provider_arn = module.eks.oidc_provider_arn - namespace_service_accounts = ["kube-system:aws-node"] - } - } -} - -# irsa - csi ebs storage -module "ebs_csi_irsa_role" { - source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" - - role_name = "${module.eks.cluster_name}-ebs-csi" - attach_ebs_csi_policy = true - - oidc_providers = { - ex = { - provider_arn = module.eks.oidc_provider_arn - namespace_service_accounts = ["kube-system:ebs-csi-controller-sa"] - } - } -} - -# irsa - alb controller -module "alb_controller_irsa_role" { - source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" - - role_name = "${module.eks.cluster_name}-alb-controller" - attach_load_balancer_controller_policy = true - - oidc_providers = { - ex = { - provider_arn = module.eks.oidc_provider_arn - namespace_service_accounts = ["kube-system:aws-load-balancer-controller"] - } - } -} - module "eks" { - source = "terraform-aws-modules/eks/aws" - version = "18.31.0" - cluster_name = var.cluster_name - cluster_version = var.cluster_version + source = "terraform-aws-modules/eks/aws" + # was pinned to 18.31.0 (Nov 2022, missing even the 18.31.2 patch from + # 2 days later). Current major is 21.x. v21 also drops aws-auth configmap + # management in favor of EKS access entries (see access_entries below), + # and dropped the `cluster_` prefix from most input variable names to + # better match the underlying API - verify against the registry for the + # latest 21.x patch before applying. + version = "~> 21.0" + name = var.cluster_name + kubernetes_version = var.cluster_version #networking - subnet_ids = module.vpc.private_subnets - vpc_id = module.vpc.vpc_id - cluster_endpoint_public_access = false - cluster_endpoint_private_access = true + subnet_ids = module.vpc.private_subnets + vpc_id = module.vpc.vpc_id + endpoint_public_access = false + endpoint_private_access = true //enable logs and OIDC - cluster_enabled_log_types = ["audit", "api", "authenticator"] - enable_irsa = true - - # install the add-ons - cluster_addons = { - kube-proxy = { - resolve_conflicts = "OVERWRITE" - } - coredns = { - resolve_conflicts = "OVERWRITE" - } - vpc-cni = { - resolve_conflicts = "OVERWRITE" - service_account_role_arn = module.vpc_cni_irsa_role.iam_role_arn - } - aws-ebs-csi-driver = { - resolve_conflicts = "OVERWRITE" - service_account_role_arn = module.ebs_csi_irsa_role.iam_role_arn - } + enabled_log_types = ["audit", "api", "authenticator"] + enable_irsa = true + + # EKS Auto Mode: AWS manages compute (nodes), storage (replaces the EBS CSI + # driver), and load balancing (replaces the AWS Load Balancer Controller) + # for us. This removes the need for the vpc-cni / aws-ebs-csi-driver + # add-ons, the CNI/CSI/ALB-controller IRSA roles, the ALB controller Helm + # release, and the eks_managed_node_group* configuration that used to live + # in this file. + compute_config = { + enabled = true + node_pools = ["general-purpose"] } - # make worker nodes work with SSM - eks_managed_node_group_defaults = { - instance_types = [var.cluster_node_instance_type] - iam_role_additional_policies = [ - "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", - "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy" - ] - # We are using the IRSA created above for CNI permissions - # However, we have to provision a new cluster with the policy attached FIRST - # before we can disable. Without this initial policy, - # the VPC CNI fails to assign IPs and nodes cannot join the new cluster - # see https://github.com/aws/containers-roadmap/issues/1666 for more context - iam_role_attach_cni_policy = true - - # Creates persistent volumes - block_device_mappings = { - "xvda" = { - device_name = "/dev/xvda" + # coredns still runs as a regular add-on under Auto Mode. + addons = { + coredns = {} + } - ebs = { - volume_size = 60 - volume_type = "gp3" + # Auth: use EKS access entries instead of the (now removed) aws-auth + # configmap management. Grants the same cluster-admin access the + # aws_auth_roles block used to grant via "system:masters". + authentication_mode = "API" + + access_entries = { + admin = { + principal_arn = aws_iam_role.eks_cluster_admin.arn + + policy_associations = { + admin = { + policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy" + access_scope = { + type = "cluster" + } } } } } - - #allow node to node communication and rds access - node_security_group_additional_rules = { - ingress_allow_access_from_nodes = { - description = "Node to node access" - type = "ingress" - protocol = "-1" - from_port = 0 - to_port = 0 - self = true - } - egress_allow_access_to_nodes = { - description = "Node to node access" - type = "egress" - protocol = "-1" - from_port = 0 - to_port = 0 - self = true - } - ingress_allow_access_from_control_plane = { - description = "Cluster to node access" - protocol = "-1" - from_port = 0 - to_port = 0 - type = "ingress" - source_cluster_security_group = true - } - egress_allow_access_to_rds = { - description = "Node to RDS access" - type = "egress" - protocol = "tcp" - from_port = 5432 - to_port = 5432 - cidr_blocks = module.vpc.private_subnets_cidr_blocks - } - } - - # default to three worker nodes across AZs - two nodes is okay based on t-shirt sizing. - eks_managed_node_groups = { - worker = { - name = "opal-worker" - max_size = 3 - desired_size = 3 - } - } - - # show example auth config map - manage_aws_auth_configmap = true - aws_auth_roles = [ - { - rolearn = aws_iam_role.eks_cluster_admin.arn - username = aws_iam_role.eks_cluster_admin.name - groups = ["system:masters"] - } - ] } -#alb controller -resource "helm_release" "alb_controller" { - name = "aws-load-balancer-controller" - - repository = "https://aws.github.io/eks-charts" - chart = "aws-load-balancer-controller" - - namespace = "kube-system" - - set { - name = "clusterName" - value = module.eks.cluster_name - } - set { - name = "serviceAccount.create" - value = true - } - set { - name = "serviceAccount.name" - value = "aws-load-balancer-controller " - } - set { - name = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" - value = module.alb_controller_irsa_role.iam_role_arn - } -} +# Allow Aurora/RDS access from the nodes EKS Auto Mode provisions. +# +# Uses module.eks.cluster_primary_security_group_id - NOT +# node_security_group_id and NOT cluster_security_group_id, both of which +# look right but aren't: +# - node_security_group_id: under Auto Mode there's no separate per-node- +# group security group the way traditional eks_managed_node_groups work, +# so this points at a security group nothing actually uses. +# - cluster_security_group_id: this is an *additional* security group the +# module itself creates and attaches to the cluster - a different +# resource from the one AWS's EKS service auto-creates for the cluster. +# +# cluster_primary_security_group_id is the module's output for that +# AWS-auto-created security group - the one Auto Mode nodes actually use for +# control-plane-to-data-plane communication, and the one shown as "Cluster +# security group" in the EKS console / clusterSecurityGroupId via the API. +resource "aws_security_group_rule" "nodes_to_rds" { + description = "Node to RDS access" + type = "egress" + protocol = "tcp" + from_port = 5432 + to_port = 5432 + security_group_id = module.eks.cluster_primary_security_group_id + source_security_group_id = aws_security_group.rds.id +} \ No newline at end of file diff --git a/aws/iam.tf b/aws/iam.tf index 4028464..64fecf3 100644 --- a/aws/iam.tf +++ b/aws/iam.tf @@ -42,7 +42,8 @@ resource "aws_iam_policy" "eks_cluster_admin" { "arn:aws:eks:${var.region}:${data.aws_caller_identity.current.account_id}:addon/${module.eks.cluster_name}/*/*", "arn:aws:eks:${var.region}:${data.aws_caller_identity.current.account_id}:identityproviderconfig/${module.eks.cluster_name}/*/*/*", "arn:aws:eks:${var.region}:${data.aws_caller_identity.current.account_id}:cluster/${module.eks.cluster_name}", - "arn:aws:eks:${var.region}:${data.aws_caller_identity.current.account_id}:nodegroup/${module.eks.cluster_name}/*/*" + "arn:aws:eks:${var.region}:${data.aws_caller_identity.current.account_id}:nodegroup/${module.eks.cluster_name}/*/*", + "arn:aws:eks:${var.region}:${data.aws_caller_identity.current.account_id}:access-entry/${module.eks.cluster_name}/*/*/*" ] } ] diff --git a/aws/main.tf b/aws/main.tf index b62decf..5704bbd 100644 --- a/aws/main.tf +++ b/aws/main.tf @@ -1,16 +1,18 @@ terraform { + # >= 1.11 is required for ephemeral resources / write-only arguments + # (used in rds.tf to keep the DB master password out of state & plan output). + required_version = ">= 1.11.0" + required_providers { aws = { source = "hashicorp/aws" - version = "~> 4.67.0" - } - kubernetes = { - source = "hashicorp/kubernetes" - version = "~> 2.23.0" + # was pinned to 4.67.0 (last 4.x release, ~2yrs old). Current major is 6.x. + version = ">= 6.43, < 7.0" } - helm = { - source = "hashicorp/helm" - version = "~> 2.11.0" + random = { + source = "hashicorp/random" + # 3.7+ ships the `ephemeral "random_password"` resource used in rds.tf. + version = ">= 3.7.0, < 4.0" } } } @@ -19,16 +21,50 @@ provider "aws" { region = var.region } -provider "kubernetes" { - host = data.aws_eks_cluster.cluster.endpoint - token = data.aws_eks_cluster_auth.cluster.token - cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data) -} - -provider "helm" { - kubernetes { - host = data.aws_eks_cluster.cluster.endpoint - token = data.aws_eks_cluster_auth.cluster.token - cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data) - } -} +# NOTE: the `kubernetes` and `helm` providers previously configured here have +# been removed. They existed only to install the AWS Load Balancer Controller +# and EBS CSI/VPC CNI IRSA roles by hand. With EKS Auto Mode enabled (see +# eks.tf) those add-ons are managed natively by EKS, so nothing in this repo +# needs to talk to the Kubernetes API directly anymore. +# +# One deliberate exception: Auto Mode registers the EBS CSI driver +# (ebs.csi.eks.amazonaws.com) but does NOT create a default StorageClass +# pointing at it, so PVC-backed workloads (KOTS, Helm charts, etc.) will get +# stuck Pending until one exists. We tried managing that StorageClass here +# via the `kubernetes` provider, but this cluster's endpoint is private +# (endpoint_public_access = false), so the `kubernetes` provider's calls - +# unlike the `aws` provider's calls to public AWS APIs - must originate from +# inside the VPC. That makes `terraform apply` only runnable from the +# bastion (or a VPN'd-in host) the moment this provider is configured, even +# for applies that don't touch the StorageClass. Not worth that operational +# cost for one resource, so it's intentionally left out of Terraform. +# +# Instead, apply the default StorageClass manually, once, from the bastion +# right after `terraform apply` creates the cluster: +# +# cat < +output "rds_password_secret_arn" { + description = "ARN of the Secrets Manager secret holding the RDS master password" + value = aws_secretsmanager_secret.db_password.arn } diff --git a/aws/rds.tf b/aws/rds.tf index 64b3c9b..b7e3934 100644 --- a/aws/rds.tf +++ b/aws/rds.tf @@ -1,6 +1,28 @@ -resource "random_password" "password" { - length = 16 - special = false +# --- Master password: generated, stored in Secrets Manager, and wired into +# the cluster via write-only arguments so the plaintext password is never +# written to the Terraform state or plan file (previously it was a regular +# `random_password` resource stored in state and re-exposed via a sensitive +# output). +# +# Requires Terraform >= 1.11 and aws provider >= 6.43 (see main.tf). +ephemeral "random_password" "db_password" { + length = 20 + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +resource "aws_secretsmanager_secret" "db_password" { + name = "${var.db_identifier}-master-password" + recovery_window_in_days = 7 +} + +resource "aws_secretsmanager_secret_version" "db_password" { + secret_id = aws_secretsmanager_secret.db_password.id + secret_string_wo = ephemeral.random_password.db_password.result + secret_string_wo_version = 1 +} + +ephemeral "aws_secretsmanager_secret_version" "db_password" { + secret_id = aws_secretsmanager_secret_version.db_password.secret_id } resource "aws_security_group" "rds" { @@ -20,26 +42,47 @@ resource "aws_db_subnet_group" "opal" { subnet_ids = module.vpc.private_subnets } -resource "aws_db_instance" "opal" { - db_name = "opal" - identifier = var.db_identifier +# --- Aurora PostgreSQL, replacing the single-instance RDS Postgres setup. +# +# Aurora gives us storage that's replicated across 3 AZs and typically +# fails over to a reader in under 30s, versus RDS Postgres Multi-AZ which +# has no automatic failover between instances - a monitor has to detect the +# failure and promote a replica. This is a bigger change than a version +# bump: migrating an existing RDS Postgres instance to Aurora is a +# snapshot-restore or dump/restore exercise, not an in-place `terraform +# apply`. If you'd rather stay on plain RDS Postgres for now, keep +# `aws_db_instance` and just bump `engine_version` to a supported release +# (see UPGRADE_NOTES.md). +resource "aws_rds_cluster" "opal" { + cluster_identifier = var.db_identifier - engine = "postgres" - engine_version = "15.10" - allocated_storage = 50 - storage_type = "gp3" - instance_class = var.db_instance_class + engine = "aurora-postgresql" + engine_version = var.db_engine_version + database_name = "opal" + master_username = "postgres" - username = "postgres" - password = random_password.password.result + master_password_wo = ephemeral.aws_secretsmanager_secret_version.db_password.secret_string + master_password_wo_version = aws_secretsmanager_secret_version.db_password.secret_string_wo_version db_subnet_group_name = aws_db_subnet_group.opal.name vpc_security_group_ids = [aws_security_group.rds.id] - multi_az = true - publicly_accessible = false storage_encrypted = true backup_retention_period = 30 #not for prod - make sure your Opal snapshot is not deleted by accident skip_final_snapshot = true } + +# 1 writer + N readers, spread across AZs by the provider, giving Aurora a +# same-region failover target. +resource "aws_rds_cluster_instance" "opal" { + count = 1 + var.db_reader_count + + identifier = "${var.db_identifier}-${count.index}" + cluster_identifier = aws_rds_cluster.opal.id + instance_class = var.db_instance_class + engine = aws_rds_cluster.opal.engine + engine_version = aws_rds_cluster.opal.engine_version + + publicly_accessible = false +} diff --git a/aws/variables.tf b/aws/variables.tf index 2afdd51..33aa3d1 100644 --- a/aws/variables.tf +++ b/aws/variables.tf @@ -13,13 +13,21 @@ variable "cluster_name" { } variable "cluster_version" { - default = "1.32" - description = "EKS cluster version" + default = "1.35" + description = <<-EOT + EKS cluster version. 1.32 is out of standard support (ended Mar 2026). + EKS only guarantees ~4 versions of support at a time, so check + https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html + for the current recommended version before deploying. + EOT } +# Unused now that cluster_compute_config (EKS Auto Mode) manages node +# provisioning - see eks.tf. Left here in case you disable Auto Mode and +# go back to eks_managed_node_groups. variable "cluster_node_instance_type" { default = "m6i.xlarge" - description = "EKS cluster node instance type" + description = "EKS cluster node instance type (only used if EKS Auto Mode is disabled)" } variable "db_identifier" { @@ -27,7 +35,19 @@ variable "db_identifier" { description = "DB identifier" } +variable "db_engine_version" { + default = "17.9" + description = "Aurora PostgreSQL engine version. Was RDS Postgres 15.10, which hits end of standard support in May 2026." +} + variable "db_instance_class" { - default = "db.m6i.large" - description = "DB instance class" + # Aurora doesn't support the db.m* family used previously - r6g is the + # standard general-purpose choice for Aurora PostgreSQL. + default = "db.r6g.large" + description = "Aurora DB instance class (applies to both writer and reader instances)" +} + +variable "db_reader_count" { + default = 1 + description = "Number of Aurora reader instances to run alongside the writer, for failover" } diff --git a/aws/vpc.tf b/aws/vpc.tf index 6f729b0..88d7410 100644 --- a/aws/vpc.tf +++ b/aws/vpc.tf @@ -1,8 +1,13 @@ data "aws_availability_zones" "available" {} module "vpc" { - source = "terraform-aws-modules/vpc/aws" - version = "3.2.0" + source = "terraform-aws-modules/vpc/aws" + # was pinned to 3.2.0 (2021-era), which predates and is not validated + # against aws provider 6.x. Bumping alongside the provider upgrade since + # v3 will very likely fail plan/apply against provider >= 6. This module + # jumps 3 major versions (3 -> 6) - diff the plan carefully before + # applying, per the module's upgrade guides on the registry. + version = "~> 6.0" name = var.vpc_name cidr = "10.0.0.0/16" From 2ca5446c831fd4b8431522104749e36d7d0b7705 Mon Sep 17 00:00:00 2001 From: John Clark Date: Mon, 27 Jul 2026 19:23:01 -0700 Subject: [PATCH 2/3] Changes per feedback from engineering --- .DS_Store | Bin 6148 -> 8196 bytes UPGRADE_NOTES.md | 24 ++++++++++++++++++++++-- aws/iam.tf | 1 - aws/outputs.tf | 4 ++-- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.DS_Store b/.DS_Store index ed3fdb6eba76adbd3ffbe4843a06fce9f744ab92..ef8f042e13d26a4fb0ddc8b20efb5e22e372a2b7 100644 GIT binary patch literal 8196 zcmeHMU2GLa6rOKeV3!TJ)B*)rduxSaxrS1pwW4sjZ6iM|f48MS{M5a77rJ4)Tkh_? zr8a1N(tw)yqVfNYs1K-76n!%BKQTTKBuY%gnE1qtM)1MJcxGpBp)GyX5ZFm(&YUyn z%=Dc7&di-DV+`qnxrVVS#+Xc%Q%$Aj28ECFI;}{Nq?{r|=0%?2+!v#98%H3Hz-<`;zCR?Wa+-?hxS-+IK}~oCAj+=* zVWGapJ46!^O+|EEP#U2EVM7^n%E~qePh%O(9Rz`F~LAW~2p9s|% zQi8^99Dz6j(<8vsr;HhFg!$Q#>+kQ5?FS7F7a*0D&ze0)o+GRBfy|&ck_n8U;HQoJ zKF)P{o|7&0jqG0A8X3?kTRk^0Y}d+%2DWLD%KjePHNBCvS8z=~w9SYCS&@|it!iwn zxurRoXl!meo=l9jG&Up?>((|MKd#6NYgcdBGjPNia=jD60YsYsn>WRwlh?d){9>zk zjVQ}xhlMv6OWr7RRdt@;)7#g-Z~uU%6?|)tvQld4FT`L#g(yox9K9p)IITE7e6kMccQtj@3P4 zo55h8;pZ&ZwB5t~6i+v>57|~;S%9R{r=H$v-B3S_R)eJjB2&A zEY_&eYZdK`yx$}zJ3W)nXoe$tjb10q`?$O{Z&546YQ3IRG(+z4$z?IcYITjG`AXLj z`sl)Ro!TU8rqb;gS<4Zk7IlNHZC84V!eb$OdOY3d2_O*PFFWh1H z!OmgJE&WMpeTyvj`-4W_+BrgyD_L~Na`+iZ&Q01)JMi*hG{1}cAy_Qm^g&Pa8N)I$1sY=@f4oM37o|9cp0zYHN1{fcn9y|J$!=C@HxK3SGa(m zaS6ZRGOpk%{*n}_LRu#2(%n*>v{I^%z&6}34$ZRPV^yXGFXTAM`)BiI#n#LlK<5$vjDb+sl@ zjtaJ6O|`0(Q3fiZ?fN9Kh?tfKRaGmeTohv)bz)h>q+4kko>}sLh4F9LIram)#4Z!Y z|3MjKEJgw=kR(iRM2c|ThIYdBF6==DT{wUN7|0UJ9k>|AQRFd(<9Gt&c#<&w44%bv zco8oV&R->@pTe7X3vc6noWVypi;qLZe~a&N5x-8saOY$U z~6JAyE`+xh)@Bg=?@3B*H1mXyY2%x+z)0U?GOnV={YbU5aN|hI`H!f&sLd~Ry m0OBY9VMzT1sdAfCM8^ds36=l-Az=Cs?fCqU&;Q%*{QnoX&1A*^ delta 189 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{MGjdEU6q~50D9QxlfW(Rml5+BsfV`ba1rrOG zPd1QY5oTscWGH7SPAW%8PF4~qg0d64S3bJuy;dkcA{4$mbAYq`LAge(%3y21CKyCv$mtk`}&m3j| DwQnJ( diff --git a/UPGRADE_NOTES.md b/UPGRADE_NOTES.md index ea16ceb..af9ba2b 100644 --- a/UPGRADE_NOTES.md +++ b/UPGRADE_NOTES.md @@ -73,8 +73,28 @@ manifests and rationale. These are suggested changes for review, not a tested migration plan. - **RDS -> Aurora is a data migration, not a version bump.** Terraform will want to destroy the `aws_db_instance` and create a new `aws_rds_cluster`. - Plan an actual migration (snapshot restore into Aurora, or logical - dump/restore) and cut Opal over during a maintenance window. + Staged migration procedure (per platform-engineering review): + 1. Before touching this branch, take a manual snapshot of the existing + `aws_db_instance` (`aws rds create-db-snapshot ...`) as a safety net + that doesn't depend on Terraform's own state. + 2. On the branch still targeting the old `aws_db_instance`, make sure a + stray `apply` can't silently destroy it during the migration window: + ```hcl + resource "aws_db_instance" "opal" { + # ... + skip_final_snapshot = false + final_snapshot_identifier = "opal-final-${formatdate("YYYYMMDD-hhmmss", timestamp())}" + + lifecycle { + prevent_destroy = true + } + } + ``` + 3. Provision the new `aws_rds_cluster` (this branch) alongside the old + instance, restore data into it (snapshot restore or logical + dump/restore), and validate against Opal during a maintenance window. + 4. Only after cutover is confirmed, remove the `prevent_destroy` lifecycle + block and the old `aws_db_instance` resource in a follow-up apply. - **VPC module bumped 3.2.0 -> ~> 6.0** (not one of the 7 items, but the old version isn't validated against AWS provider 6.x and would likely break planning). This also jumps several majors - diff the plan carefully. diff --git a/aws/iam.tf b/aws/iam.tf index 64fecf3..2038ec0 100644 --- a/aws/iam.tf +++ b/aws/iam.tf @@ -38,7 +38,6 @@ resource "aws_iam_policy" "eks_cluster_admin" { "Effect": "Allow", "Action": "eks:*", "Resource": [ - "arn:aws:eks:${var.region}:${data.aws_caller_identity.current.account_id}:fargateprofile/${module.eks.cluster_name}/*/*", "arn:aws:eks:${var.region}:${data.aws_caller_identity.current.account_id}:addon/${module.eks.cluster_name}/*/*", "arn:aws:eks:${var.region}:${data.aws_caller_identity.current.account_id}:identityproviderconfig/${module.eks.cluster_name}/*/*/*", "arn:aws:eks:${var.region}:${data.aws_caller_identity.current.account_id}:cluster/${module.eks.cluster_name}", diff --git a/aws/outputs.tf b/aws/outputs.tf index a64fe3b..959c429 100644 --- a/aws/outputs.tf +++ b/aws/outputs.tf @@ -9,12 +9,12 @@ output "rds_reader_hostname" { } output "rds_port" { - description = "RDS instance port" + description = "Aurora cluster port" value = aws_rds_cluster.opal.port } output "rds_username" { - description = "RDS instance root username" + description = "Aurora cluster master username" value = aws_rds_cluster.opal.master_username } From 2edba3db660a5c0f25bad1634cc2ca6660b7c2c8 Mon Sep 17 00:00:00 2001 From: John Clark Date: Mon, 27 Jul 2026 19:24:43 -0700 Subject: [PATCH 3/3] Remove tracked .DS_Store and ignore it going forward --- .DS_Store | Bin 8196 -> 0 bytes .gitignore | 3 +++ 2 files changed, 3 insertions(+) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index ef8f042e13d26a4fb0ddc8b20efb5e22e372a2b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMU2GLa6rOKeV3!TJ)B*)rduxSaxrS1pwW4sjZ6iM|f48MS{M5a77rJ4)Tkh_? zr8a1N(tw)yqVfNYs1K-76n!%BKQTTKBuY%gnE1qtM)1MJcxGpBp)GyX5ZFm(&YUyn z%=Dc7&di-DV+`qnxrVVS#+Xc%Q%$Aj28ECFI;}{Nq?{r|=0%?2+!v#98%H3Hz-<`;zCR?Wa+-?hxS-+IK}~oCAj+=* zVWGapJ46!^O+|EEP#U2EVM7^n%E~qePh%O(9Rz`F~LAW~2p9s|% zQi8^99Dz6j(<8vsr;HhFg!$Q#>+kQ5?FS7F7a*0D&ze0)o+GRBfy|&ck_n8U;HQoJ zKF)P{o|7&0jqG0A8X3?kTRk^0Y}d+%2DWLD%KjePHNBCvS8z=~w9SYCS&@|it!iwn zxurRoXl!meo=l9jG&Up?>((|MKd#6NYgcdBGjPNia=jD60YsYsn>WRwlh?d){9>zk zjVQ}xhlMv6OWr7RRdt@;)7#g-Z~uU%6?|)tvQld4FT`L#g(yox9K9p)IITE7e6kMccQtj@3P4 zo55h8;pZ&ZwB5t~6i+v>57|~;S%9R{r=H$v-B3S_R)eJjB2&A zEY_&eYZdK`yx$}zJ3W)nXoe$tjb10q`?$O{Z&546YQ3IRG(+z4$z?IcYITjG`AXLj z`sl)Ro!TU8rqb;gS<4Zk7IlNHZC84V!eb$OdOY3d2_O*PFFWh1H z!OmgJE&WMpeTyvj`-4W_+BrgyD_L~Na`+iZ&Q01)JMi*hG{1}cAy_Qm^g&Pa8N)I$1sY=@f4oM37o|9cp0zYHN1{fcn9y|J$!=C@HxK3SGa(m zaS6ZRGOpk%{*n}_LRu#2(%n*>v{I^%z&6}34$ZRPV^yXGFXTAM`)BiI#n#LlK<5$vjDb+sl@ zjtaJ6O|`0(Q3fiZ?fN9Kh?tfKRaGmeTohv)bz)h>q+4kko>}sLh4F9LIram)#4Z!Y z|3MjKEJgw=kR(iRM2c|ThIYdBF6==DT{wUN7|0UJ9k>|AQRFd(<9Gt&c#<&w44%bv zco8oV&R->@pTe7X3vc6noWVypi;qLZe~a&N5x-8saOY$U z~6JAyE`+xh)@Bg=?@3B*H1mXyY2%x+z)0U?GOnV={YbU5aN|hI`H!f&sLd~Ry m0OBY9VMzT1sdAfCM8^ds36=l-Az=Cs?fCqU&;Q%*{QnoX&1A*^ diff --git a/.gitignore b/.gitignore index a8b15f5..c3ff4c9 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ override.tf.json # example: *tfplan* **/.idea/* + +# macOS +.DS_Store