# ============================================================================= # AI Software Factory — Terraform (simple self-host path) # # This stack is the SIMPLER of the two supported deployment paths: # 1. Kubernetes (RECOMMENDED) → ../kubernetes/ (recommended for production) # 2. Terraform + EC2 + systemd → THIS DIRECTORY (simple single-team self-host) # # See the README in this directory for the full walkthrough. # ============================================================================= terraform { required_version = ">= 1.6.0" # Remote state in S3 + DynamoDB locking. # # Bootstrap flow (the backend block is active from the first `init`): # 1. Create the state bucket + lock table ONCE with the AWS CLI (or # console), because `terraform init` requires them to already exist: # aws s3api create-bucket --bucket ai-software-factory-tfstate \ # --region us-east-1 # aws dynamodb create-table \ # --table-name ai-software-factory-tflock \ # --attribute-definitions AttributeName=LockID,AttributeType=S \ # --key-schema AttributeName=LockID,KeyType=HASH \ # --billing-mode PAY_PER_REQUEST # 2. `terraform init` (backend comes online), `terraform apply`. # The matching resources below keep the bucket/lock under terraform # management from then on (they become no-ops once imported/adopted). # If your org pre-creates state storage, comment out those resources. backend "s3" { bucket = "ai-software-factory-tfstate" key = "factory/terraform.tfstate" region = "us-east-1" dynamodb_table = "ai-software-factory-tflock" encrypt = true } required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } provider "aws" { region = var.aws_region default_tags { tags = merge(var.tags, { Project = var.project ManagedBy = "terraform" Environment = var.environment }) } } # ---------------------------------------------------------------------------- # Module composition # ---------------------------------------------------------------------------- # The resources in this directory are organized by concern (not by AWS # service) so each file answers one question: # # variables.tf — inputs (region, sizing, CIDRs, tags) # main.tf — this file: provider, locals, bootstrap, module seams # network.tf — VPC, subnets (2 AZs), IGW, NAT, route tables # security.tf — security groups (edge/app/data), KMS, S3 bucket policy # compute.tf — EC2: web / api / worker / realtime node groups (ASG) # data.tf — RDS (Postgres 16), ElastiCache (Redis), S3, OpenSearch # outputs.tf — everything operators need after apply # # The composition mirrors the k8s manifests 1:1 (same components, same # names), so moving between the two paths is a matter of swapping the # deployment tooling, not rethinking the architecture. # ---------------------------------------------------------------------------- locals { project = var.project # Environment tag, e.g. "dev" / "staging" / "prod" — used in resource names. env = var.environment # Canonical name helpers (keep < 63 chars for AWS naming rules). names = { vpc = "${local.project}-${local.env}-vpc" igw = "${local.project}-${local.env}-igw" nat = "${local.project}-${local.env}-nat" eip = "${local.project}-${local.env}-eip" web_alb = "${local.project}-${local.env}-web-alb" rds = "${local.project}-${local.env}-rds" redis = "${local.project}-${local.env}-redis" s3 = "${local.project}-${local.env}-uploads" opensearch = "${local.project}-${local.env}-os" kms = "${local.project}-${local.env}-kms" } # Availability zone indices (we require at least 2 for multi-AZ RDS). az_count = 2 # Common labels for ALB target groups / ASGs / LBs. common_tags = { Project = local.project Environment = local.env Path = "terraform-ec2" } } # ---------------------------------------------------------------------------- # Bootstrap: S3 state bucket + DynamoDB lock table # ---------------------------------------------------------------------------- # These are created with LOCAL state on the very first apply (the backend is # not usable yet because the bucket does not exist). They are cheap and # idempotent. After the first apply, `terraform init -migrate-state` moves # state into the bucket (the backend block above becomes active). # # NOTE: if you prefer to pre-create the state bucket manually (org policy), # comment out the two resources below and keep the backend block. resource "aws_s3_bucket" "terraform_state" { bucket = "ai-software-factory-tfstate" tags = { Project = var.project } } resource "aws_s3_bucket_versioning" "terraform_state" { bucket = aws_s3_bucket.terraform_state.id versioning_configuration { status = "Enabled" } } resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" { bucket = aws_s3_bucket.terraform_state.id rule { apply_server_side_encryption_by_default { sse_algorithm = "aws:kms" } } } resource "aws_dynamodb_table" "terraform_lock" { name = "ai-software-factory-tflock" billing_mode = "PAY_PER_REQUEST" hash_key = "LockID" attribute { name = "LockID" type = "S" } point_in_time_recovery { enabled = false } tags = { Project = var.project } }