From b381933c3b47c9d0ddf6995cedd66190ada09f97 Mon Sep 17 00:00:00 2001 From: Manmohan Sharma Date: Thu, 16 Apr 2026 11:11:02 -0700 Subject: [PATCH] feat(terraform): provision full AWS stack for samosaChaat (issue #4) Add reusable Terraform modules and per-environment configs (dev/uat/prod) in us-west-2 covering: VPC (3 AZ public/private), EKS 1.29 with IRSA and ALB/EBS/EFS CSI add-ons, RDS PostgreSQL 15, four ECR repos, IAM roles (EKS node, ALB controller IRSA, GitHub Actions OIDC), Route53 + ACM for samosachaat.art, and EFS for model weights. State backend on S3 (samosachaat-terraform-state) with DynamoDB lock table. terraform validate passes for dev, uat, and prod. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 12 + terraform/backend.tf | 35 +++ .../environments/dev/.terraform.lock.hcl | 125 ++++++++++ terraform/environments/dev/main.tf | 107 +++++++++ terraform/environments/dev/outputs.tf | 70 ++++++ terraform/environments/dev/variables.tf | 23 ++ terraform/environments/dev/versions.tf | 38 +++ .../environments/prod/.terraform.lock.hcl | 125 ++++++++++ terraform/environments/prod/main.tf | 104 +++++++++ terraform/environments/prod/outputs.tf | 70 ++++++ terraform/environments/prod/variables.tf | 23 ++ terraform/environments/prod/versions.tf | 38 +++ .../environments/uat/.terraform.lock.hcl | 125 ++++++++++ terraform/environments/uat/main.tf | 104 +++++++++ terraform/environments/uat/outputs.tf | 70 ++++++ terraform/environments/uat/variables.tf | 23 ++ terraform/environments/uat/versions.tf | 38 +++ terraform/modules/acm/main.tf | 29 +++ terraform/modules/acm/outputs.tf | 29 +++ terraform/modules/acm/variables.tf | 28 +++ terraform/modules/ecr/main.tf | 50 ++++ terraform/modules/ecr/outputs.tf | 14 ++ terraform/modules/ecr/variables.tf | 22 ++ terraform/modules/efs/main.tf | 73 ++++++ terraform/modules/efs/outputs.tf | 26 +++ terraform/modules/efs/variables.tf | 37 +++ terraform/modules/eks/main.tf | 60 +++++ terraform/modules/eks/outputs.tf | 34 +++ terraform/modules/eks/variables.tf | 50 ++++ terraform/modules/iam/main.tf | 197 ++++++++++++++++ terraform/modules/iam/outputs.tf | 29 +++ .../modules/iam/policies/alb_controller.json | 219 ++++++++++++++++++ terraform/modules/iam/variables.tf | 34 +++ terraform/modules/rds/main.tf | 87 +++++++ terraform/modules/rds/outputs.tf | 36 +++ terraform/modules/rds/variables.tf | 73 ++++++ terraform/modules/route53/main.tf | 58 +++++ terraform/modules/route53/outputs.tf | 14 ++ terraform/modules/route53/variables.tf | 32 +++ terraform/modules/vpc/main.tf | 44 ++++ terraform/modules/vpc/outputs.tf | 34 +++ terraform/modules/vpc/variables.tf | 45 ++++ terraform/versions.tf | 18 ++ 43 files changed, 2502 insertions(+) create mode 100644 terraform/backend.tf create mode 100644 terraform/environments/dev/.terraform.lock.hcl create mode 100644 terraform/environments/dev/main.tf create mode 100644 terraform/environments/dev/outputs.tf create mode 100644 terraform/environments/dev/variables.tf create mode 100644 terraform/environments/dev/versions.tf create mode 100644 terraform/environments/prod/.terraform.lock.hcl create mode 100644 terraform/environments/prod/main.tf create mode 100644 terraform/environments/prod/outputs.tf create mode 100644 terraform/environments/prod/variables.tf create mode 100644 terraform/environments/prod/versions.tf create mode 100644 terraform/environments/uat/.terraform.lock.hcl create mode 100644 terraform/environments/uat/main.tf create mode 100644 terraform/environments/uat/outputs.tf create mode 100644 terraform/environments/uat/variables.tf create mode 100644 terraform/environments/uat/versions.tf create mode 100644 terraform/modules/acm/main.tf create mode 100644 terraform/modules/acm/outputs.tf create mode 100644 terraform/modules/acm/variables.tf create mode 100644 terraform/modules/ecr/main.tf create mode 100644 terraform/modules/ecr/outputs.tf create mode 100644 terraform/modules/ecr/variables.tf create mode 100644 terraform/modules/efs/main.tf create mode 100644 terraform/modules/efs/outputs.tf create mode 100644 terraform/modules/efs/variables.tf create mode 100644 terraform/modules/eks/main.tf create mode 100644 terraform/modules/eks/outputs.tf create mode 100644 terraform/modules/eks/variables.tf create mode 100644 terraform/modules/iam/main.tf create mode 100644 terraform/modules/iam/outputs.tf create mode 100644 terraform/modules/iam/policies/alb_controller.json create mode 100644 terraform/modules/iam/variables.tf create mode 100644 terraform/modules/rds/main.tf create mode 100644 terraform/modules/rds/outputs.tf create mode 100644 terraform/modules/rds/variables.tf create mode 100644 terraform/modules/route53/main.tf create mode 100644 terraform/modules/route53/outputs.tf create mode 100644 terraform/modules/route53/variables.tf create mode 100644 terraform/modules/vpc/main.tf create mode 100644 terraform/modules/vpc/outputs.tf create mode 100644 terraform/modules/vpc/variables.tf create mode 100644 terraform/versions.tf diff --git a/.gitignore b/.gitignore index ad7db224..53723aa5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,15 @@ eval_bundle/ CLAUDE.md wandb/ onnx_export/ + +# Terraform +**/.terraform/* +*.tfstate +*.tfstate.* +*.tfvars +crash.log +crash.*.log +override.tf +override.tf.json +*_override.tf +*_override.tf.json diff --git a/terraform/backend.tf b/terraform/backend.tf new file mode 100644 index 00000000..c7b7ea0e --- /dev/null +++ b/terraform/backend.tf @@ -0,0 +1,35 @@ +# Shared remote-state configuration. +# +# Each environment overrides the `key` via `terraform init -backend-config="key=..."` +# (the harness in environments//main.tf passes `terraform { backend "s3" {} }` +# with no inline values so the same bucket can host multiple state files). +# +# Bootstrap (run once, manually): +# +# aws s3api create-bucket \ +# --bucket samosachaat-terraform-state \ +# --region us-west-2 \ +# --create-bucket-configuration LocationConstraint=us-west-2 +# aws s3api put-bucket-versioning \ +# --bucket samosachaat-terraform-state \ +# --versioning-configuration Status=Enabled +# aws s3api put-bucket-encryption \ +# --bucket samosachaat-terraform-state \ +# --server-side-encryption-configuration \ +# '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}' +# aws dynamodb create-table \ +# --table-name samosachaat-terraform-locks \ +# --attribute-definitions AttributeName=LockID,AttributeType=S \ +# --key-schema AttributeName=LockID,KeyType=HASH \ +# --billing-mode PAY_PER_REQUEST \ +# --region us-west-2 + +terraform { + backend "s3" { + bucket = "samosachaat-terraform-state" + key = "global/placeholder.tfstate" + region = "us-west-2" + encrypt = true + dynamodb_table = "samosachaat-terraform-locks" + } +} diff --git a/terraform/environments/dev/.terraform.lock.hcl b/terraform/environments/dev/.terraform.lock.hcl new file mode 100644 index 00000000..3c454646 --- /dev/null +++ b/terraform/environments/dev/.terraform.lock.hcl @@ -0,0 +1,125 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = ">= 4.33.0, >= 5.0.0, >= 5.79.0, >= 5.92.0, >= 5.95.0, < 6.0.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} + +provider "registry.terraform.io/hashicorp/cloudinit" { + version = "2.3.7" + constraints = ">= 2.0.0" + hashes = [ + "h1:M9TpQxKAE/hyOwytdX9MUNZw30HoD/OXqYIug5fkqH8=", + "zh:06f1c54e919425c3139f8aeb8fcf9bceca7e560d48c9f0c1e3bb0a8ad9d9da1e", + "zh:0e1e4cf6fd98b019e764c28586a386dc136129fef50af8c7165a067e7e4a31d5", + "zh:1871f4337c7c57287d4d67396f633d224b8938708b772abfc664d1f80bd67edd", + "zh:2b9269d91b742a71b2248439d5e9824f0447e6d261bfb86a8a88528609b136d1", + "zh:3d8ae039af21426072c66d6a59a467d51f2d9189b8198616888c1b7fc42addc7", + "zh:3ef4e2db5bcf3e2d915921adced43929214e0946a6fb11793085d9a48995ae01", + "zh:42ae54381147437c83cbb8790cc68935d71b6357728a154109d3220b1beb4dc9", + "zh:4496b362605ae4cbc9ef7995d102351e2fe311897586ffc7a4a262ccca0c782a", + "zh:652a2401257a12706d32842f66dac05a735693abcb3e6517d6b5e2573729ba13", + "zh:7406c30806f5979eaed5f50c548eced2ea18ea121e01801d2f0d4d87a04f6a14", + "zh:7848429fd5a5bcf35f6fee8487df0fb64b09ec071330f3ff240c0343fe2a5224", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.4" + constraints = ">= 3.0.0" + hashes = [ + "h1:L5V05xwp/Gto1leRryuesxjMfgZwjb7oool4WS1UEFQ=", + "zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43", + "zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a", + "zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991", + "zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f", + "zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e", + "zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615", + "zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442", + "zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5", + "zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f", + "zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.8.1" + constraints = ">= 3.1.0, >= 3.5.0" + hashes = [ + "h1:u8AKlWVDTH5r9YLSeswoVEjiY72Rt4/ch7U+61ZDkiQ=", + "zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4", + "zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae", + "zh:229665ddf060aa0ed315597908483eee5b818a17d09b6417a0f52fd9405c4f57", + "zh:2469d2e48f28076254a2a3fc327f184914566d9e40c5780b8d96ebf7205f8bc0", + "zh:37d7eb334d9561f335e748280f5535a384a88675af9a9eac439d4cfd663bcb66", + "zh:741101426a2f2c52dee37122f0f4a2f2d6af6d852cb1db634480a86398fa3511", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:a902473f08ef8df62cfe6116bd6c157070a93f66622384300de235a533e9d4a9", + "zh:b85c511a23e57a2147355932b3b6dce2a11e856b941165793a0c3d7578d94d05", + "zh:c5172226d18eaac95b1daac80172287b69d4ce32750c82ad77fa0768be4ea4b8", + "zh:dab4434dba34aad569b0bc243c2d3f3ff86dd7740def373f2a49816bd2ff819b", + "zh:f49fd62aa8c5525a5c17abd51e27ca5e213881d58882fd42fec4a545b53c9699", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.13.1" + constraints = ">= 0.9.0" + hashes = [ + "h1:ZT5ppCNIModqk3iOkVt5my8b8yBHmDpl663JtXAIRqM=", + "zh:02cb9aab1002f0f2a94a4f85acec8893297dc75915f7404c165983f720a54b74", + "zh:04429b2b31a492d19e5ecf999b116d396dac0b24bba0d0fb19ecaefe193fdb8f", + "zh:26f8e51bb7c275c404ba6028c1b530312066009194db721a8427a7bc5cdbc83a", + "zh:772ff8dbdbef968651ab3ae76d04afd355c32f8a868d03244db3f8496e462690", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:898db5d2b6bd6ca5457dccb52eedbc7c5b1a71e4a4658381bcbb38cedbbda328", + "zh:8de913bf09a3fa7bedc29fec18c47c571d0c7a3d0644322c46f3aa648cf30cd8", + "zh:9402102c86a87bdfe7e501ffbb9c685c32bbcefcfcf897fd7d53df414c36877b", + "zh:b18b9bb1726bb8cfbefc0a29cf3657c82578001f514bcf4c079839b6776c47f0", + "zh:b9d31fdc4faecb909d7c5ce41d2479dd0536862a963df434be4b16e8e4edc94d", + "zh:c951e9f39cca3446c060bd63933ebb89cedde9523904813973fbc3d11863ba75", + "zh:e5b773c0d07e962291be0e9b413c7a22c044b8c7b58c76e8aa91d1659990dfb5", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.2.1" + constraints = ">= 3.0.0, >= 4.0.0" + hashes = [ + "h1:akFNuHwvrtnYMBofieoeXhPJDhYZzJVu/Q/BgZK2fgg=", + "zh:0d1e7d07ac973b97fa228f46596c800de830820506ee145626f079dd6bbf8d8a", + "zh:5c7e3d4348cb4861ab812973ef493814a4b224bdd3e9d534a7c8a7c992382b86", + "zh:7c6d4a86cd7a4e9c1025c6b3a3a6a45dea202af85d870cddbab455fb1bd568ad", + "zh:7d0864755ba093664c4b2c07c045d3f5e3d7c799dda1a3ef33d17ed1ac563191", + "zh:83734f57950ab67c0d6a87babdb3f13c908cbe0a48949333f489698532e1391b", + "zh:951e3c285218ebca0cf20eaa4265020b4ef042fea9c6ade115ad1558cfe459e5", + "zh:b9543955b4297e1d93b85900854891c0e645d936d8285a190030475379c5c635", + "zh:bb1bd9e86c003d08c30c1b00d44118ed5bbbf6b1d2d6f7eaac4fa5c6ebea5933", + "zh:c9477bfe00653629cd77ddac3968475f7ad93ac3ca8bc45b56d1d9efb25e4a6e", + "zh:d4cfda8687f736d0cba664c22ec49dae1188289e214ef57f5afe6a7217854fed", + "zh:dc77ee066cf96532a48f0578c35b1eaf6dc4d8ddd0e3ae8e029a3b10676dd5d3", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/terraform/environments/dev/main.tf b/terraform/environments/dev/main.tf new file mode 100644 index 00000000..f7565cbc --- /dev/null +++ b/terraform/environments/dev/main.tf @@ -0,0 +1,107 @@ +locals { + name_prefix = "samosachaat-${var.environment}" + cluster_name = "${local.name_prefix}-eks" + + tags = { + Project = "samosachaat" + Environment = var.environment + } +} + +module "vpc" { + source = "../../modules/vpc" + + name = local.name_prefix + cluster_name = local.cluster_name + cidr = "10.0.0.0/16" + azs = ["us-west-2a", "us-west-2b", "us-west-2c"] + 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"] + single_nat_gateway = true + tags = local.tags +} + +module "eks" { + source = "../../modules/eks" + + cluster_name = local.cluster_name + cluster_version = "1.29" + vpc_id = module.vpc.vpc_id + private_subnet_ids = module.vpc.private_subnet_ids + + node_instance_type = "t3.large" + node_min_size = 2 + node_max_size = 4 + node_desired_size = 2 + + tags = local.tags +} + +module "ecr" { + source = "../../modules/ecr" + + force_delete = true + tags = local.tags +} + +module "iam" { + source = "../../modules/iam" + + name_prefix = local.name_prefix + oidc_provider_arn = module.eks.oidc_provider_arn + oidc_provider_url = module.eks.oidc_provider_url + create_github_oidc = true + github_repositories = var.github_repositories + tags = local.tags +} + +module "rds" { + source = "../../modules/rds" + + identifier = "${local.name_prefix}-pg" + vpc_id = module.vpc.vpc_id + private_subnet_ids = module.vpc.private_subnet_ids + eks_node_security_group_id = module.eks.node_security_group_id + + instance_class = "db.t3.micro" + multi_az = false + skip_final_snapshot = true + deletion_protection = false + + tags = local.tags +} + +module "efs" { + source = "../../modules/efs" + + name = "${local.name_prefix}-models" + vpc_id = module.vpc.vpc_id + private_subnet_ids = module.vpc.private_subnet_ids + eks_node_security_group_id = module.eks.node_security_group_id + + tags = local.tags +} + +module "acm" { + source = "../../modules/acm" + + domain_name = var.domain_name + subject_alternative_names = ["*.${var.domain_name}"] + # First apply: leave wait_for_validation = false so Route53 records can be + # created in the same plan. Flip to true on a follow-up apply if desired. + wait_for_validation = false + + tags = local.tags +} + +module "route53" { + source = "../../modules/route53" + + domain_name = var.domain_name + subdomains = ["grafana"] + acm_validation_records = module.acm.validation_records + # alb_dns_name / alb_zone_id are populated after the AWS Load Balancer + # Controller provisions the Ingress. Re-apply with -var to wire them up. + alb_dns_name = "" + alb_zone_id = "" +} diff --git a/terraform/environments/dev/outputs.tf b/terraform/environments/dev/outputs.tf new file mode 100644 index 00000000..8113c702 --- /dev/null +++ b/terraform/environments/dev/outputs.tf @@ -0,0 +1,70 @@ +output "vpc_id" { + description = "VPC identifier." + value = module.vpc.vpc_id +} + +output "private_subnet_ids" { + description = "Private subnet identifiers." + value = module.vpc.private_subnet_ids +} + +output "public_subnet_ids" { + description = "Public subnet identifiers." + value = module.vpc.public_subnet_ids +} + +output "cluster_name" { + description = "EKS cluster name." + value = module.eks.cluster_name +} + +output "cluster_endpoint" { + description = "EKS API endpoint." + value = module.eks.cluster_endpoint +} + +output "cluster_oidc_provider_arn" { + description = "OIDC provider ARN for IRSA bindings." + value = module.eks.oidc_provider_arn +} + +output "rds_endpoint" { + description = "RDS endpoint (host:port)." + value = module.rds.db_instance_endpoint +} + +output "rds_password" { + description = "Generated RDS master password." + value = module.rds.db_password + sensitive = true +} + +output "ecr_repository_urls" { + description = "ECR repository URLs by name." + value = module.ecr.repository_urls +} + +output "efs_file_system_id" { + description = "EFS filesystem ID for model weights." + value = module.efs.file_system_id +} + +output "acm_certificate_arn" { + description = "ACM cert ARN for the ALB Ingress." + value = module.acm.certificate_arn +} + +output "route53_zone_id" { + description = "Route53 hosted zone ID." + value = module.route53.zone_id +} + +output "alb_controller_role_arn" { + description = "IRSA role ARN for the AWS Load Balancer Controller." + value = module.iam.alb_controller_role_arn +} + +output "github_actions_role_arn" { + description = "IAM role for GitHub Actions OIDC assumption." + value = module.iam.github_actions_role_arn +} diff --git a/terraform/environments/dev/variables.tf b/terraform/environments/dev/variables.tf new file mode 100644 index 00000000..95698452 --- /dev/null +++ b/terraform/environments/dev/variables.tf @@ -0,0 +1,23 @@ +variable "region" { + description = "AWS region." + type = string + default = "us-west-2" +} + +variable "environment" { + description = "Environment name (dev/uat/prod)." + type = string + default = "dev" +} + +variable "domain_name" { + description = "Apex domain — must already have a Route53 hosted zone." + type = string + default = "samosachaat.art" +} + +variable "github_repositories" { + description = "GitHub repos that may assume the CI role." + type = list(string) + default = ["manmohan659/nanochat"] +} diff --git a/terraform/environments/dev/versions.tf b/terraform/environments/dev/versions.tf new file mode 100644 index 00000000..2ded7ae2 --- /dev/null +++ b/terraform/environments/dev/versions.tf @@ -0,0 +1,38 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.5" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0" + } + } + + backend "s3" { + bucket = "samosachaat-terraform-state" + key = "envs/dev/terraform.tfstate" + region = "us-west-2" + encrypt = true + dynamodb_table = "samosachaat-terraform-locks" + } +} + +provider "aws" { + region = var.region + + default_tags { + tags = { + Project = "samosachaat" + Environment = var.environment + ManagedBy = "terraform" + } + } +} diff --git a/terraform/environments/prod/.terraform.lock.hcl b/terraform/environments/prod/.terraform.lock.hcl new file mode 100644 index 00000000..3c454646 --- /dev/null +++ b/terraform/environments/prod/.terraform.lock.hcl @@ -0,0 +1,125 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = ">= 4.33.0, >= 5.0.0, >= 5.79.0, >= 5.92.0, >= 5.95.0, < 6.0.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} + +provider "registry.terraform.io/hashicorp/cloudinit" { + version = "2.3.7" + constraints = ">= 2.0.0" + hashes = [ + "h1:M9TpQxKAE/hyOwytdX9MUNZw30HoD/OXqYIug5fkqH8=", + "zh:06f1c54e919425c3139f8aeb8fcf9bceca7e560d48c9f0c1e3bb0a8ad9d9da1e", + "zh:0e1e4cf6fd98b019e764c28586a386dc136129fef50af8c7165a067e7e4a31d5", + "zh:1871f4337c7c57287d4d67396f633d224b8938708b772abfc664d1f80bd67edd", + "zh:2b9269d91b742a71b2248439d5e9824f0447e6d261bfb86a8a88528609b136d1", + "zh:3d8ae039af21426072c66d6a59a467d51f2d9189b8198616888c1b7fc42addc7", + "zh:3ef4e2db5bcf3e2d915921adced43929214e0946a6fb11793085d9a48995ae01", + "zh:42ae54381147437c83cbb8790cc68935d71b6357728a154109d3220b1beb4dc9", + "zh:4496b362605ae4cbc9ef7995d102351e2fe311897586ffc7a4a262ccca0c782a", + "zh:652a2401257a12706d32842f66dac05a735693abcb3e6517d6b5e2573729ba13", + "zh:7406c30806f5979eaed5f50c548eced2ea18ea121e01801d2f0d4d87a04f6a14", + "zh:7848429fd5a5bcf35f6fee8487df0fb64b09ec071330f3ff240c0343fe2a5224", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.4" + constraints = ">= 3.0.0" + hashes = [ + "h1:L5V05xwp/Gto1leRryuesxjMfgZwjb7oool4WS1UEFQ=", + "zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43", + "zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a", + "zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991", + "zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f", + "zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e", + "zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615", + "zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442", + "zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5", + "zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f", + "zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.8.1" + constraints = ">= 3.1.0, >= 3.5.0" + hashes = [ + "h1:u8AKlWVDTH5r9YLSeswoVEjiY72Rt4/ch7U+61ZDkiQ=", + "zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4", + "zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae", + "zh:229665ddf060aa0ed315597908483eee5b818a17d09b6417a0f52fd9405c4f57", + "zh:2469d2e48f28076254a2a3fc327f184914566d9e40c5780b8d96ebf7205f8bc0", + "zh:37d7eb334d9561f335e748280f5535a384a88675af9a9eac439d4cfd663bcb66", + "zh:741101426a2f2c52dee37122f0f4a2f2d6af6d852cb1db634480a86398fa3511", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:a902473f08ef8df62cfe6116bd6c157070a93f66622384300de235a533e9d4a9", + "zh:b85c511a23e57a2147355932b3b6dce2a11e856b941165793a0c3d7578d94d05", + "zh:c5172226d18eaac95b1daac80172287b69d4ce32750c82ad77fa0768be4ea4b8", + "zh:dab4434dba34aad569b0bc243c2d3f3ff86dd7740def373f2a49816bd2ff819b", + "zh:f49fd62aa8c5525a5c17abd51e27ca5e213881d58882fd42fec4a545b53c9699", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.13.1" + constraints = ">= 0.9.0" + hashes = [ + "h1:ZT5ppCNIModqk3iOkVt5my8b8yBHmDpl663JtXAIRqM=", + "zh:02cb9aab1002f0f2a94a4f85acec8893297dc75915f7404c165983f720a54b74", + "zh:04429b2b31a492d19e5ecf999b116d396dac0b24bba0d0fb19ecaefe193fdb8f", + "zh:26f8e51bb7c275c404ba6028c1b530312066009194db721a8427a7bc5cdbc83a", + "zh:772ff8dbdbef968651ab3ae76d04afd355c32f8a868d03244db3f8496e462690", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:898db5d2b6bd6ca5457dccb52eedbc7c5b1a71e4a4658381bcbb38cedbbda328", + "zh:8de913bf09a3fa7bedc29fec18c47c571d0c7a3d0644322c46f3aa648cf30cd8", + "zh:9402102c86a87bdfe7e501ffbb9c685c32bbcefcfcf897fd7d53df414c36877b", + "zh:b18b9bb1726bb8cfbefc0a29cf3657c82578001f514bcf4c079839b6776c47f0", + "zh:b9d31fdc4faecb909d7c5ce41d2479dd0536862a963df434be4b16e8e4edc94d", + "zh:c951e9f39cca3446c060bd63933ebb89cedde9523904813973fbc3d11863ba75", + "zh:e5b773c0d07e962291be0e9b413c7a22c044b8c7b58c76e8aa91d1659990dfb5", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.2.1" + constraints = ">= 3.0.0, >= 4.0.0" + hashes = [ + "h1:akFNuHwvrtnYMBofieoeXhPJDhYZzJVu/Q/BgZK2fgg=", + "zh:0d1e7d07ac973b97fa228f46596c800de830820506ee145626f079dd6bbf8d8a", + "zh:5c7e3d4348cb4861ab812973ef493814a4b224bdd3e9d534a7c8a7c992382b86", + "zh:7c6d4a86cd7a4e9c1025c6b3a3a6a45dea202af85d870cddbab455fb1bd568ad", + "zh:7d0864755ba093664c4b2c07c045d3f5e3d7c799dda1a3ef33d17ed1ac563191", + "zh:83734f57950ab67c0d6a87babdb3f13c908cbe0a48949333f489698532e1391b", + "zh:951e3c285218ebca0cf20eaa4265020b4ef042fea9c6ade115ad1558cfe459e5", + "zh:b9543955b4297e1d93b85900854891c0e645d936d8285a190030475379c5c635", + "zh:bb1bd9e86c003d08c30c1b00d44118ed5bbbf6b1d2d6f7eaac4fa5c6ebea5933", + "zh:c9477bfe00653629cd77ddac3968475f7ad93ac3ca8bc45b56d1d9efb25e4a6e", + "zh:d4cfda8687f736d0cba664c22ec49dae1188289e214ef57f5afe6a7217854fed", + "zh:dc77ee066cf96532a48f0578c35b1eaf6dc4d8ddd0e3ae8e029a3b10676dd5d3", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/terraform/environments/prod/main.tf b/terraform/environments/prod/main.tf new file mode 100644 index 00000000..32044f8a --- /dev/null +++ b/terraform/environments/prod/main.tf @@ -0,0 +1,104 @@ +locals { + name_prefix = "samosachaat-${var.environment}" + cluster_name = "${local.name_prefix}-eks" + + tags = { + Project = "samosachaat" + Environment = var.environment + } +} + +module "vpc" { + source = "../../modules/vpc" + + name = local.name_prefix + cluster_name = local.cluster_name + cidr = "10.0.0.0/16" + azs = ["us-west-2a", "us-west-2b", "us-west-2c"] + 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"] + single_nat_gateway = false + tags = local.tags +} + +module "eks" { + source = "../../modules/eks" + + cluster_name = local.cluster_name + cluster_version = "1.29" + vpc_id = module.vpc.vpc_id + private_subnet_ids = module.vpc.private_subnet_ids + + node_instance_type = "t3.xlarge" + node_min_size = 3 + node_max_size = 10 + node_desired_size = 3 + + tags = local.tags +} + +module "ecr" { + source = "../../modules/ecr" + + force_delete = false + tags = local.tags +} + +module "iam" { + source = "../../modules/iam" + + name_prefix = local.name_prefix + oidc_provider_arn = module.eks.oidc_provider_arn + oidc_provider_url = module.eks.oidc_provider_url + # GitHub OIDC provider is created by the dev env (account-level resource). + create_github_oidc = false + github_repositories = var.github_repositories + tags = local.tags +} + +module "rds" { + source = "../../modules/rds" + + identifier = "${local.name_prefix}-pg" + vpc_id = module.vpc.vpc_id + private_subnet_ids = module.vpc.private_subnet_ids + eks_node_security_group_id = module.eks.node_security_group_id + + instance_class = "db.t3.medium" + multi_az = true + skip_final_snapshot = false + deletion_protection = true + + tags = local.tags +} + +module "efs" { + source = "../../modules/efs" + + name = "${local.name_prefix}-models" + vpc_id = module.vpc.vpc_id + private_subnet_ids = module.vpc.private_subnet_ids + eks_node_security_group_id = module.eks.node_security_group_id + + tags = local.tags +} + +module "acm" { + source = "../../modules/acm" + + domain_name = var.domain_name + subject_alternative_names = ["*.${var.domain_name}"] + wait_for_validation = false + + tags = local.tags +} + +module "route53" { + source = "../../modules/route53" + + domain_name = var.domain_name + subdomains = ["grafana"] + acm_validation_records = module.acm.validation_records + alb_dns_name = "" + alb_zone_id = "" +} diff --git a/terraform/environments/prod/outputs.tf b/terraform/environments/prod/outputs.tf new file mode 100644 index 00000000..8113c702 --- /dev/null +++ b/terraform/environments/prod/outputs.tf @@ -0,0 +1,70 @@ +output "vpc_id" { + description = "VPC identifier." + value = module.vpc.vpc_id +} + +output "private_subnet_ids" { + description = "Private subnet identifiers." + value = module.vpc.private_subnet_ids +} + +output "public_subnet_ids" { + description = "Public subnet identifiers." + value = module.vpc.public_subnet_ids +} + +output "cluster_name" { + description = "EKS cluster name." + value = module.eks.cluster_name +} + +output "cluster_endpoint" { + description = "EKS API endpoint." + value = module.eks.cluster_endpoint +} + +output "cluster_oidc_provider_arn" { + description = "OIDC provider ARN for IRSA bindings." + value = module.eks.oidc_provider_arn +} + +output "rds_endpoint" { + description = "RDS endpoint (host:port)." + value = module.rds.db_instance_endpoint +} + +output "rds_password" { + description = "Generated RDS master password." + value = module.rds.db_password + sensitive = true +} + +output "ecr_repository_urls" { + description = "ECR repository URLs by name." + value = module.ecr.repository_urls +} + +output "efs_file_system_id" { + description = "EFS filesystem ID for model weights." + value = module.efs.file_system_id +} + +output "acm_certificate_arn" { + description = "ACM cert ARN for the ALB Ingress." + value = module.acm.certificate_arn +} + +output "route53_zone_id" { + description = "Route53 hosted zone ID." + value = module.route53.zone_id +} + +output "alb_controller_role_arn" { + description = "IRSA role ARN for the AWS Load Balancer Controller." + value = module.iam.alb_controller_role_arn +} + +output "github_actions_role_arn" { + description = "IAM role for GitHub Actions OIDC assumption." + value = module.iam.github_actions_role_arn +} diff --git a/terraform/environments/prod/variables.tf b/terraform/environments/prod/variables.tf new file mode 100644 index 00000000..00c26e2e --- /dev/null +++ b/terraform/environments/prod/variables.tf @@ -0,0 +1,23 @@ +variable "region" { + description = "AWS region." + type = string + default = "us-west-2" +} + +variable "environment" { + description = "Environment name (dev/uat/prod)." + type = string + default = "prod" +} + +variable "domain_name" { + description = "Apex domain — must already have a Route53 hosted zone." + type = string + default = "samosachaat.art" +} + +variable "github_repositories" { + description = "GitHub repos that may assume the CI role." + type = list(string) + default = ["manmohan659/nanochat"] +} diff --git a/terraform/environments/prod/versions.tf b/terraform/environments/prod/versions.tf new file mode 100644 index 00000000..a6d1223a --- /dev/null +++ b/terraform/environments/prod/versions.tf @@ -0,0 +1,38 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.5" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0" + } + } + + backend "s3" { + bucket = "samosachaat-terraform-state" + key = "envs/prod/terraform.tfstate" + region = "us-west-2" + encrypt = true + dynamodb_table = "samosachaat-terraform-locks" + } +} + +provider "aws" { + region = var.region + + default_tags { + tags = { + Project = "samosachaat" + Environment = var.environment + ManagedBy = "terraform" + } + } +} diff --git a/terraform/environments/uat/.terraform.lock.hcl b/terraform/environments/uat/.terraform.lock.hcl new file mode 100644 index 00000000..3c454646 --- /dev/null +++ b/terraform/environments/uat/.terraform.lock.hcl @@ -0,0 +1,125 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = ">= 4.33.0, >= 5.0.0, >= 5.79.0, >= 5.92.0, >= 5.95.0, < 6.0.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} + +provider "registry.terraform.io/hashicorp/cloudinit" { + version = "2.3.7" + constraints = ">= 2.0.0" + hashes = [ + "h1:M9TpQxKAE/hyOwytdX9MUNZw30HoD/OXqYIug5fkqH8=", + "zh:06f1c54e919425c3139f8aeb8fcf9bceca7e560d48c9f0c1e3bb0a8ad9d9da1e", + "zh:0e1e4cf6fd98b019e764c28586a386dc136129fef50af8c7165a067e7e4a31d5", + "zh:1871f4337c7c57287d4d67396f633d224b8938708b772abfc664d1f80bd67edd", + "zh:2b9269d91b742a71b2248439d5e9824f0447e6d261bfb86a8a88528609b136d1", + "zh:3d8ae039af21426072c66d6a59a467d51f2d9189b8198616888c1b7fc42addc7", + "zh:3ef4e2db5bcf3e2d915921adced43929214e0946a6fb11793085d9a48995ae01", + "zh:42ae54381147437c83cbb8790cc68935d71b6357728a154109d3220b1beb4dc9", + "zh:4496b362605ae4cbc9ef7995d102351e2fe311897586ffc7a4a262ccca0c782a", + "zh:652a2401257a12706d32842f66dac05a735693abcb3e6517d6b5e2573729ba13", + "zh:7406c30806f5979eaed5f50c548eced2ea18ea121e01801d2f0d4d87a04f6a14", + "zh:7848429fd5a5bcf35f6fee8487df0fb64b09ec071330f3ff240c0343fe2a5224", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.4" + constraints = ">= 3.0.0" + hashes = [ + "h1:L5V05xwp/Gto1leRryuesxjMfgZwjb7oool4WS1UEFQ=", + "zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43", + "zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a", + "zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991", + "zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f", + "zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e", + "zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615", + "zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442", + "zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5", + "zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f", + "zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.8.1" + constraints = ">= 3.1.0, >= 3.5.0" + hashes = [ + "h1:u8AKlWVDTH5r9YLSeswoVEjiY72Rt4/ch7U+61ZDkiQ=", + "zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4", + "zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae", + "zh:229665ddf060aa0ed315597908483eee5b818a17d09b6417a0f52fd9405c4f57", + "zh:2469d2e48f28076254a2a3fc327f184914566d9e40c5780b8d96ebf7205f8bc0", + "zh:37d7eb334d9561f335e748280f5535a384a88675af9a9eac439d4cfd663bcb66", + "zh:741101426a2f2c52dee37122f0f4a2f2d6af6d852cb1db634480a86398fa3511", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:a902473f08ef8df62cfe6116bd6c157070a93f66622384300de235a533e9d4a9", + "zh:b85c511a23e57a2147355932b3b6dce2a11e856b941165793a0c3d7578d94d05", + "zh:c5172226d18eaac95b1daac80172287b69d4ce32750c82ad77fa0768be4ea4b8", + "zh:dab4434dba34aad569b0bc243c2d3f3ff86dd7740def373f2a49816bd2ff819b", + "zh:f49fd62aa8c5525a5c17abd51e27ca5e213881d58882fd42fec4a545b53c9699", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.13.1" + constraints = ">= 0.9.0" + hashes = [ + "h1:ZT5ppCNIModqk3iOkVt5my8b8yBHmDpl663JtXAIRqM=", + "zh:02cb9aab1002f0f2a94a4f85acec8893297dc75915f7404c165983f720a54b74", + "zh:04429b2b31a492d19e5ecf999b116d396dac0b24bba0d0fb19ecaefe193fdb8f", + "zh:26f8e51bb7c275c404ba6028c1b530312066009194db721a8427a7bc5cdbc83a", + "zh:772ff8dbdbef968651ab3ae76d04afd355c32f8a868d03244db3f8496e462690", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:898db5d2b6bd6ca5457dccb52eedbc7c5b1a71e4a4658381bcbb38cedbbda328", + "zh:8de913bf09a3fa7bedc29fec18c47c571d0c7a3d0644322c46f3aa648cf30cd8", + "zh:9402102c86a87bdfe7e501ffbb9c685c32bbcefcfcf897fd7d53df414c36877b", + "zh:b18b9bb1726bb8cfbefc0a29cf3657c82578001f514bcf4c079839b6776c47f0", + "zh:b9d31fdc4faecb909d7c5ce41d2479dd0536862a963df434be4b16e8e4edc94d", + "zh:c951e9f39cca3446c060bd63933ebb89cedde9523904813973fbc3d11863ba75", + "zh:e5b773c0d07e962291be0e9b413c7a22c044b8c7b58c76e8aa91d1659990dfb5", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.2.1" + constraints = ">= 3.0.0, >= 4.0.0" + hashes = [ + "h1:akFNuHwvrtnYMBofieoeXhPJDhYZzJVu/Q/BgZK2fgg=", + "zh:0d1e7d07ac973b97fa228f46596c800de830820506ee145626f079dd6bbf8d8a", + "zh:5c7e3d4348cb4861ab812973ef493814a4b224bdd3e9d534a7c8a7c992382b86", + "zh:7c6d4a86cd7a4e9c1025c6b3a3a6a45dea202af85d870cddbab455fb1bd568ad", + "zh:7d0864755ba093664c4b2c07c045d3f5e3d7c799dda1a3ef33d17ed1ac563191", + "zh:83734f57950ab67c0d6a87babdb3f13c908cbe0a48949333f489698532e1391b", + "zh:951e3c285218ebca0cf20eaa4265020b4ef042fea9c6ade115ad1558cfe459e5", + "zh:b9543955b4297e1d93b85900854891c0e645d936d8285a190030475379c5c635", + "zh:bb1bd9e86c003d08c30c1b00d44118ed5bbbf6b1d2d6f7eaac4fa5c6ebea5933", + "zh:c9477bfe00653629cd77ddac3968475f7ad93ac3ca8bc45b56d1d9efb25e4a6e", + "zh:d4cfda8687f736d0cba664c22ec49dae1188289e214ef57f5afe6a7217854fed", + "zh:dc77ee066cf96532a48f0578c35b1eaf6dc4d8ddd0e3ae8e029a3b10676dd5d3", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/terraform/environments/uat/main.tf b/terraform/environments/uat/main.tf new file mode 100644 index 00000000..1ccdeef5 --- /dev/null +++ b/terraform/environments/uat/main.tf @@ -0,0 +1,104 @@ +locals { + name_prefix = "samosachaat-${var.environment}" + cluster_name = "${local.name_prefix}-eks" + + tags = { + Project = "samosachaat" + Environment = var.environment + } +} + +module "vpc" { + source = "../../modules/vpc" + + name = local.name_prefix + cluster_name = local.cluster_name + cidr = "10.0.0.0/16" + azs = ["us-west-2a", "us-west-2b", "us-west-2c"] + 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"] + single_nat_gateway = true + tags = local.tags +} + +module "eks" { + source = "../../modules/eks" + + cluster_name = local.cluster_name + cluster_version = "1.29" + vpc_id = module.vpc.vpc_id + private_subnet_ids = module.vpc.private_subnet_ids + + node_instance_type = "t3.large" + node_min_size = 2 + node_max_size = 4 + node_desired_size = 2 + + tags = local.tags +} + +module "ecr" { + source = "../../modules/ecr" + + force_delete = false + tags = local.tags +} + +module "iam" { + source = "../../modules/iam" + + name_prefix = local.name_prefix + oidc_provider_arn = module.eks.oidc_provider_arn + oidc_provider_url = module.eks.oidc_provider_url + # GitHub OIDC provider is created by the dev env (account-level resource). + create_github_oidc = false + github_repositories = var.github_repositories + tags = local.tags +} + +module "rds" { + source = "../../modules/rds" + + identifier = "${local.name_prefix}-pg" + vpc_id = module.vpc.vpc_id + private_subnet_ids = module.vpc.private_subnet_ids + eks_node_security_group_id = module.eks.node_security_group_id + + instance_class = "db.t3.micro" + multi_az = false + skip_final_snapshot = true + deletion_protection = false + + tags = local.tags +} + +module "efs" { + source = "../../modules/efs" + + name = "${local.name_prefix}-models" + vpc_id = module.vpc.vpc_id + private_subnet_ids = module.vpc.private_subnet_ids + eks_node_security_group_id = module.eks.node_security_group_id + + tags = local.tags +} + +module "acm" { + source = "../../modules/acm" + + domain_name = var.domain_name + subject_alternative_names = ["*.${var.domain_name}"] + wait_for_validation = false + + tags = local.tags +} + +module "route53" { + source = "../../modules/route53" + + domain_name = var.domain_name + subdomains = ["grafana"] + acm_validation_records = module.acm.validation_records + alb_dns_name = "" + alb_zone_id = "" +} diff --git a/terraform/environments/uat/outputs.tf b/terraform/environments/uat/outputs.tf new file mode 100644 index 00000000..8113c702 --- /dev/null +++ b/terraform/environments/uat/outputs.tf @@ -0,0 +1,70 @@ +output "vpc_id" { + description = "VPC identifier." + value = module.vpc.vpc_id +} + +output "private_subnet_ids" { + description = "Private subnet identifiers." + value = module.vpc.private_subnet_ids +} + +output "public_subnet_ids" { + description = "Public subnet identifiers." + value = module.vpc.public_subnet_ids +} + +output "cluster_name" { + description = "EKS cluster name." + value = module.eks.cluster_name +} + +output "cluster_endpoint" { + description = "EKS API endpoint." + value = module.eks.cluster_endpoint +} + +output "cluster_oidc_provider_arn" { + description = "OIDC provider ARN for IRSA bindings." + value = module.eks.oidc_provider_arn +} + +output "rds_endpoint" { + description = "RDS endpoint (host:port)." + value = module.rds.db_instance_endpoint +} + +output "rds_password" { + description = "Generated RDS master password." + value = module.rds.db_password + sensitive = true +} + +output "ecr_repository_urls" { + description = "ECR repository URLs by name." + value = module.ecr.repository_urls +} + +output "efs_file_system_id" { + description = "EFS filesystem ID for model weights." + value = module.efs.file_system_id +} + +output "acm_certificate_arn" { + description = "ACM cert ARN for the ALB Ingress." + value = module.acm.certificate_arn +} + +output "route53_zone_id" { + description = "Route53 hosted zone ID." + value = module.route53.zone_id +} + +output "alb_controller_role_arn" { + description = "IRSA role ARN for the AWS Load Balancer Controller." + value = module.iam.alb_controller_role_arn +} + +output "github_actions_role_arn" { + description = "IAM role for GitHub Actions OIDC assumption." + value = module.iam.github_actions_role_arn +} diff --git a/terraform/environments/uat/variables.tf b/terraform/environments/uat/variables.tf new file mode 100644 index 00000000..ee087385 --- /dev/null +++ b/terraform/environments/uat/variables.tf @@ -0,0 +1,23 @@ +variable "region" { + description = "AWS region." + type = string + default = "us-west-2" +} + +variable "environment" { + description = "Environment name (dev/uat/prod)." + type = string + default = "uat" +} + +variable "domain_name" { + description = "Apex domain — must already have a Route53 hosted zone." + type = string + default = "samosachaat.art" +} + +variable "github_repositories" { + description = "GitHub repos that may assume the CI role." + type = list(string) + default = ["manmohan659/nanochat"] +} diff --git a/terraform/environments/uat/versions.tf b/terraform/environments/uat/versions.tf new file mode 100644 index 00000000..3d1fd132 --- /dev/null +++ b/terraform/environments/uat/versions.tf @@ -0,0 +1,38 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.5" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0" + } + } + + backend "s3" { + bucket = "samosachaat-terraform-state" + key = "envs/uat/terraform.tfstate" + region = "us-west-2" + encrypt = true + dynamodb_table = "samosachaat-terraform-locks" + } +} + +provider "aws" { + region = var.region + + default_tags { + tags = { + Project = "samosachaat" + Environment = var.environment + ManagedBy = "terraform" + } + } +} diff --git a/terraform/modules/acm/main.tf b/terraform/modules/acm/main.tf new file mode 100644 index 00000000..16236963 --- /dev/null +++ b/terraform/modules/acm/main.tf @@ -0,0 +1,29 @@ +terraform { + required_version = ">= 1.5.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} + +resource "aws_acm_certificate" "this" { + domain_name = var.domain_name + subject_alternative_names = var.subject_alternative_names + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } + + tags = var.tags +} + +# Route53 records are created in the route53 module from validation_records output. +resource "aws_acm_certificate_validation" "this" { + count = var.wait_for_validation ? 1 : 0 + + certificate_arn = aws_acm_certificate.this.arn + validation_record_fqdns = var.validation_record_fqdns +} diff --git a/terraform/modules/acm/outputs.tf b/terraform/modules/acm/outputs.tf new file mode 100644 index 00000000..c05532e7 --- /dev/null +++ b/terraform/modules/acm/outputs.tf @@ -0,0 +1,29 @@ +output "certificate_arn" { + description = "ARN of the issued certificate (use on the ALB Ingress annotation alb.ingress.kubernetes.io/certificate-arn)." + value = aws_acm_certificate.this.arn +} + +output "domain_validation_options" { + description = "Raw validation options from AWS — used by the route53 module." + value = aws_acm_certificate.this.domain_validation_options +} + +output "validation_records" { + description = "Map keyed by domain → { name, type, record } ready to plug into route53.acm_validation_records." + value = { + for dvo in aws_acm_certificate.this.domain_validation_options : + dvo.domain_name => { + name = dvo.resource_record_name + type = dvo.resource_record_type + record = dvo.resource_record_value + } + } +} + +output "validation_record_fqdns" { + description = "List of FQDNs to feed into aws_acm_certificate_validation." + value = [ + for dvo in aws_acm_certificate.this.domain_validation_options : + dvo.resource_record_name + ] +} diff --git a/terraform/modules/acm/variables.tf b/terraform/modules/acm/variables.tf new file mode 100644 index 00000000..e0991799 --- /dev/null +++ b/terraform/modules/acm/variables.tf @@ -0,0 +1,28 @@ +variable "domain_name" { + description = "Primary domain on the certificate (e.g. samosachaat.art)." + type = string +} + +variable "subject_alternative_names" { + description = "SAN list (e.g. [\"*.samosachaat.art\"])." + type = list(string) + default = [] +} + +variable "wait_for_validation" { + description = "Block apply until DNS validation succeeds. Disable on first apply if Route53 records are created in the same plan." + type = bool + default = true +} + +variable "validation_record_fqdns" { + description = "FQDNs of the DNS validation records (passed in from the route53 module)." + type = list(string) + default = [] +} + +variable "tags" { + description = "Tags applied to every resource." + type = map(string) + default = {} +} diff --git a/terraform/modules/ecr/main.tf b/terraform/modules/ecr/main.tf new file mode 100644 index 00000000..b810b49e --- /dev/null +++ b/terraform/modules/ecr/main.tf @@ -0,0 +1,50 @@ +terraform { + required_version = ">= 1.5.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} + +resource "aws_ecr_repository" "this" { + for_each = toset(var.repository_names) + + name = each.key + image_tag_mutability = "MUTABLE" + force_delete = var.force_delete + + image_scanning_configuration { + scan_on_push = true + } + + encryption_configuration { + encryption_type = "AES256" + } + + tags = var.tags +} + +resource "aws_ecr_lifecycle_policy" "keep_last_20" { + for_each = aws_ecr_repository.this + + repository = each.value.name + + policy = jsonencode({ + rules = [ + { + rulePriority = 1 + description = "Keep only the last 20 images" + selection = { + tagStatus = "any" + countType = "imageCountMoreThan" + countNumber = 20 + } + action = { + type = "expire" + } + } + ] + }) +} diff --git a/terraform/modules/ecr/outputs.tf b/terraform/modules/ecr/outputs.tf new file mode 100644 index 00000000..73083c63 --- /dev/null +++ b/terraform/modules/ecr/outputs.tf @@ -0,0 +1,14 @@ +output "repository_urls" { + description = "Map of repository name → registry URL (used by CI/CD `docker push`)." + value = { for name, repo in aws_ecr_repository.this : name => repo.repository_url } +} + +output "repository_arns" { + description = "Map of repository name → ARN." + value = { for name, repo in aws_ecr_repository.this : name => repo.arn } +} + +output "registry_id" { + description = "Account ID hosting the registry (same for all repos)." + value = values(aws_ecr_repository.this)[0].registry_id +} diff --git a/terraform/modules/ecr/variables.tf b/terraform/modules/ecr/variables.tf new file mode 100644 index 00000000..1be80cb0 --- /dev/null +++ b/terraform/modules/ecr/variables.tf @@ -0,0 +1,22 @@ +variable "repository_names" { + description = "ECR repositories to create." + type = list(string) + default = [ + "samosachaat-frontend", + "samosachaat-auth", + "samosachaat-chat-api", + "samosachaat-inference", + ] +} + +variable "force_delete" { + description = "Allow Terraform to destroy repositories even if they contain images (true for dev only)." + type = bool + default = false +} + +variable "tags" { + description = "Tags applied to every resource." + type = map(string) + default = {} +} diff --git a/terraform/modules/efs/main.tf b/terraform/modules/efs/main.tf new file mode 100644 index 00000000..44d9abf5 --- /dev/null +++ b/terraform/modules/efs/main.tf @@ -0,0 +1,73 @@ +terraform { + required_version = ">= 1.5.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} + +resource "aws_security_group" "efs" { + name = "${var.name}-efs-sg" + description = "NFS from EKS nodes to model-weights EFS" + vpc_id = var.vpc_id + + ingress { + description = "NFS from EKS nodes" + from_port = 2049 + to_port = 2049 + protocol = "tcp" + security_groups = [var.eks_node_security_group_id] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = var.tags +} + +resource "aws_efs_file_system" "this" { + creation_token = var.name + encrypted = true + performance_mode = var.performance_mode + throughput_mode = var.throughput_mode + + lifecycle_policy { + transition_to_ia = "AFTER_30_DAYS" + } + + tags = merge(var.tags, { Name = var.name }) +} + +resource "aws_efs_mount_target" "this" { + for_each = toset(var.private_subnet_ids) + file_system_id = aws_efs_file_system.this.id + subnet_id = each.key + security_groups = [aws_security_group.efs.id] +} + +# Access point used by inference pods (UID/GID match the container user). +resource "aws_efs_access_point" "model_weights" { + file_system_id = aws_efs_file_system.this.id + + posix_user { + uid = 1000 + gid = 1000 + } + + root_directory { + path = "/model-weights" + creation_info { + owner_uid = 1000 + owner_gid = 1000 + permissions = "0755" + } + } + + tags = var.tags +} diff --git a/terraform/modules/efs/outputs.tf b/terraform/modules/efs/outputs.tf new file mode 100644 index 00000000..adea40db --- /dev/null +++ b/terraform/modules/efs/outputs.tf @@ -0,0 +1,26 @@ +output "file_system_id" { + description = "EFS filesystem ID — pass to the EFS CSI driver StorageClass." + value = aws_efs_file_system.this.id +} + +output "file_system_arn" { + description = "Filesystem ARN." + value = aws_efs_file_system.this.arn +} + +output "dns_name" { + description = "Mount DNS name." + value = "${aws_efs_file_system.this.id}.efs.${data.aws_region.current.name}.amazonaws.com" +} + +output "access_point_id" { + description = "Access point for the model-weights directory." + value = aws_efs_access_point.model_weights.id +} + +output "security_group_id" { + description = "EFS security group." + value = aws_security_group.efs.id +} + +data "aws_region" "current" {} diff --git a/terraform/modules/efs/variables.tf b/terraform/modules/efs/variables.tf new file mode 100644 index 00000000..960af95a --- /dev/null +++ b/terraform/modules/efs/variables.tf @@ -0,0 +1,37 @@ +variable "name" { + description = "Filesystem name (used in tags and the creation token)." + type = string +} + +variable "vpc_id" { + description = "VPC the mount targets live in." + type = string +} + +variable "private_subnet_ids" { + description = "Private subnets that get mount targets (one per AZ)." + type = list(string) +} + +variable "eks_node_security_group_id" { + description = "Node SG allowed to mount the filesystem." + type = string +} + +variable "performance_mode" { + description = "EFS performance mode." + type = string + default = "generalPurpose" +} + +variable "throughput_mode" { + description = "EFS throughput mode." + type = string + default = "bursting" +} + +variable "tags" { + description = "Tags applied to every resource." + type = map(string) + default = {} +} diff --git a/terraform/modules/eks/main.tf b/terraform/modules/eks/main.tf new file mode 100644 index 00000000..796f198b --- /dev/null +++ b/terraform/modules/eks/main.tf @@ -0,0 +1,60 @@ +terraform { + required_version = ">= 1.5.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} + +data "aws_ssm_parameter" "eks_ami_id" { + name = "/aws/service/eks/optimized-ami/${var.cluster_version}/amazon-linux-2/recommended/image_id" +} + +module "eks" { + source = "terraform-aws-modules/eks/aws" + version = "~> 20.0" + + cluster_name = var.cluster_name + cluster_version = var.cluster_version + + cluster_endpoint_public_access = true + cluster_endpoint_private_access = true + + enable_irsa = true + + vpc_id = var.vpc_id + subnet_ids = var.private_subnet_ids + control_plane_subnet_ids = var.private_subnet_ids + + cluster_addons = { + coredns = { most_recent = true } + kube-proxy = { most_recent = true } + vpc-cni = { most_recent = true } + aws-ebs-csi-driver = { most_recent = true } + aws-efs-csi-driver = { most_recent = true } + } + + eks_managed_node_group_defaults = { + ami_id = data.aws_ssm_parameter.eks_ami_id.value + enable_bootstrap_user_data = true + } + + eks_managed_node_groups = { + default = { + min_size = var.node_min_size + max_size = var.node_max_size + desired_size = var.node_desired_size + + instance_types = [var.node_instance_type] + capacity_type = "ON_DEMAND" + + labels = { + role = "general" + } + } + } + + tags = var.tags +} diff --git a/terraform/modules/eks/outputs.tf b/terraform/modules/eks/outputs.tf new file mode 100644 index 00000000..c6f044f2 --- /dev/null +++ b/terraform/modules/eks/outputs.tf @@ -0,0 +1,34 @@ +output "cluster_name" { + description = "EKS cluster name." + value = module.eks.cluster_name +} + +output "cluster_endpoint" { + description = "EKS API server endpoint." + value = module.eks.cluster_endpoint +} + +output "cluster_certificate_authority_data" { + description = "Base64-encoded cluster CA certificate." + value = module.eks.cluster_certificate_authority_data +} + +output "cluster_security_group_id" { + description = "Cluster control-plane security group." + value = module.eks.cluster_security_group_id +} + +output "node_security_group_id" { + description = "Security group attached to managed node group ENIs (used by RDS / EFS to allow inbound traffic from nodes)." + value = module.eks.node_security_group_id +} + +output "oidc_provider_arn" { + description = "IRSA OIDC provider ARN." + value = module.eks.oidc_provider_arn +} + +output "oidc_provider_url" { + description = "IRSA OIDC issuer URL (without https://)." + value = module.eks.oidc_provider +} diff --git a/terraform/modules/eks/variables.tf b/terraform/modules/eks/variables.tf new file mode 100644 index 00000000..0649befc --- /dev/null +++ b/terraform/modules/eks/variables.tf @@ -0,0 +1,50 @@ +variable "cluster_name" { + description = "EKS cluster name." + type = string +} + +variable "cluster_version" { + description = "Kubernetes version for the EKS control plane." + type = string + default = "1.29" +} + +variable "vpc_id" { + description = "VPC the cluster lives in." + type = string +} + +variable "private_subnet_ids" { + description = "Private subnets for nodes and control-plane ENIs." + type = list(string) +} + +variable "node_instance_type" { + description = "EC2 instance type for the managed node group." + type = string + default = "t3.large" +} + +variable "node_min_size" { + description = "Minimum nodes in the managed node group." + type = number + default = 2 +} + +variable "node_max_size" { + description = "Maximum nodes in the managed node group." + type = number + default = 4 +} + +variable "node_desired_size" { + description = "Desired nodes in the managed node group." + type = number + default = 2 +} + +variable "tags" { + description = "Tags applied to every resource." + type = map(string) + default = {} +} diff --git a/terraform/modules/iam/main.tf b/terraform/modules/iam/main.tf new file mode 100644 index 00000000..0e807bab --- /dev/null +++ b/terraform/modules/iam/main.tf @@ -0,0 +1,197 @@ +terraform { + required_version = ">= 1.5.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0" + } + } +} + +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} + +############################################## +# EKS managed-node-group instance role +############################################## + +data "aws_iam_policy_document" "ec2_assume" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "eks_node" { + name = "${var.name_prefix}-eks-node" + assume_role_policy = data.aws_iam_policy_document.ec2_assume.json + tags = var.tags +} + +locals { + eks_node_managed_policies = { + worker = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKSWorkerNodePolicy" + cni = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKS_CNI_Policy" + ecr_pull = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + ebs_csi = "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy" + efs_csi = "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AmazonEFSCSIDriverPolicy" + ssm = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonSSMManagedInstanceCore" + } +} + +resource "aws_iam_role_policy_attachment" "eks_node" { + for_each = local.eks_node_managed_policies + role = aws_iam_role.eks_node.name + policy_arn = each.value +} + +resource "aws_iam_instance_profile" "eks_node" { + name = aws_iam_role.eks_node.name + role = aws_iam_role.eks_node.name +} + +############################################## +# AWS Load Balancer Controller IRSA role +############################################## + +data "aws_iam_policy_document" "alb_irsa_assume" { + count = var.oidc_provider_arn == "" ? 0 : 1 + + statement { + actions = ["sts:AssumeRoleWithWebIdentity"] + principals { + type = "Federated" + identifiers = [var.oidc_provider_arn] + } + condition { + test = "StringEquals" + variable = "${var.oidc_provider_url}:sub" + values = ["system:serviceaccount:kube-system:aws-load-balancer-controller"] + } + condition { + test = "StringEquals" + variable = "${var.oidc_provider_url}:aud" + values = ["sts.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "alb_controller" { + count = var.oidc_provider_arn == "" ? 0 : 1 + name = "${var.name_prefix}-alb-controller" + assume_role_policy = data.aws_iam_policy_document.alb_irsa_assume[0].json + tags = var.tags +} + +resource "aws_iam_policy" "alb_controller" { + count = var.oidc_provider_arn == "" ? 0 : 1 + name = "${var.name_prefix}-alb-controller" + description = "Permissions required by the AWS Load Balancer Controller." + policy = file("${path.module}/policies/alb_controller.json") +} + +resource "aws_iam_role_policy_attachment" "alb_controller" { + count = var.oidc_provider_arn == "" ? 0 : 1 + role = aws_iam_role.alb_controller[0].name + policy_arn = aws_iam_policy.alb_controller[0].arn +} + +############################################## +# GitHub Actions OIDC provider + CI/CD role +############################################## + +data "tls_certificate" "github" { + count = var.create_github_oidc ? 1 : 0 + url = "https://token.actions.githubusercontent.com" +} + +resource "aws_iam_openid_connect_provider" "github" { + count = var.create_github_oidc ? 1 : 0 + url = "https://token.actions.githubusercontent.com" + client_id_list = ["sts.amazonaws.com"] + thumbprint_list = data.tls_certificate.github[0].certificates[*].sha1_fingerprint + tags = var.tags +} + +data "aws_iam_policy_document" "github_assume" { + count = var.create_github_oidc ? 1 : 0 + + statement { + actions = ["sts:AssumeRoleWithWebIdentity"] + principals { + type = "Federated" + identifiers = [aws_iam_openid_connect_provider.github[0].arn] + } + condition { + test = "StringEquals" + variable = "token.actions.githubusercontent.com:aud" + values = ["sts.amazonaws.com"] + } + condition { + test = "StringLike" + variable = "token.actions.githubusercontent.com:sub" + values = [for r in var.github_repositories : "repo:${r}:*"] + } + } +} + +resource "aws_iam_role" "github_actions" { + count = var.create_github_oidc ? 1 : 0 + name = "${var.name_prefix}-github-actions" + assume_role_policy = data.aws_iam_policy_document.github_assume[0].json + tags = var.tags +} + +# Permissions the CI role needs to push images, update kubeconfig, and apply manifests. +data "aws_iam_policy_document" "github_actions" { + count = var.create_github_oidc ? 1 : 0 + + statement { + sid = "ECRAuth" + actions = [ + "ecr:GetAuthorizationToken", + ] + resources = ["*"] + } + + statement { + sid = "ECRPushPull" + actions = [ + "ecr:BatchCheckLayerAvailability", + "ecr:BatchGetImage", + "ecr:CompleteLayerUpload", + "ecr:GetDownloadUrlForLayer", + "ecr:InitiateLayerUpload", + "ecr:PutImage", + "ecr:UploadLayerPart", + "ecr:DescribeRepositories", + "ecr:ListImages", + ] + resources = ["arn:${data.aws_partition.current.partition}:ecr:*:${data.aws_caller_identity.current.account_id}:repository/samosachaat-*"] + } + + statement { + sid = "EKSDescribe" + actions = ["eks:DescribeCluster", "eks:ListClusters"] + resources = ["*"] + } +} + +resource "aws_iam_policy" "github_actions" { + count = var.create_github_oidc ? 1 : 0 + name = "${var.name_prefix}-github-actions" + policy = data.aws_iam_policy_document.github_actions[0].json +} + +resource "aws_iam_role_policy_attachment" "github_actions" { + count = var.create_github_oidc ? 1 : 0 + role = aws_iam_role.github_actions[0].name + policy_arn = aws_iam_policy.github_actions[0].arn +} diff --git a/terraform/modules/iam/outputs.tf b/terraform/modules/iam/outputs.tf new file mode 100644 index 00000000..671390bc --- /dev/null +++ b/terraform/modules/iam/outputs.tf @@ -0,0 +1,29 @@ +output "eks_node_role_arn" { + description = "ARN of the EKS managed-node-group instance role." + value = aws_iam_role.eks_node.arn +} + +output "eks_node_role_name" { + description = "Name of the EKS node role." + value = aws_iam_role.eks_node.name +} + +output "eks_node_instance_profile_name" { + description = "Instance profile attached to EKS nodes." + value = aws_iam_instance_profile.eks_node.name +} + +output "alb_controller_role_arn" { + description = "IAM role to bind to the aws-load-balancer-controller ServiceAccount via IRSA." + value = try(aws_iam_role.alb_controller[0].arn, "") +} + +output "github_actions_role_arn" { + description = "Role to assume from GitHub Actions for CI/CD (empty if not enabled)." + value = try(aws_iam_role.github_actions[0].arn, "") +} + +output "github_oidc_provider_arn" { + description = "GitHub OIDC provider ARN (empty if not enabled)." + value = try(aws_iam_openid_connect_provider.github[0].arn, "") +} diff --git a/terraform/modules/iam/policies/alb_controller.json b/terraform/modules/iam/policies/alb_controller.json new file mode 100644 index 00000000..a7601906 --- /dev/null +++ b/terraform/modules/iam/policies/alb_controller.json @@ -0,0 +1,219 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:CreateServiceLinkedRole" + ], + "Resource": "*", + "Condition": { + "StringEquals": { + "iam:AWSServiceName": "elasticloadbalancing.amazonaws.com" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "ec2:DescribeAccountAttributes", + "ec2:DescribeAddresses", + "ec2:DescribeAvailabilityZones", + "ec2:DescribeInternetGateways", + "ec2:DescribeVpcs", + "ec2:DescribeVpcPeeringConnections", + "ec2:DescribeSubnets", + "ec2:DescribeSecurityGroups", + "ec2:DescribeInstances", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeTags", + "ec2:GetCoipPoolUsage", + "ec2:DescribeCoipPools", + "elasticloadbalancing:DescribeLoadBalancers", + "elasticloadbalancing:DescribeLoadBalancerAttributes", + "elasticloadbalancing:DescribeListeners", + "elasticloadbalancing:DescribeListenerCertificates", + "elasticloadbalancing:DescribeSSLPolicies", + "elasticloadbalancing:DescribeRules", + "elasticloadbalancing:DescribeTargetGroups", + "elasticloadbalancing:DescribeTargetGroupAttributes", + "elasticloadbalancing:DescribeTargetHealth", + "elasticloadbalancing:DescribeTags" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "cognito-idp:DescribeUserPoolClient", + "acm:ListCertificates", + "acm:DescribeCertificate", + "iam:ListServerCertificates", + "iam:GetServerCertificate", + "waf-regional:GetWebACL", + "waf-regional:GetWebACLForResource", + "waf-regional:AssociateWebACL", + "waf-regional:DisassociateWebACL", + "wafv2:GetWebACL", + "wafv2:GetWebACLForResource", + "wafv2:AssociateWebACL", + "wafv2:DisassociateWebACL", + "shield:GetSubscriptionState", + "shield:DescribeProtection", + "shield:CreateProtection", + "shield:DeleteProtection" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "ec2:AuthorizeSecurityGroupIngress", + "ec2:RevokeSecurityGroupIngress" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "ec2:CreateSecurityGroup" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "ec2:CreateTags" + ], + "Resource": "arn:aws:ec2:*:*:security-group/*", + "Condition": { + "StringEquals": { + "ec2:CreateAction": "CreateSecurityGroup" + }, + "Null": { + "aws:RequestTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "ec2:CreateTags", + "ec2:DeleteTags" + ], + "Resource": "arn:aws:ec2:*:*:security-group/*", + "Condition": { + "Null": { + "aws:RequestTag/elbv2.k8s.aws/cluster": "true", + "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "ec2:AuthorizeSecurityGroupIngress", + "ec2:RevokeSecurityGroupIngress", + "ec2:DeleteSecurityGroup" + ], + "Resource": "*", + "Condition": { + "Null": { + "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:CreateLoadBalancer", + "elasticloadbalancing:CreateTargetGroup" + ], + "Resource": "*", + "Condition": { + "Null": { + "aws:RequestTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:CreateListener", + "elasticloadbalancing:DeleteListener", + "elasticloadbalancing:CreateRule", + "elasticloadbalancing:DeleteRule" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:AddTags", + "elasticloadbalancing:RemoveTags" + ], + "Resource": [ + "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*", + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*" + ], + "Condition": { + "Null": { + "aws:RequestTag/elbv2.k8s.aws/cluster": "true", + "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:AddTags", + "elasticloadbalancing:RemoveTags" + ], + "Resource": [ + "arn:aws:elasticloadbalancing:*:*:listener/net/*/*/*", + "arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*", + "arn:aws:elasticloadbalancing:*:*:listener-rule/net/*/*/*", + "arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*" + ] + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:ModifyLoadBalancerAttributes", + "elasticloadbalancing:SetIpAddressType", + "elasticloadbalancing:SetSecurityGroups", + "elasticloadbalancing:SetSubnets", + "elasticloadbalancing:DeleteLoadBalancer", + "elasticloadbalancing:ModifyTargetGroup", + "elasticloadbalancing:ModifyTargetGroupAttributes", + "elasticloadbalancing:DeleteTargetGroup" + ], + "Resource": "*", + "Condition": { + "Null": { + "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" + } + } + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:RegisterTargets", + "elasticloadbalancing:DeregisterTargets" + ], + "Resource": "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*" + }, + { + "Effect": "Allow", + "Action": [ + "elasticloadbalancing:SetWebAcl", + "elasticloadbalancing:ModifyListener", + "elasticloadbalancing:AddListenerCertificates", + "elasticloadbalancing:RemoveListenerCertificates", + "elasticloadbalancing:ModifyRule" + ], + "Resource": "*" + } + ] +} diff --git a/terraform/modules/iam/variables.tf b/terraform/modules/iam/variables.tf new file mode 100644 index 00000000..62531c54 --- /dev/null +++ b/terraform/modules/iam/variables.tf @@ -0,0 +1,34 @@ +variable "name_prefix" { + description = "Prefix for IAM resource names (e.g. samosachaat-dev)." + type = string +} + +variable "oidc_provider_arn" { + description = "EKS OIDC provider ARN. Pass empty string to skip ALB controller role creation." + type = string + default = "" +} + +variable "oidc_provider_url" { + description = "EKS OIDC issuer hostname (no scheme, e.g. oidc.eks.us-west-2.amazonaws.com/id/XXX)." + type = string + default = "" +} + +variable "create_github_oidc" { + description = "Create the GitHub Actions OIDC provider + CI role. Set to true exactly once per AWS account." + type = bool + default = false +} + +variable "github_repositories" { + description = "GitHub repositories allowed to assume the CI role (e.g. [\"manmohan659/nanochat\"])." + type = list(string) + default = [] +} + +variable "tags" { + description = "Tags applied to every resource." + type = map(string) + default = {} +} diff --git a/terraform/modules/rds/main.tf b/terraform/modules/rds/main.tf new file mode 100644 index 00000000..96bad1bf --- /dev/null +++ b/terraform/modules/rds/main.tf @@ -0,0 +1,87 @@ +terraform { + required_version = ">= 1.5.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.5" + } + } +} + +resource "random_password" "db" { + length = 32 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +resource "aws_security_group" "db" { + name = "${var.identifier}-rds-sg" + description = "PostgreSQL access for samosaChaat from EKS nodes only" + vpc_id = var.vpc_id + + ingress { + description = "PostgreSQL from EKS nodes" + from_port = 5432 + to_port = 5432 + protocol = "tcp" + security_groups = [var.eks_node_security_group_id] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = var.tags +} + +module "db" { + source = "terraform-aws-modules/rds/aws" + version = "~> 6.0" + + identifier = var.identifier + + engine = "postgres" + engine_version = "15" + family = "postgres15" + major_engine_version = "15" + instance_class = var.instance_class + + allocated_storage = var.allocated_storage + max_allocated_storage = var.max_allocated_storage + storage_encrypted = true + + db_name = var.db_name + username = var.db_username + password = random_password.db.result + port = 5432 + + manage_master_user_password = false + + multi_az = var.multi_az + db_subnet_group_name = null + subnet_ids = var.private_subnet_ids + create_db_subnet_group = true + vpc_security_group_ids = [aws_security_group.db.id] + + publicly_accessible = false + + backup_retention_period = 7 + backup_window = "03:00-04:00" + maintenance_window = "Mon:04:00-Mon:05:00" + + skip_final_snapshot = var.skip_final_snapshot + deletion_protection = var.deletion_protection + + performance_insights_enabled = true + create_monitoring_role = true + monitoring_interval = 60 + + tags = var.tags +} diff --git a/terraform/modules/rds/outputs.tf b/terraform/modules/rds/outputs.tf new file mode 100644 index 00000000..9e862fb1 --- /dev/null +++ b/terraform/modules/rds/outputs.tf @@ -0,0 +1,36 @@ +output "db_instance_endpoint" { + description = "Endpoint of the form host:port." + value = module.db.db_instance_endpoint +} + +output "db_instance_address" { + description = "Hostname of the DB instance." + value = module.db.db_instance_address +} + +output "db_instance_port" { + description = "Listening port." + value = module.db.db_instance_port +} + +output "db_instance_name" { + description = "Initial database name." + value = module.db.db_instance_name +} + +output "db_instance_username" { + description = "Master username." + value = module.db.db_instance_username + sensitive = true +} + +output "db_password" { + description = "Generated master password (write to Secrets Manager / Parameter Store from your env config)." + value = random_password.db.result + sensitive = true +} + +output "db_security_group_id" { + description = "Security group attached to the DB." + value = aws_security_group.db.id +} diff --git a/terraform/modules/rds/variables.tf b/terraform/modules/rds/variables.tf new file mode 100644 index 00000000..f0946403 --- /dev/null +++ b/terraform/modules/rds/variables.tf @@ -0,0 +1,73 @@ +variable "identifier" { + description = "DB instance identifier (also used as name prefix)." + type = string +} + +variable "vpc_id" { + description = "VPC the database lives in." + type = string +} + +variable "private_subnet_ids" { + description = "Private subnets for the DB subnet group (>= 2 AZs)." + type = list(string) +} + +variable "eks_node_security_group_id" { + description = "Node SG that should be allowed inbound to PostgreSQL." + type = string +} + +variable "instance_class" { + description = "RDS instance class (e.g. db.t3.micro for dev, db.t3.medium for prod)." + type = string + default = "db.t3.micro" +} + +variable "db_name" { + description = "Initial database name." + type = string + default = "samosachaat" +} + +variable "db_username" { + description = "Master username." + type = string + default = "samosachaat_admin" +} + +variable "allocated_storage" { + description = "Initial storage (GB)." + type = number + default = 20 +} + +variable "max_allocated_storage" { + description = "Storage autoscaling cap (GB)." + type = number + default = 100 +} + +variable "multi_az" { + description = "Enable Multi-AZ (recommended for prod)." + type = bool + default = false +} + +variable "skip_final_snapshot" { + description = "Skip the final snapshot when destroying (true for dev)." + type = bool + default = true +} + +variable "deletion_protection" { + description = "Block accidental deletion (recommended for prod)." + type = bool + default = false +} + +variable "tags" { + description = "Tags applied to every resource." + type = map(string) + default = {} +} diff --git a/terraform/modules/route53/main.tf b/terraform/modules/route53/main.tf new file mode 100644 index 00000000..5742f0b0 --- /dev/null +++ b/terraform/modules/route53/main.tf @@ -0,0 +1,58 @@ +terraform { + required_version = ">= 1.5.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} + +# Use an existing hosted zone (created out-of-band when registering the domain). +data "aws_route53_zone" "this" { + name = var.domain_name + private_zone = false +} + +# alb_dns_name / alb_zone_id come from the AWS Load Balancer Controller after the +# Ingress is created (look up via `kubectl get ingress` or a data source). Pass +# empty strings to skip A-record creation on the first apply, then re-apply. +resource "aws_route53_record" "apex" { + count = var.alb_dns_name == "" ? 0 : 1 + + zone_id = data.aws_route53_zone.this.zone_id + name = var.domain_name + type = "A" + + alias { + name = var.alb_dns_name + zone_id = var.alb_zone_id + evaluate_target_health = true + } +} + +resource "aws_route53_record" "subdomains" { + for_each = var.alb_dns_name == "" ? toset([]) : toset(var.subdomains) + + zone_id = data.aws_route53_zone.this.zone_id + name = "${each.key}.${var.domain_name}" + type = "A" + + alias { + name = var.alb_dns_name + zone_id = var.alb_zone_id + evaluate_target_health = true + } +} + +# ACM DNS-validation CNAMEs. Pass the map exported by the ACM module. +resource "aws_route53_record" "acm_validation" { + for_each = var.acm_validation_records + + zone_id = data.aws_route53_zone.this.zone_id + name = each.value.name + type = each.value.type + records = [each.value.record] + ttl = 60 + allow_overwrite = true +} diff --git a/terraform/modules/route53/outputs.tf b/terraform/modules/route53/outputs.tf new file mode 100644 index 00000000..97bc80e4 --- /dev/null +++ b/terraform/modules/route53/outputs.tf @@ -0,0 +1,14 @@ +output "zone_id" { + description = "Hosted zone ID for the apex domain." + value = data.aws_route53_zone.this.zone_id +} + +output "name_servers" { + description = "Authoritative name servers (configure these at the registrar)." + value = data.aws_route53_zone.this.name_servers +} + +output "apex_record_fqdn" { + description = "FQDN of the apex A record (empty until alb_dns_name is supplied)." + value = try(aws_route53_record.apex[0].fqdn, "") +} diff --git a/terraform/modules/route53/variables.tf b/terraform/modules/route53/variables.tf new file mode 100644 index 00000000..3efd6f6f --- /dev/null +++ b/terraform/modules/route53/variables.tf @@ -0,0 +1,32 @@ +variable "domain_name" { + description = "Apex domain (e.g. samosachaat.art). A hosted zone for this domain must already exist." + type = string +} + +variable "subdomains" { + description = "Subdomains to alias to the ALB (e.g. [\"grafana\", \"api\"])." + type = list(string) + default = ["grafana"] +} + +variable "alb_dns_name" { + description = "ALB DNS name from the AWS Load Balancer Controller. Empty string skips A-record creation (first-apply bootstrap)." + type = string + default = "" +} + +variable "alb_zone_id" { + description = "ALB hosted-zone ID (region-specific)." + type = string + default = "" +} + +variable "acm_validation_records" { + description = "Map keyed by domain → { name, type, record } — pass module.acm.validation_records here." + type = map(object({ + name = string + type = string + record = string + })) + default = {} +} diff --git a/terraform/modules/vpc/main.tf b/terraform/modules/vpc/main.tf new file mode 100644 index 00000000..75a3c1b4 --- /dev/null +++ b/terraform/modules/vpc/main.tf @@ -0,0 +1,44 @@ +terraform { + required_version = ">= 1.5.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} + +locals { + cluster_tag_key = "kubernetes.io/cluster/${var.cluster_name}" +} + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 5.0" + + name = "${var.name}-vpc" + cidr = var.cidr + + azs = var.azs + private_subnets = var.private_subnets + public_subnets = var.public_subnets + + enable_nat_gateway = true + single_nat_gateway = var.single_nat_gateway + one_nat_gateway_per_az = !var.single_nat_gateway + + enable_dns_hostnames = true + enable_dns_support = true + + public_subnet_tags = { + "kubernetes.io/role/elb" = "1" + (local.cluster_tag_key) = "shared" + } + + private_subnet_tags = { + "kubernetes.io/role/internal-elb" = "1" + (local.cluster_tag_key) = "shared" + } + + tags = var.tags +} diff --git a/terraform/modules/vpc/outputs.tf b/terraform/modules/vpc/outputs.tf new file mode 100644 index 00000000..417502f7 --- /dev/null +++ b/terraform/modules/vpc/outputs.tf @@ -0,0 +1,34 @@ +output "vpc_id" { + description = "VPC identifier." + value = module.vpc.vpc_id +} + +output "vpc_cidr_block" { + description = "Primary CIDR block of the VPC." + value = module.vpc.vpc_cidr_block +} + +output "private_subnet_ids" { + description = "Private subnet identifiers (one per AZ)." + value = module.vpc.private_subnets +} + +output "public_subnet_ids" { + description = "Public subnet identifiers (one per AZ)." + value = module.vpc.public_subnets +} + +output "private_subnet_cidrs" { + description = "Private subnet CIDR blocks." + value = module.vpc.private_subnets_cidr_blocks +} + +output "azs" { + description = "AZs in use." + value = module.vpc.azs +} + +output "natgw_ids" { + description = "NAT gateway identifiers." + value = module.vpc.natgw_ids +} diff --git a/terraform/modules/vpc/variables.tf b/terraform/modules/vpc/variables.tf new file mode 100644 index 00000000..4a1694dd --- /dev/null +++ b/terraform/modules/vpc/variables.tf @@ -0,0 +1,45 @@ +variable "name" { + description = "Name prefix used for the VPC and related resources." + type = string +} + +variable "cluster_name" { + description = "EKS cluster name used to tag subnets so the AWS Load Balancer Controller can discover them." + type = string +} + +variable "cidr" { + description = "CIDR block for the VPC." + type = string + default = "10.0.0.0/16" +} + +variable "azs" { + description = "Availability zones to spread subnets across." + type = list(string) + default = ["us-west-2a", "us-west-2b", "us-west-2c"] +} + +variable "private_subnets" { + description = "Private subnet CIDRs (one per AZ, in matching order)." + type = list(string) + default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] +} + +variable "public_subnets" { + description = "Public subnet CIDRs (one per AZ, in matching order)." + type = list(string) + default = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] +} + +variable "single_nat_gateway" { + description = "When true, all private subnets route through a single NAT gateway (dev). When false, one NAT per AZ (prod)." + type = bool + default = true +} + +variable "tags" { + description = "Tags applied to every resource." + type = map(string) + default = {} +} diff --git a/terraform/versions.tf b/terraform/versions.tf new file mode 100644 index 00000000..f3e11f70 --- /dev/null +++ b/terraform/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.5" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0" + } + } +}