1# ============================================================================= 2# AI Software Factory — Terraform (simple self-host path) 3# 4# This stack is the SIMPLER of the two supported deployment paths: 5# 1. Kubernetes (RECOMMENDED) → ../kubernetes/ (recommended for production) 6# 2. Terraform + EC2 + systemd → THIS DIRECTORY (simple single-team self-host) 7# 8# See the README in this directory for the full walkthrough. 9# ============================================================================= 10 11terraform { 12 required_version = ">= 1.6.0" 13 14 # Remote state in S3 + DynamoDB locking. 15 # 16 # Bootstrap flow (the backend block is active from the first `init`): 17 # 1. Create the state bucket + lock table ONCE with the AWS CLI (or 18 # console), because `terraform init` requires them to already exist: 19 # aws s3api create-bucket --bucket ai-software-factory-tfstate \ 20 # --region us-east-1 21 # aws dynamodb create-table \ 22 # --table-name ai-software-factory-tflock \ 23 # --attribute-definitions AttributeName=LockID,AttributeType=S \ 24 # --key-schema AttributeName=LockID,KeyType=HASH \ 25 # --billing-mode PAY_PER_REQUEST 26 # 2. `terraform init` (backend comes online), `terraform apply`. 27 # The matching resources below keep the bucket/lock under terraform 28 # management from then on (they become no-ops once imported/adopted). 29 # If your org pre-creates state storage, comment out those resources. 30 backend "s3" { 31 bucket = "ai-software-factory-tfstate" 32 key = "factory/terraform.tfstate" 33 region = "us-east-1" 34 dynamodb_table = "ai-software-factory-tflock" 35 encrypt = true 36 } 37 38 required_providers { 39 aws = { 40 source = "hashicorp/aws" 41 version = "~> 5.0" 42 } 43 } 44} 45 46provider "aws" { 47 region = var.aws_region 48 49 default_tags { 50 tags = merge(var.tags, { 51 Project = var.project 52 ManagedBy = "terraform" 53 Environment = var.environment 54 }) 55 } 56} 57 58# ---------------------------------------------------------------------------- 59# Module composition 60# ---------------------------------------------------------------------------- 61# The resources in this directory are organized by concern (not by AWS 62# service) so each file answers one question: 63# 64# variables.tf — inputs (region, sizing, CIDRs, tags) 65# main.tf — this file: provider, locals, bootstrap, module seams 66# network.tf — VPC, subnets (2 AZs), IGW, NAT, route tables 67# security.tf — security groups (edge/app/data), KMS, S3 bucket policy 68# compute.tf — EC2: web / api / worker / realtime node groups (ASG) 69# data.tf — RDS (Postgres 16), ElastiCache (Redis), S3, OpenSearch 70# outputs.tf — everything operators need after apply 71# 72# The composition mirrors the k8s manifests 1:1 (same components, same 73# names), so moving between the two paths is a matter of swapping the 74# deployment tooling, not rethinking the architecture. 75# ---------------------------------------------------------------------------- 76 77locals { 78 project = var.project 79 80 # Environment tag, e.g. "dev" / "staging" / "prod" — used in resource names. 81 env = var.environment 82 83 # Canonical name helpers (keep < 63 chars for AWS naming rules). 84 names = { 85 vpc = "${local.project}-${local.env}-vpc" 86 igw = "${local.project}-${local.env}-igw" 87 nat = "${local.project}-${local.env}-nat" 88 eip = "${local.project}-${local.env}-eip" 89 web_alb = "${local.project}-${local.env}-web-alb" 90 rds = "${local.project}-${local.env}-rds" 91 redis = "${local.project}-${local.env}-redis" 92 s3 = "${local.project}-${local.env}-uploads" 93 opensearch = "${local.project}-${local.env}-os" 94 kms = "${local.project}-${local.env}-kms" 95 } 96 97 # Availability zone indices (we require at least 2 for multi-AZ RDS). 98 az_count = 2 99 100 # Common labels for ALB target groups / ASGs / LBs. 101 common_tags = { 102 Project = local.project 103 Environment = local.env 104 Path = "terraform-ec2" 105 } 106} 107 108# ---------------------------------------------------------------------------- 109# Bootstrap: S3 state bucket + DynamoDB lock table 110# ---------------------------------------------------------------------------- 111# These are created with LOCAL state on the very first apply (the backend is 112# not usable yet because the bucket does not exist). They are cheap and 113# idempotent. After the first apply, `terraform init -migrate-state` moves 114# state into the bucket (the backend block above becomes active). 115# 116# NOTE: if you prefer to pre-create the state bucket manually (org policy), 117# comment out the two resources below and keep the backend block. 118 119resource "aws_s3_bucket" "terraform_state" { 120 bucket = "ai-software-factory-tfstate" 121 122 tags = { 123 Project = var.project 124 } 125} 126 127resource "aws_s3_bucket_versioning" "terraform_state" { 128 bucket = aws_s3_bucket.terraform_state.id 129 versioning_configuration { 130 status = "Enabled" 131 } 132} 133 134resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" { 135 bucket = aws_s3_bucket.terraform_state.id 136 rule { 137 apply_server_side_encryption_by_default { 138 sse_algorithm = "aws:kms" 139 } 140 } 141} 142 143resource "aws_dynamodb_table" "terraform_lock" { 144 name = "ai-software-factory-tflock" 145 billing_mode = "PAY_PER_REQUEST" 146 hash_key = "LockID" 147 148 attribute { 149 name = "LockID" 150 type = "S" 151 } 152 153 point_in_time_recovery { 154 enabled = false 155 } 156 157 tags = { 158 Project = var.project 159 } 160} 161