Full stack app
A commercial reference stack in kaizen-ext with a container service, load balancer, Postgres, and an uploads bucket.
examples/full-stack-app is the commercial reference stack. It deploys a containerized web app to the kaizen-ext account in us-east-2: a Fargate service behind a load balancer, a Postgres database, and an S3 bucket for user uploads. The agent playbook uses it as the fallback when the juniper reference app is not available.
What it builds
| Module | Purpose in this stack |
|---|---|
| ecr | Private repository for the app image |
| alb | Public HTTPS endpoint with the account's ACM certificate |
| ecs-cluster | Cluster named after the app |
| ecs-service | The app on Fargate, with DATABASE_URL from SSM |
| rds-postgres | Postgres that accepts connections from the app's tasks only |
| s3-bucket | Uploads bucket with CORS for browser uploads |
The stack does not create a VPC. It looks up the shared kaizen-ext-vpc by its Name tag, which the README calls Pattern B. A commented networking block shows the Pattern A alternative for the first app in a new account.
Files
provider.tf # aws ~> 5.0, S3 backend kaizenlabs-ext-terraform-state, key my-app/terraform.tfstate
locals.tf # app name, sizes, bucket name, certificate ARN, tags
main.tf # VPC lookup, module calls, SSM DATABASE_URL, task role S3 policy
outputs.tf # alb_dns, ecr_repository_url, rds_host, s3_uploads_bucketKey excerpts
The VPC lookup finds the shared network and its subnets by tag.
data "aws_vpc" "main" {
tags = { Name = "kaizen-ext-vpc" }
}
data "aws_subnets" "private" {
filter {
name = "vpc-id"
values = [data.aws_vpc.main.id]
}
tags = { tier = "private" }
}The database accepts connections only from the app's security group, and the connection string goes into SSM with TLS required.
module "rds" {
source = "git::ssh://git@github.com/the-kaizen-labs/terraform-modules.git//rds-postgres?ref=v1.7.0"
environment = local.environment
app_name = local.app_name
vpc_id = data.aws_vpc.main.id
private_subnet_ids = data.aws_subnets.private.ids
allowed_security_group_ids = [module.ecs_service.security_group_id]
instance_class = local.rds_instance_class
db_name = local.rds_db_name
db_username = local.rds_username
db_password = data.aws_ssm_parameter.db_password.value
tags = local.tags
}
resource "aws_ssm_parameter" "database_url" {
name = "/${local.app_name}/database-url"
type = "SecureString"
value = "postgresql://${local.rds_username}:${data.aws_ssm_parameter.db_password.value}@${module.rds.db_host}:5432/${local.rds_db_name}?sslmode=require"
tags = local.tags
}The app reaches its bucket through a policy on the task role, scoped to that one bucket.
resource "aws_iam_role_policy" "task_s3" {
name = "${local.app_name}-task-s3"
role = module.ecs_service.task_role_name
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket"]
Resource = [
module.s3_uploads.bucket_arn,
"${module.s3_uploads.bucket_arn}/*",
]
}]
})
}Before the first plan
Store the database password at /my-app/db-password in SSM, and replace ACCOUNT_ID and CERTIFICATE_ID in locals.tf with the real certificate ARN.