# Kickoff Java Project — Terraform + Workflows Terraform infrastructure and GitHub workflows. Reference: Janus `infrastructure/` directory. See `reference.md` for AWS env table, S3 backend pattern, and ECS cluster naming. --- ## Module structure ``` infrastructure/ ├── AGENTS.md ├── tflint.hcl ├── generate-terraform-docs.sh ├── env/ │ ├── dev/ (main.tf, providers.tf, variables.tf, versions.tf, outputs.tf) │ ├── stg/ │ ├── prd/ │ ├── testslot1/ │ └── testslot2/ └── modules/ ├── datadog/ (main.tf, variables.tf, outputs.tf, versions.tf) ├── db/ (if Own database=Yes) └── / ├── main.tf, variables.tf, outputs.tf, versions.tf ├── modules/ │ └── lb/ (if Load Balancer=Yes) └── service/ (main.tf, variables.tf, outputs.tf, versions.tf) ``` --- ## Root infrastructure files | File | Source | Notes | |------|--------|-------| | `AGENTS.md` | Generated in verification step | Module structure, key commands | | `tflint.hcl` | Copy from Janus byte-for-byte | Do not modify | | `generate-terraform-docs.sh` | Copy from Janus, `chmod +x` | Do not modify | --- ## datadog/ module Copy from Janus `infrastructure/modules/datadog/` as-is. No substitutions needed. **Variables:** `service`, `aws_account`, `env`, `dd_source` **Outputs:** `log_configuration`, sidecar container definitions (log-router, datadog-agent) Called from **inside** the project module (not from service). --- ## db/ module (if Own database=Yes) Copy from Janus `infrastructure/modules/db/` and parameterize: **Substitution rules** — replace all `janus` references: - `humand-janus-db` → `humand--db` - `janus_database_name` → `_database_name` - `janus_master_username` → `_master_username` - `janus_jooq_username` → `_jooq_username` (or `_hibernate_username` if Hibernate) - `janus_liquibase_username` → `_liquibase_username` - `janus_iam_username` → `_iam_username` - `janus_db_params` → `_db_params` - `janus_db_connect_policy_arn` → `_db_connect_policy_arn` - `humand-janus-db-connect-policy` → `humand--db-connect-policy` **Key patterns preserved:** - Aurora PostgreSQL (engine version via `data.aws_rds_engine_version.postgresql`) - Secret Manager (`manage_master_user_password = true`) - IAM database authentication (`iam_database_authentication_enabled = true`) - Users: liquibase, jooq/hibernate, rds_iam - `null_resource` for user creation + default privileges - `aws_iam_policy.db_connect_policy` **Outputs:** `cluster_endpoint`, `cluster_reader_endpoint`, `cluster_id`, `_db_params`, `_db_connect_policy_arn` --- ## modules/\/ (ECS project module) Copy from Janus `infrastructure/modules/janus/` and rename. Defines one ECS service (api or worker). ### Variables ```hcl variable "type" { type = string default = "api" validation { condition = contains(["api", "worker"], var.type) error_message = "Type must be either 'api' or 'worker'" } } variable "service_name" { type = string } variable "aws_account" { type = string } variable "env" { type = string } variable "server_domain_url" { type = string; nullable = false } variable "humand_vpc_id" { type = string } variable "ecs_cluster_arn" { type = string } variable "_docker_image" { type = string } variable "commit_sha" { type = string; default = "local" } variable "_lb_host_condition" { type = string } variable "lb_listener_rule_priority" { type = number; default = 0 } variable "logging_http_log_body" { type = bool; default = false } # If Kafka=Yes: variable "msk_cluster_name" { type = string; default = "" } # If DB=Yes: variable "db_params" { type = object({ endpoint = string port = number database = string jooq_username = string # or hibernate_username liquibase_username = string }) } variable "db_instance_count" { type = number; default = 1 } variable "db_connect_policy_arn" { type = string } # Resources: variable "cpu" { type = number; default = 1024 } variable "memory" { type = number; default = 2048 } variable "desired_count" { type = number; default = 1 } variable "java_tool_options" { type = string; default = "-Xmx1280m -Xms512m" } ``` ### Subnet auto-discovery ```hcl data "aws_subnets" "private" { filter { name = "vpc-id" values = [var.humand_vpc_id] } filter { name = "map-public-ip-on-launch" values = [false] } } ``` ### Type-specific environment variables ```hcl locals { type_environments = { api = [ # If Kafka=Yes: { name = "HUMAND__KAFKA_CDC_LISTENER_ENABLED", value = "false" }, ] worker = [ # If Kafka=Yes: { name = "HUMAND__KAFKA_CDC_LISTENER_ENABLED", value = "true" }, ] } } ``` When Kafka=No, omit the Kafka listener toggle. ### Conditional LB (api only) ```hcl module "lb" { count = var.type == "api" && var.lb_listener_rule_priority > 0 ? 1 : 0 source = "./modules/lb" humand_vpc_id = var.humand_vpc_id _lb_host_condition = var._lb_host_condition lb_listener_rule_priority = var.lb_listener_rule_priority } ``` ### Conditional port mappings ```hcl locals { port_mappings = var.type == "api" ? { http = { containerPort = 8080, hostPort = 8080, protocol = "tcp" } mgmt = { containerPort = 8081, hostPort = 8081, protocol = "tcp" } } : {} load_balancer = var.type == "api" && var.lb_listener_rule_priority > 0 ? { service = { target_group_arn = module.lb[0].target_group_arn container_name = var.service_name container_port = 8080 } } : {} } ``` ### MSK data source (Kafka=Yes only) ```hcl data "aws_msk_cluster" "main" { count = var.msk_cluster_name != "" ? 1 : 0 cluster_name = var.msk_cluster_name } ``` When Kafka=No, omit this data source **and** the `msk_cluster_name` variable entirely (TFLint unused-variable). ### Module calls ```hcl module "datadog" { source = "../datadog" service = var.service_name aws_account = var.aws_account env = var.env dd_source = "" } module "ecs_service" { source = "terraform-aws-modules/ecs/aws//modules/service" version = "5.12.0" # ... full ECS task definition, container definitions, IAM, etc. } ``` ### Outputs ```hcl output "service_name" { value = module.ecs_service.name } output "task_role_arn" { value = module.ecs_service.tasks_iam_role_arn } ``` Reference `module.ecs_service.*` outputs — NOT bare resources. --- ## modules/\/modules/lb/ (LB submodule) Only when Load Balancer=Yes. Copy from Janus `modules/janus/modules/lb/`. ### main.tf ```hcl data "aws_lb" "public_services" { name = "public-services" } data "aws_lb_listener" "public_services_https" { load_balancer_arn = data.aws_lb.public_services.arn port = 443 } resource "aws_lb_target_group" "_tg" { name = "humand--tg" port = 8080 protocol = "HTTP" vpc_id = var.humand_vpc_id target_type = "ip" health_check { enabled = true healthy_threshold = 5 unhealthy_threshold = 5 timeout = 5 interval = 60 path = "/actuator/health?target_group=true" matcher = "200" port = "8081" protocol = "HTTP" } } resource "aws_lb_listener_rule" "_lb_rule" { listener_arn = data.aws_lb_listener.public_services_https.arn priority = var.lb_listener_rule_priority action { type = "forward" target_group_arn = aws_lb_target_group._tg.arn } condition { path_pattern { values = ["/api/v1//*"] } } condition { host_header { values = [var._lb_host_condition] } } } ``` ### Variables ```hcl variable "humand_vpc_id" { type = string } variable "_lb_host_condition" { type = string } variable "lb_listener_rule_priority" { type = number } ``` ### Output ```hcl output "target_group_arn" { value = aws_lb_target_group._tg.arn } ``` When Load Balancer=No, do NOT create this submodule directory. --- ## modules/service/ Orchestrates all infrastructure submodules. ### main.tf ```hcl # If Own database=Yes: module "database" { source = "../db" humand_vpc_id = var.humand_vpc_id subnet_group_name = var.subnet_group_name backup_retention_period = var.db_backup_retention_period instance_class = var.db_instance_class instance_count = var.db_instance_count } # API instance (always): module "" { source = "../" type = "api" service_name = "humand--service" aws_account = var.aws_account env = var.env server_domain_url = var.server_domain_url humand_vpc_id = var.humand_vpc_id ecs_cluster_arn = var.ecs_public_cluster_arn _docker_image = var._docker_image commit_sha = var.commit_sha _lb_host_condition = var._lb_host_condition lb_listener_rule_priority = logging_http_log_body = var.logging_http_log_body msk_cluster_name = var.msk_cluster_name # if Kafka=Yes db_connect_policy_arn = module.database._db_connect_policy_arn # if DB=Yes db_params = module.database._db_params # if DB=Yes db_instance_count = var.db_instance_count # if DB=Yes cpu = var.cpu memory = var.memory java_tool_options = var.java_tool_options depends_on = [module.database] # if DB=Yes } # Worker instance (if Worker=Yes): module "-worker" { source = "../" type = "worker" service_name = "humand--worker" aws_account = var.aws_account env = var.env server_domain_url = var.server_domain_url humand_vpc_id = var.humand_vpc_id ecs_cluster_arn = var.ecs_private_cluster_arn _docker_image = var._docker_image commit_sha = var.commit_sha _lb_host_condition = "" lb_listener_rule_priority = 0 logging_http_log_body = var.logging_http_log_body msk_cluster_name = var.msk_cluster_name # if Kafka=Yes db_connect_policy_arn = module.database._db_connect_policy_arn # if DB=Yes db_params = module.database._db_params # if DB=Yes db_instance_count = var.db_instance_count # if DB=Yes cpu = var.cpu memory = var.memory java_tool_options = var.java_tool_options depends_on = [module.database] # if DB=Yes } ``` ### Variables ```hcl variable "aws_account" { type = string } variable "env" { type = string } variable "humand_vpc_id" { type = string } variable "ecs_public_cluster_arn" { type = string } variable "ecs_private_cluster_arn" { type = string } variable "_docker_image" { type = string } variable "commit_sha" { type = string; default = "local" } variable "server_domain_url" { type = string; nullable = false } variable "logging_http_log_body" { type = bool; default = false } variable "_lb_host_condition" { type = string } variable "msk_cluster_name" { type = string } # if Kafka=Yes variable "subnet_group_name" { type = string } # if DB=Yes variable "db_instance_class" { type = string } # if DB=Yes variable "db_backup_retention_period" { type = number } # if DB=Yes variable "db_instance_count" { type = number; default = 1 } # if DB=Yes variable "cpu" { type = number; default = 1024 } variable "memory" { type = number; default = 2048 } variable "java_tool_options" { type = string; default = "-Xmx1280m -Xms512m" } ``` ### Outputs ```hcl output "db_cluster_endpoint" { # if DB=Yes value = module.database.cluster_endpoint } ``` --- ## env/ files For each of **dev**, **stg**, **prd**, **testslot1**, **testslot2**, create 5 files. Use values from `reference.md` AWS Environment Table. ### main.tf ```hcl data "aws_ecs_cluster" "public_cluster" { cluster_name = "public-{env}-services" } data "aws_ecs_cluster" "private_cluster" { cluster_name = "private-{env}-services" } module "service" { source = "../../modules/service" aws_account = "" env = "" server_domain_url = "" humand_vpc_id = "" ecs_public_cluster_arn = data.aws_ecs_cluster.public_cluster.arn ecs_private_cluster_arn = data.aws_ecs_cluster.private_cluster.arn _docker_image = var.docker_image commit_sha = var.commit_sha logging_http_log_body = false _lb_host_condition = "" msk_cluster_name = "humand-aws-msk-cluster" # if Kafka=Yes # If DB=Yes: subnet_group_name = "{env}-humand-aurora-{env}" db_instance_class = "db.t4g.medium" db_backup_retention_period = 1 # higher for prd } ``` ### providers.tf ```hcl provider "aws" { region = "us-east-1" default_tags { tags = { Environment = "" Name = "humand-" Terraform = "true" Repository = "humand--service" } } } provider "null" {} ``` ### variables.tf ```hcl variable "docker_image" { description = "Docker image to deploy" type = string } variable "commit_sha" { description = "The commit SHA for Datadog version tagging" type = string default = "local" } ``` ### versions.tf ```hcl terraform { required_version = ">= 1.5.7, < 2.0.0" required_providers { aws = { source = "hashicorp/aws"; version = "~> 5.0" } null = { source = "hashicorp/null"; version = "~> 3.0" } } backend "s3" { bucket = "humand-terraform-state-{env}" key = "service/humand--service/terraform.tfstate" region = "us-east-1" dynamodb_table = "humand-terraform-locks-{env}" encrypt = true } } ``` ### outputs.tf ```hcl # If DB=Yes: output "db_cluster_endpoint" { value = module.service.db_cluster_endpoint } ``` --- ## GitHub Workflows Copy all 9 workflows from Janus `.github/workflows/`. Substitution rules: - `humand-janus` → `humand-` - `janus-webapp` → `-webapp` - ECR repository name → new ECR repo name - Service names in deployment configs - JDK version: `25` (already correct in Janus) - Container images: `postgres:17.7`, `cp-kafka:7.8.0` (keep as-is) | Workflow | Purpose | |----------|---------| | `ci.yml` | Build + test on PRs | | `ci-infra.yml` | Terraform fmt + validate + tflint | | `ci-terraform-init.yml` | Terraform init for all envs | | `deployment.yml` | Reusable deployment workflow | | `dev.yml` | Deploy to dev | | `stg.yml` | Deploy to stg | | `prd.yml` | Deploy to prd | | `testslot1.yml` | Deploy to testslot1 | | `testslot2.yml` | Deploy to testslot2 | If **Expose gRPC=Yes**, also copy `publish-libs.yml`. --- ## Post-scaffold Run `terraform fmt -recursive` from `infrastructure/` to ensure consistent formatting. CI (`ci-infra.yml`) checks `terraform fmt -check -recursive`.