# 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)
    └── <project>/
        ├── 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-<project>-db`
- `janus_database_name` → `<project>_database_name`
- `janus_master_username` → `<project>_master_username`
- `janus_jooq_username` → `<project>_jooq_username` (or `<project>_hibernate_username` if Hibernate)
- `janus_liquibase_username` → `<project>_liquibase_username`
- `janus_iam_username` → `<project>_iam_username`
- `janus_db_params` → `<project>_db_params`
- `janus_db_connect_policy_arn` → `<project>_db_connect_policy_arn`
- `humand-janus-db-connect-policy` → `humand-<project>-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`, `<project>_db_params`, `<project>_db_connect_policy_arn`

---

## modules/\<project\>/ (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 "<project>_docker_image"   { type = string }
variable "commit_sha"               { type = string; default = "local" }
variable "<project>_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_<PROJECT>_KAFKA_CDC_LISTENER_ENABLED", value = "false" },
    ]
    worker = [
      # If Kafka=Yes:
      { name = "HUMAND_<PROJECT>_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
  <project>_lb_host_condition   = var.<project>_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   = "<project>"
}

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/\<project\>/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" "<project>_tg" {
  name        = "humand-<project>-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" "<project>_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.<project>_tg.arn
  }

  condition {
    path_pattern {
      values = ["/api/v1/<project>/*"]
    }
  }

  condition {
    host_header {
      values = [var.<project>_lb_host_condition]
    }
  }
}
```

### Variables

```hcl
variable "humand_vpc_id"              { type = string }
variable "<project>_lb_host_condition" { type = string }
variable "lb_listener_rule_priority"   { type = number }
```

### Output

```hcl
output "target_group_arn" {
  value = aws_lb_target_group.<project>_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 "<project>" {
  source       = "../<project>"
  type         = "api"
  service_name = "humand-<project>-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
  <project>_docker_image      = var.<project>_docker_image
  commit_sha                  = var.commit_sha
  <project>_lb_host_condition = var.<project>_lb_host_condition
  lb_listener_rule_priority   = <LB_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.<project>_db_connect_policy_arn  # if DB=Yes
  db_params             = module.database.<project>_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 "<project>-worker" {
  source       = "../<project>"
  type         = "worker"
  service_name = "humand-<project>-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
  <project>_docker_image      = var.<project>_docker_image
  commit_sha                  = var.commit_sha
  <project>_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.<project>_db_connect_policy_arn  # if DB=Yes
  db_params             = module.database.<project>_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 "<project>_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 "<project>_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       = "<AWS_ACCOUNT>"
  env               = "<ENV>"
  server_domain_url = "<SERVER_DOMAIN_URL>"
  humand_vpc_id     = "<VPC_ID>"

  ecs_public_cluster_arn  = data.aws_ecs_cluster.public_cluster.arn
  ecs_private_cluster_arn = data.aws_ecs_cluster.private_cluster.arn

  <project>_docker_image      = var.docker_image
  commit_sha                  = var.commit_sha
  logging_http_log_body       = false
  <project>_lb_host_condition = "<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 = "<ENV>"
      Name        = "humand-<project>"
      Terraform   = "true"
      Repository  = "humand-<project>-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-<project>-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-<project>`
- `janus-webapp` → `<prefix>-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`.
