From 0715ec300ab7a97b47efb6e6b30993653eee57e8 Mon Sep 17 00:00:00 2001 From: Alexandros Koufatzis Date: Fri, 7 Jun 2024 13:12:10 +0300 Subject: [PATCH] Simplify terraform setup to allow easier bootstrap of new projects --- terraform/deployment/.terraform.lock.hcl | 77 +-- terraform/deployment/main.tf | 640 ++---------------- terraform/deployment/modules/base/main.tf | 306 +++++++++ .../deployment/modules/base/variables.tf | 33 + terraform/deployment/modules/ecs/main.tf | 27 +- terraform/deployment/modules/vpc/main.tf | 34 +- terraform/deployment/variables.tf | 11 +- 7 files changed, 468 insertions(+), 660 deletions(-) create mode 100644 terraform/deployment/modules/base/main.tf create mode 100644 terraform/deployment/modules/base/variables.tf diff --git a/terraform/deployment/.terraform.lock.hcl b/terraform/deployment/.terraform.lock.hcl index ba8d204..c54bf20 100644 --- a/terraform/deployment/.terraform.lock.hcl +++ b/terraform/deployment/.terraform.lock.hcl @@ -2,61 +2,44 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "4.40.0" - constraints = "~> 4.40.0" + version = "5.53.0" + constraints = ">= 4.30.0, ~> 5.0" hashes = [ - "h1:wZ0mPxigFhz6C+0YUzI5vecGwya1PqlCGTSr6giqjvg=", - "zh:04ca7287b7f5a2a310b60308cc08df11e97714d32d1a10c34a94454d330af66e", - "zh:13c28ba9b324c526580783a3807007a296ce58c607c7bdc94ae2bb72b35b6495", - "zh:2c84dbc0701b9724802f7343f916f50b6914a044dfbfc6654f264c9347f02dac", - "zh:33255a22e1d1ecec2ad8ccfec1e4a54dc33a8d71f3edad098c25d822958a138b", - "zh:4583b5e92b8de3662c8d8ff8a6527572ec23ad8c64dd686ff9dd528bc6934a4f", - "zh:4a9f502c0b8abe45abda846e0601f8d8ef582e62e0b92cb747b4200a711ba739", - "zh:558959e19935ec5e7f0647e900fc8561f4961a377be0178496a6495805136721", - "zh:6b3dc4b034d34885db620d73c75d3bb9abeee539e61ca9d0670fb995353e165d", - "zh:72f0dac5dbba355bce88599ded2baabc7d109ee786b89c6648ae720cb00a4bbf", - "zh:77981b87e2bcbb278402e8ff863d5e50aafbdc03629d7a57273c06989884a22f", + "h1:ucNFgeMRknvGjwQrVf6FzR9I5kYpFxEl3F0MeVgloBw=", + "zh:2adad39412111d19a5195474d6b95577fc25ccf06d88a90019bee0efba33a1e3", + "zh:51226453a14f95b0d1163cfecafc9cf1a92ce5f66e42e6b4065d83a813836a2c", + "zh:62450fadb56db9c18d50bb8b7728a3d009be608d7ee0d4fe95c85ccb521dff83", + "zh:6f3ad977a9cc4800847c136690b1c0a0fd8437705062163d29dc4e9429598950", + "zh:71ca0a16b735b8d34b7127dd7d1e1e5d1eaac9c9f792e08abde291b5beb947d5", + "zh:7ae9cf4838eea80288305be0a3e69b39ffff86ede7b4319be421f06d32d04fb6", + "zh:93abc2db5ad995cfee014eb7446abc7caedc427e141d375a11993e6e199076b5", + "zh:9560b3424d97da804e98ee86b474b7370afefa09baf350cae7f33afb3f1aa209", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:c5b4dd61558a4887a23847d23cd3b41a97ad03a9f3624d0687cb5461fee514b0", - "zh:c8949bc6600ec10ea5c0abdd4c1ffee8f82519c0cda8cc7a651e6258960e6249", - "zh:d1c88ab98f126d65cd0c7b6c9e1d06d59e766217ae374d5a908052817e3692a3", - "zh:ff2e921440bcbfd440ef84f5127ba881c930b2b70773e725de35c0fa3baddc4b", + "zh:9eb57a9b649c217ac4eeb27af2a1935c18bd9bc8fb1be07434e7de74729eff46", + "zh:b5f32dcbe71ea22c2090eeeaec9af3e098d7b8c3e4491f34ffdfdc6f1c1abf81", + "zh:c9fbd5417f266c773055178e87bb4091df7f0542b72bf5ad0a4ae27045a2b7ca", + "zh:d518b3c52c8a9f79769dbe1b3683d25b4cdc8bfc77a3b3cd9c85f74e6c7383e1", + "zh:db741be21f32404bb87d73d25b1b7fd9b813b00aeb20a130ed8806d44dc26680", + "zh:ed1a8bb4d08653d87265ae534d6fc33bbdabae1608692a1ee364fce03548d36c", ] } provider "registry.terraform.io/hashicorp/random" { - version = "3.4.3" + version = "3.6.2" constraints = ">= 2.2.0" hashes = [ - "h1:tL3katm68lX+4lAncjQA9AXL4GR/VM+RPwqYf4D2X8Q=", - "zh:41c53ba47085d8261590990f8633c8906696fa0a3c4b384ff6a7ecbf84339752", - "zh:59d98081c4475f2ad77d881c4412c5129c56214892f490adf11c7e7a5a47de9b", - "zh:686ad1ee40b812b9e016317e7f34c0d63ef837e084dea4a1f578f64a6314ad53", + "h1:VavG5unYCa3SYISMKF9pzc3718M0bhPlcbUZZGl7wuo=", + "zh:0ef01a4f81147b32c1bea3429974d4d104bbc4be2ba3cfa667031a8183ef88ec", + "zh:1bcd2d8161e89e39886119965ef0f37fcce2da9c1aca34263dd3002ba05fcb53", + "zh:37c75d15e9514556a5f4ed02e1548aaa95c0ecd6ff9af1119ac905144c70c114", + "zh:4210550a767226976bc7e57d988b9ce48f4411fa8a60cd74a6b246baf7589dad", + "zh:562007382520cd4baa7320f35e1370ffe84e46ed4e2071fdc7e4b1a9b1f8ae9b", + "zh:5efb9da90f665e43f22c2e13e0ce48e86cae2d960aaf1abf721b497f32025916", + "zh:6f71257a6b1218d02a573fc9bff0657410404fb2ef23bc66ae8cd968f98d5ff6", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:84103eae7251384c0d995f5a257c72b0096605048f757b749b7b62107a5dccb3", - "zh:8ee974b110adb78c7cd18aae82b2729e5124d8f115d484215fd5199451053de5", - "zh:9dd4561e3c847e45de603f17fa0c01ae14cae8c4b7b4e6423c9ef3904b308dda", - "zh:bb07bb3c2c0296beba0beec629ebc6474c70732387477a65966483b5efabdbc6", - "zh:e891339e96c9e5a888727b45b2e1bb3fcbdfe0fd7c5b4396e4695459b38c8cb1", - "zh:ea4739860c24dfeaac6c100b2a2e357106a89d18751f7693f3c31ecf6a996f8d", - "zh:f0c76ac303fd0ab59146c39bc121c5d7d86f878e9a69294e29444d4c653786f8", - "zh:f143a9a5af42b38fed328a161279906759ff39ac428ebcfe55606e05e1518b93", - ] -} - -provider "registry.terraform.io/hashicorp/template" { - version = "2.2.0" - hashes = [ - "h1:0wlehNaxBX7GJQnPfQwTNvvAf38Jm0Nv7ssKGMaG6Og=", - "zh:01702196f0a0492ec07917db7aaa595843d8f171dc195f4c988d2ffca2a06386", - "zh:09aae3da826ba3d7df69efeb25d146a1de0d03e951d35019a0f80e4f58c89b53", - "zh:09ba83c0625b6fe0a954da6fbd0c355ac0b7f07f86c91a2a97849140fea49603", - "zh:0e3a6c8e16f17f19010accd0844187d524580d9fdb0731f675ffcf4afba03d16", - "zh:45f2c594b6f2f34ea663704cc72048b212fe7d16fb4cfd959365fa997228a776", - "zh:77ea3e5a0446784d77114b5e851c970a3dde1e08fa6de38210b8385d7605d451", - "zh:8a154388f3708e3df5a69122a23bdfaf760a523788a5081976b3d5616f7d30ae", - "zh:992843002f2db5a11e626b3fc23dc0c87ad3729b3b3cff08e32ffb3df97edbde", - "zh:ad906f4cebd3ec5e43d5cd6dc8f4c5c9cc3b33d2243c89c5fc18f97f7277b51d", - "zh:c979425ddb256511137ecd093e23283234da0154b7fa8b21c2687182d9aea8b2", + "zh:9647e18f221380a85f2f0ab387c68fdafd58af6193a932417299cdcae4710150", + "zh:bb6297ce412c3c2fa9fec726114e5e0508dd2638cad6a0cb433194930c97a544", + "zh:f83e925ed73ff8a5ef6e3608ad9225baa5376446349572c2449c0c0b3cf184b7", + "zh:fbef0781cb64de76b1df1ca11078aecba7800d82fd4a956302734999cfd9a4af", ] } diff --git a/terraform/deployment/main.tf b/terraform/deployment/main.tf index 99ba596..260148c 100644 --- a/terraform/deployment/main.tf +++ b/terraform/deployment/main.tf @@ -1,14 +1,3 @@ -locals { - autoscaling_settings = { - max_capacity = 5 - min_capacity = 1 - target_cpu_value = 60 - target_memory_value = 80 - scale_in_cooldown = 60 - scale_out_cooldown = 900 - } -} - provider "aws" { shared_credentials_files = ["$HOME/.aws/credentials"] # profile = var.aws_profile @@ -18,567 +7,78 @@ provider "aws" { # } } -resource "aws_ecs_cluster" "main" { - name = "server-benchmarks" - tags = { - Name = "server-benchmarks-ecs-cluster" - } -} +module "ecs_apps" { + source = "./modules/base" -module "vpc" { - source = "./modules/vpc" - vpc_cidr = var.cidr_block -} + aws_region = var.aws_region -module "alb_sg" { - source = "./modules/security" - sg_name = "load-balancer-security-group" - description = "controls access to the ALB" - vpc_id = module.vpc.vpc_id - egress_cidr_rules = { - 1 = { - description = "allow all outbound" - protocol = "-1" - from_port = 0 - to_port = 0 - cidr_blocks = ["0.0.0.0/0"] - ipv6_cidr_blocks = ["::/0"] - } - } - egress_source_sg_rules = {} - ingress_cidr_rules = { - 1 = { - description = "controls access to the ALB" - protocol = "tcp" - from_port = 80 - to_port = 80 - cidr_blocks = ["0.0.0.0/0"] - ipv6_cidr_blocks = ["::/0"] - } - } - ingress_source_sg_rules = {} -} - -module "public_alb" { - source = "./modules/alb" - load_balancer_type = "application" - alb_name = "main-ecs-lb" - internal = false - vpc_id = module.vpc.vpc_id - security_groups = [module.alb_sg.security_group_id] - subnet_ids = module.vpc.public_subnet_ids - http_tcp_listeners = [ + ecs_apps_configs = [ + { + target_group = "quarkus-tg" + target_group_paths = ["/quarkus/*"] + name = "bookstore-quarkus-reactive" + image = "bookstore-quarkus-reactive" + host_port = 3000 + container_port = 3000 + container_name = "bookstore-quarkus-reactive" + health_check_path = "/quarkus/q/health" + }, { - port = 80 - protocol = "HTTP" - action_type = "fixed-response" - fixed_response = { - content_type = "text/plain" - message_body = "Resource not found" - status_code = "404" - } + target_group = "quarkus-sync-tg" + target_group_paths = ["/quarkus-sync/*"] + name = "bookstore-quarkus-sync" + image = "bookstore-quarkus-sync" + host_port = 3000 + container_port = 3000 + container_name = "bookstore-quarkus-sync" + health_check_path = "/quarkus-sync/q/health" + }, + { + target_group = "springboot-tg" + target_group_paths = ["/springboot/*"] + name = "bookstore-springboot" + image = "bookstore-springboot" + host_port = 3000 + container_port = 3000 + container_name = "bookstore-springboot" + health_check_path = "/springboot/actuator/health" + # Only if extra env vars are needed + env_vars = [ + { + "name" : "SPRING_PROFILES_ACTIVE", + "value" : "prod" + } + ] + }, + { + target_group = "nestjs-tg" + target_group_paths = ["/nestjs/*"] + name = "bookstore-nestjs" + image = "bookstore-nestjs" + host_port = 3000 + container_port = 3000 + container_name = "bookstore-nestjs" + health_check_path = "/nestjs/health" + env_vars = [ + { + "name" : "APP_PORT", + "value" : "3000" + }, + { + "name" : "USE_FASTIFY", + "value" : "true" + } + ] + }, + { + target_group = "actix-tg" + target_group_paths = ["/actix/*"] + name = "bookstore-actix" + image = "bookstore-actix" + host_port = 3000 + container_port = 3000 + container_name = "bookstore-actix" + health_check_path = "/actix/a/health" } ] -} - -################################################################################ -# ECS Tasks Execution IAM -################################################################################ -# ECS task execution role data -data "aws_iam_policy_document" "ecs_task_execution_role" { - version = "2012-10-17" - statement { - sid = "" - effect = "Allow" - actions = ["sts:AssumeRole"] - - principals { - type = "Service" - identifiers = ["ecs-tasks.amazonaws.com"] - } - } -} - -# ECS task execution role -resource "aws_iam_role" "ecs_task_execution_role" { - name = "ecsTaskExecutionRole" - assume_role_policy = data.aws_iam_policy_document.ecs_task_execution_role.json -} - -# ECS task execution role policy attachment -resource "aws_iam_role_policy_attachment" "ecs_task_execution_role" { - role = aws_iam_role.ecs_task_execution_role.name - policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" -} - -module "ecs_tasks_sg" { - source = "./modules/security" - sg_name = "ecs-tasks-security-group" - description = "controls access to the ECS tasks" - vpc_id = module.vpc.vpc_id - egress_cidr_rules = { - 1 = { - description = "allow all outbound" - protocol = "-1" - from_port = 0 - to_port = 0 - cidr_blocks = ["0.0.0.0/0"] - ipv6_cidr_blocks = ["::/0"] - } - } - egress_source_sg_rules = {} - ingress_source_sg_rules = { - 1 = { - description = "allow inbound access from the ALB only" - protocol = "-1" - from_port = 0 - to_port = 0 - source_security_group_id = module.alb_sg.security_group_id - } - } - ingress_cidr_rules = {} -} - -data "aws_ecr_repository" "quarkus_repository" { - name = "bookstore-quarkus-reactive" -} - -module "ecs_quarkus_app" { - source = "./modules/ecs" - alb_listener = module.public_alb.alb_listener - alb = { - target_group = "quarkus-tg" - target_group_paths = ["/quarkus/*"] - arn = module.public_alb.alb_listener_http_tcp_arn - rule_priority = 1 - } - aws_region = var.aws_region - cluster_id = aws_ecs_cluster.main.id - cluster_name = aws_ecs_cluster.main.name - fargate_cpu = "1024" - fargate_memory = "2048" - iam_role_ecs_task_execution_role = aws_iam_role.ecs_task_execution_role - iam_role_policy_ecs_task_execution_role = aws_iam_role_policy_attachment.ecs_task_execution_role - logs_retention_in_days = 30 - service_security_groups_ids = [module.ecs_tasks_sg.security_group_id] - subnet_ids = module.vpc.private_subnet_ids - vpc_id = module.vpc.vpc_id - service = { - name = "bookstore-quarkus-reactive" - desired_count = 1 - max_count = 1 - } - task_definition = { - name = "bookstore-quarkus-reactive" - image = "${data.aws_ecr_repository.quarkus_repository.repository_url}:${var.image_tag}" - aws_logs_group = "ecs/bookstore-quarkus-reactive" - host_port = 3000 - container_port = 3000 - container_name = "bookstore-quarkus-reactive" - health_check_path = "/quarkus/q/health" - family = "bookstore-quarkus-reactive-task" - env_vars = [ - # Check how to configure writer and reader endpoints - { - "name" : "DB_HOST", - "value" : tostring(module.books-database.db_endpoint), - }, - { - "name" : "DB_NAME", - "value" : tostring(module.books-database.db_name), - }, - { - "name" : "DB_PORT", - "value" : tostring(module.books-database.db_port), - } - ] - secret_vars = [ - { - "name" : "DB_USER", - "valueFrom" : module.database_secrets.db_username_secret_arn, - }, - { - "name" : "DB_PASSWORD", - "valueFrom" : module.database_secrets.db_password_secret_arn, - } - ] - } -} - -data "aws_ecr_repository" "quarkus_sync_repository" { - name = "bookstore-quarkus-sync" -} - -module "ecs_quarkus_sync_app" { - source = "./modules/ecs" - alb_listener = module.public_alb.alb_listener - alb = { - target_group = "quarkus-sync-tg" - target_group_paths = ["/quarkus-sync/*"] - arn = module.public_alb.alb_listener_http_tcp_arn - rule_priority = 5 - } - aws_region = var.aws_region - cluster_id = aws_ecs_cluster.main.id - cluster_name = aws_ecs_cluster.main.name - fargate_cpu = "1024" - fargate_memory = "2048" - iam_role_ecs_task_execution_role = aws_iam_role.ecs_task_execution_role - iam_role_policy_ecs_task_execution_role = aws_iam_role_policy_attachment.ecs_task_execution_role - logs_retention_in_days = 30 - service_security_groups_ids = [module.ecs_tasks_sg.security_group_id] - subnet_ids = module.vpc.private_subnet_ids - vpc_id = module.vpc.vpc_id - service = { - name = "bookstore-quarkus-sync" - desired_count = 1 - max_count = 1 - } - task_definition = { - name = "bookstore-quarkus-sync" - image = "${data.aws_ecr_repository.quarkus_sync_repository.repository_url}:${var.image_tag}" - aws_logs_group = "ecs/bookstore-quarkus-sync" - host_port = 3000 - container_port = 3000 - container_name = "bookstore-quarkus-sync" - health_check_path = "/quarkus-sync/q/health" - family = "bookstore-quarkus-sync-task" - env_vars = [ - # Check how to configure writer and reader endpoints - { - "name" : "DB_HOST", - "value" : tostring(module.books-database.db_endpoint), - }, - { - "name" : "DB_NAME", - "value" : tostring(module.books-database.db_name), - }, - { - "name" : "DB_PORT", - "value" : tostring(module.books-database.db_port), - } - ] - secret_vars = [ - { - "name" : "DB_USER", - "valueFrom" : module.database_secrets.db_username_secret_arn, - }, - { - "name" : "DB_PASSWORD", - "valueFrom" : module.database_secrets.db_password_secret_arn, - } - ] - } -} - -data "aws_ecr_repository" "springboot_repository" { - name = "bookstore-springboot" -} - -module "ecs_springboot_app" { - source = "./modules/ecs" - alb_listener = module.public_alb.alb_listener - alb = { - target_group = "springboot-tg" - target_group_paths = ["/springboot/*"] - arn = module.public_alb.alb_listener_http_tcp_arn - rule_priority = 2 - } - aws_region = var.aws_region - cluster_id = aws_ecs_cluster.main.id - cluster_name = aws_ecs_cluster.main.name - fargate_cpu = "1024" - fargate_memory = "2048" - iam_role_ecs_task_execution_role = aws_iam_role.ecs_task_execution_role - iam_role_policy_ecs_task_execution_role = aws_iam_role_policy_attachment.ecs_task_execution_role - logs_retention_in_days = 30 - service_security_groups_ids = [module.ecs_tasks_sg.security_group_id] - subnet_ids = module.vpc.private_subnet_ids - vpc_id = module.vpc.vpc_id - service = { - name = "bookstore-springboot" - desired_count = 1 - max_count = 1 - } - task_definition = { - name = "bookstore-springboot" - image = "${data.aws_ecr_repository.springboot_repository.repository_url}:${var.image_tag}" - aws_logs_group = "ecs/bookstore-springboot" - host_port = 3000 - container_port = 3000 - container_name = "bookstore-springboot" - health_check_path = "/springboot/actuator/health" - family = "bookstore-springboot-task" - env_vars = [ - # Check how to configure writer and reader endpoints - { - "name" : "DB_HOST", - "value" : tostring(module.books-database.db_endpoint), - }, - { - "name" : "DB_NAME", - "value" : tostring(module.books-database.db_name), - }, - { - "name" : "DB_PORT", - "value" : tostring(module.books-database.db_port), - }, - { - "name" : "SPRING_PROFILES_ACTIVE", - "value" : "prod" - } - ] - secret_vars = [ - { - "name" : "DB_USER", - "valueFrom" : module.database_secrets.db_username_secret_arn, - }, - { - "name" : "DB_PASSWORD", - "valueFrom" : module.database_secrets.db_password_secret_arn, - } - ] - } -} - -data "aws_ecr_repository" "nestjs_repository" { - name = "bookstore-nestjs" -} - -module "ecs_nestjs_app" { - source = "./modules/ecs" - alb_listener = module.public_alb.alb_listener - alb = { - target_group = "nestjs-tg" - target_group_paths = ["/nestjs/*"] - arn = module.public_alb.alb_listener_http_tcp_arn - rule_priority = 3 - } - aws_region = var.aws_region - cluster_id = aws_ecs_cluster.main.id - cluster_name = aws_ecs_cluster.main.name - fargate_cpu = "1024" - fargate_memory = "2048" - iam_role_ecs_task_execution_role = aws_iam_role.ecs_task_execution_role - iam_role_policy_ecs_task_execution_role = aws_iam_role_policy_attachment.ecs_task_execution_role - logs_retention_in_days = 30 - service_security_groups_ids = [module.ecs_tasks_sg.security_group_id] - subnet_ids = module.vpc.private_subnet_ids - vpc_id = module.vpc.vpc_id - service = { - name = "bookstore-nestjs" - desired_count = 1 - max_count = 1 - } - task_definition = { - name = "bookstore-nestjs" - image = "${data.aws_ecr_repository.nestjs_repository.repository_url}:${var.image_tag}" - aws_logs_group = "ecs/bookstore-nestjs" - host_port = 3000 - container_port = 3000 - container_name = "bookstore-nestjs" - health_check_path = "/nestjs/health" - family = "bookstore-nestjs-task" - env_vars = [ - # Check how to configure writer and reader endpoints - { - "name" : "DB_HOST", - "value" : tostring(module.books-database.db_endpoint), - }, - { - "name" : "DB_NAME", - "value" : tostring(module.books-database.db_name), - }, - { - "name" : "DB_PORT", - "value" : tostring(module.books-database.db_port), - }, - { - "name" : "APP_PORT", - "value" : "3000" - }, - { - "name" : "USE_FASTIFY", - "value" : "true" - } - ] - secret_vars = [ - { - "name" : "DB_USER", - "valueFrom" : module.database_secrets.db_username_secret_arn, - }, - { - "name" : "DB_PASSWORD", - "valueFrom" : module.database_secrets.db_password_secret_arn, - } - ] - } -} - -data "aws_ecr_repository" "actix_repository" { - name = "bookstore-actix" -} - - -module "ecs_actix_app" { - source = "./modules/ecs" - alb_listener = module.public_alb.alb_listener - alb = { - target_group = "actix-tg" - target_group_paths = ["/actix/*"] - arn = module.public_alb.alb_listener_http_tcp_arn - rule_priority = 4 - } - aws_region = var.aws_region - cluster_id = aws_ecs_cluster.main.id - cluster_name = aws_ecs_cluster.main.name - fargate_cpu = "1024" - fargate_memory = "2048" - iam_role_ecs_task_execution_role = aws_iam_role.ecs_task_execution_role - iam_role_policy_ecs_task_execution_role = aws_iam_role_policy_attachment.ecs_task_execution_role - logs_retention_in_days = 30 - service_security_groups_ids = [module.ecs_tasks_sg.security_group_id] - subnet_ids = module.vpc.private_subnet_ids - vpc_id = module.vpc.vpc_id - service = { - name = "bookstore-actix" - desired_count = 1 - max_count = 1 - } - task_definition = { - name = "bookstore-actix" - image = "${data.aws_ecr_repository.actix_repository.repository_url}:${var.image_tag}" - aws_logs_group = "ecs/bookstore-actix" - host_port = 3000 - container_port = 3000 - container_name = "bookstore-actix" - health_check_path = "/actix/a/health" - family = "bookstore-actix-task" - env_vars = [ - # Check how to configure writer and reader endpoints - { - "name" : "DB_HOST", - "value" : tostring(module.books-database.db_endpoint), - }, - { - "name" : "DB_NAME", - "value" : tostring(module.books-database.db_name), - }, - { - "name" : "DB_PORT", - "value" : tostring(module.books-database.db_port), - } - ] - secret_vars = [ - { - "name" : "DB_USER", - "valueFrom" : module.database_secrets.db_username_secret_arn, - }, - { - "name" : "DB_PASSWORD", - "valueFrom" : module.database_secrets.db_password_secret_arn, - } - ] - } -} - -################################################################################ -# Database -################################################################################ - -module "database_secrets" { - source = "./modules/secrets" - database_password_key = "booksdb_password" - database_username_key = "booksdb_username" - role_id = aws_iam_role.ecs_task_execution_role.id -} - -module "private_database_sg" { - source = "./modules/security" - sg_name = "private_database_sg" - description = "Controls access to the private database (not internet facing)" - vpc_id = module.vpc.vpc_id - egress_cidr_rules = { - 1 = { - description = "allow all outbound" - protocol = "-1" - from_port = 0 - to_port = 0 - cidr_blocks = ["0.0.0.0/0"] - ipv6_cidr_blocks = ["::/0"] - } - } - egress_source_sg_rules = {} - ingress_source_sg_rules = {} - ingress_cidr_rules = { - 1 = { - description = "allow inbound access only from resources in VPC" - protocol = "-1" - from_port = 0 - to_port = 0 - cidr_blocks = [module.vpc.vpc_cidr_block] - ipv6_cidr_blocks = [module.vpc.vpc_ipv6_cidr_block] - } - } -} - -module "books-database" { - source = "./modules/db" - aws_region = var.aws_region - name = "booksdb" - database_name = "booksdb" - subnet_ids = module.vpc.private_subnet_ids - security_groups = [module.private_database_sg.security_group_id] - vpc_id = module.vpc.vpc_id - database_password = module.database_secrets.db_password_secret_value - database_username = module.database_secrets.db_username_secret_value -} - -################################################################################ -# VPC Flow Logs IAM -################################################################################ -resource "aws_iam_role" "vpc_flow_cloudwatch_logs_role" { - name = "vpc-flow-cloudwatch-logs-role" - assume_role_policy = file("./common/templates/policies/vpc_flow_cloudwatch_logs_role.json.tpl") -} - -resource "aws_iam_role_policy" "vpc_flow_cloudwatch_logs_policy" { - name = "vpc-flow-cloudwatch-logs-policy" - role = aws_iam_role.vpc_flow_cloudwatch_logs_role.id - policy = file("./common/templates/policies/vpc_flow_cloudwatch_logs_policy.json.tpl") -} - -# VPC Flows -################################################################################ -# Provides a VPC/Subnet/ENI Flow Log to capture IP traffic for a specific network interface, -# subnet, or VPC. Logs are sent to a CloudWatch Log Group or a S3 Bucket. -# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/flow_log -resource "aws_flow_log" "vpc_flow_logs" { - iam_role_arn = aws_iam_role.vpc_flow_cloudwatch_logs_role.arn - log_destination = aws_cloudwatch_log_group.vpc_flow_logs.arn - traffic_type = "ALL" - vpc_id = module.vpc.vpc_id -} - -resource "aws_cloudwatch_log_group" "vpc_flow_logs" { - name = "bookstore-vpc-flow-logs" - retention_in_days = 30 -} - -# Route 53 - -data "aws_route53_zone" "selected" { - name = "server-benchmarks.com" -} - -resource "aws_route53_record" "www" { - zone_id = data.aws_route53_zone.selected.zone_id - name = "server-benchmarks.com" - type = "A" - alias { - name = module.public_alb.alb_dns_name - zone_id = module.public_alb.zone_id - evaluate_target_health = true - } - allow_overwrite = true -} +} \ No newline at end of file diff --git a/terraform/deployment/modules/base/main.tf b/terraform/deployment/modules/base/main.tf new file mode 100644 index 0000000..f167f4f --- /dev/null +++ b/terraform/deployment/modules/base/main.tf @@ -0,0 +1,306 @@ +locals { + autoscaling_settings = { + max_capacity = 5 + min_capacity = 1 + target_cpu_value = 60 + target_memory_value = 80 + scale_in_cooldown = 60 + scale_out_cooldown = 900 + } +} + +resource "aws_ecs_cluster" "main" { + name = "server-benchmarks" + tags = { + Name = "server-benchmarks-ecs-cluster" + } +} + +module "vpc" { + source = "../vpc" + vpc_cidr = var.cidr_block +} + +module "alb_sg" { + source = "../security" + sg_name = "load-balancer-security-group" + description = "controls access to the ALB" + vpc_id = module.vpc.vpc_id + egress_cidr_rules = { + 1 = { + description = "allow all outbound" + protocol = "-1" + from_port = 0 + to_port = 0 + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + } + } + egress_source_sg_rules = {} + ingress_cidr_rules = { + 1 = { + description = "controls access to the ALB" + protocol = "tcp" + from_port = 80 + to_port = 80 + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + } + } + ingress_source_sg_rules = {} +} + +module "public_alb" { + source = "../alb" + load_balancer_type = "application" + alb_name = "main-ecs-lb" + internal = false + vpc_id = module.vpc.vpc_id + security_groups = [module.alb_sg.security_group_id] + subnet_ids = module.vpc.public_subnet_ids + http_tcp_listeners = [ + { + port = 80 + protocol = "HTTP" + action_type = "fixed-response" + fixed_response = { + content_type = "text/plain" + message_body = "Resource not found" + status_code = "404" + } + } + ] +} + +################################################################################ +# ECS Tasks Execution IAM +################################################################################ +# ECS task execution role data +data "aws_iam_policy_document" "ecs_task_execution_role" { + version = "2012-10-17" + statement { + sid = "" + effect = "Allow" + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + } +} + +# ECS task execution role +resource "aws_iam_role" "ecs_task_execution_role" { + name = "ecsTaskExecutionRole" + assume_role_policy = data.aws_iam_policy_document.ecs_task_execution_role.json +} + +# ECS task execution role policy attachment +resource "aws_iam_role_policy_attachment" "ecs_task_execution_role" { + role = aws_iam_role.ecs_task_execution_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +module "ecs_tasks_sg" { + source = "../security" + sg_name = "ecs-tasks-security-group" + description = "controls access to the ECS tasks" + vpc_id = module.vpc.vpc_id + egress_cidr_rules = { + 1 = { + description = "allow all outbound" + protocol = "-1" + from_port = 0 + to_port = 0 + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + } + } + egress_source_sg_rules = {} + ingress_source_sg_rules = { + 1 = { + description = "allow inbound access from the ALB only" + protocol = "-1" + from_port = 0 + to_port = 0 + source_security_group_id = module.alb_sg.security_group_id + } + } + ingress_cidr_rules = {} +} + +# Define data source for ECR repositories +data "aws_ecr_repository" "repository" { + for_each = {for idx, app in var.ecs_apps_configs : idx => app.image} + + name = each.value +} + +# Using for_each to iterate over ecs_apps_configs +module "ecs_apps" { + source = "../ecs" + + for_each = {for idx, app in var.ecs_apps_configs : idx => app} + + alb_listener = module.public_alb.alb_listener + alb = { + target_group = each.value.target_group + target_group_paths = each.value.target_group_paths + arn = module.public_alb.alb_listener_http_tcp_arn + # rule priority has to start from 1 + rule_priority = each.key + 1 + } + aws_region = var.aws_region + cluster_id = aws_ecs_cluster.main.id + cluster_name = aws_ecs_cluster.main.name + fargate_cpu = "1024" + fargate_memory = "2048" + iam_role_ecs_task_execution_role = aws_iam_role.ecs_task_execution_role + iam_role_policy_ecs_task_execution_role = aws_iam_role_policy_attachment.ecs_task_execution_role + logs_retention_in_days = 30 + service_security_groups_ids = [module.ecs_tasks_sg.security_group_id] + subnet_ids = module.vpc.private_subnet_ids + vpc_id = module.vpc.vpc_id + service = { + name = each.value.name + desired_count = 1 + max_count = 1 + } + task_definition = { + name = each.value.name + image = "${data.aws_ecr_repository.repository[each.key].repository_url}:${each.value.image_tag}" + aws_logs_group = "ecs/${each.value.name}" + host_port = 3000 + container_port = 3000 + container_name = each.value.container_name + health_check_path = each.value.health_check_path + family = "${each.value.name}-task" + env_vars = concat(each.value.env_vars, [ + # Check how to configure writer and reader endpoints + { + name : "DB_HOST", + value : tostring(module.books-database.db_endpoint), + }, + { + name : "DB_NAME", + value : tostring(module.books-database.db_name), + }, + { + name : "DB_PORT", + value : tostring(module.books-database.db_port), + } + ]) + secret_vars = [ + { + "name" : "DB_USER", + "valueFrom" : module.database_secrets.db_username_secret_arn, + }, + { + "name" : "DB_PASSWORD", + "valueFrom" : module.database_secrets.db_password_secret_arn, + } + ] + } +} + +################################################################################ +# Database +################################################################################ + +module "database_secrets" { + source = "../secrets" + database_password_key = "booksdb_password" + database_username_key = "booksdb_username" + role_id = aws_iam_role.ecs_task_execution_role.id +} + +module "private_database_sg" { + source = "../security" + sg_name = "private_database_sg" + description = "Controls access to the private database (not internet facing)" + vpc_id = module.vpc.vpc_id + egress_cidr_rules = { + 1 = { + description = "allow all outbound" + protocol = "-1" + from_port = 0 + to_port = 0 + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + } + } + egress_source_sg_rules = {} + ingress_source_sg_rules = {} + ingress_cidr_rules = { + 1 = { + description = "allow inbound access only from resources in VPC" + protocol = "-1" + from_port = 0 + to_port = 0 + cidr_blocks = [module.vpc.vpc_cidr_block] + ipv6_cidr_blocks = [module.vpc.vpc_ipv6_cidr_block] + } + } +} + +module "books-database" { + source = "../db" + aws_region = var.aws_region + name = "booksdb" + database_name = "booksdb" + subnet_ids = module.vpc.private_subnet_ids + security_groups = [module.private_database_sg.security_group_id] + vpc_id = module.vpc.vpc_id + database_password = module.database_secrets.db_password_secret_value + database_username = module.database_secrets.db_username_secret_value +} + +################################################################################ +# VPC Flow Logs IAM +################################################################################ +resource "aws_iam_role" "vpc_flow_cloudwatch_logs_role" { + name = "vpc-flow-cloudwatch-logs-role" + assume_role_policy = file("./common/templates/policies/vpc_flow_cloudwatch_logs_role.json.tpl") +} + +resource "aws_iam_role_policy" "vpc_flow_cloudwatch_logs_policy" { + name = "vpc-flow-cloudwatch-logs-policy" + role = aws_iam_role.vpc_flow_cloudwatch_logs_role.id + policy = file("./common/templates/policies/vpc_flow_cloudwatch_logs_policy.json.tpl") +} + +# VPC Flows +################################################################################ +# Provides a VPC/Subnet/ENI Flow Log to capture IP traffic for a specific network interface, +# subnet, or VPC. Logs are sent to a CloudWatch Log Group or a S3 Bucket. +# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/flow_log +resource "aws_flow_log" "vpc_flow_logs" { + iam_role_arn = aws_iam_role.vpc_flow_cloudwatch_logs_role.arn + log_destination = aws_cloudwatch_log_group.vpc_flow_logs.arn + traffic_type = "ALL" + vpc_id = module.vpc.vpc_id +} + +resource "aws_cloudwatch_log_group" "vpc_flow_logs" { + name = "bookstore-vpc-flow-logs" + retention_in_days = 30 +} + +# Route 53 + +data "aws_route53_zone" "selected" { + name = "server-benchmarks.com" +} + +resource "aws_route53_record" "www" { + zone_id = data.aws_route53_zone.selected.zone_id + name = "server-benchmarks.com" + type = "A" + alias { + name = module.public_alb.alb_dns_name + zone_id = module.public_alb.zone_id + evaluate_target_health = true + } + allow_overwrite = true +} diff --git a/terraform/deployment/modules/base/variables.tf b/terraform/deployment/modules/base/variables.tf new file mode 100644 index 0000000..153e78a --- /dev/null +++ b/terraform/deployment/modules/base/variables.tf @@ -0,0 +1,33 @@ +variable "aws_region" { + description = "The AWS region things are created in" +} + +variable "cidr_block" { + description = "Network IP range" + default = "10.0.0.0/16" +} + +variable "image_tag" { + description = "Defines image tag used for all containers" + default = "latest" +} + +variable "ecs_apps_configs" { + description = "Configuration for running apps on AWS ECS" + type = list(object({ + target_group = string + target_group_paths = list(string) + name = string + image = string + image_tag = optional(string, "latest") + host_port = number + container_port = number + container_name = string + health_check_path = string + env_vars = optional(list(object({ + name = string + value = string + })), []) + })) + default = [] +} \ No newline at end of file diff --git a/terraform/deployment/modules/ecs/main.tf b/terraform/deployment/modules/ecs/main.tf index 8a63f28..6071427 100644 --- a/terraform/deployment/modules/ecs/main.tf +++ b/terraform/deployment/modules/ecs/main.tf @@ -8,9 +8,14 @@ resource "aws_cloudwatch_log_group" "this" { } } -data "template_file" "this" { - template = file("./common/templates/task.json.tpl") - vars = { +resource "aws_ecs_task_definition" "this" { + family = var.task_definition.family + execution_role_arn = var.iam_role_ecs_task_execution_role.arn + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = var.fargate_cpu + memory = var.fargate_memory + container_definitions = templatefile("./common/templates/task.json.tpl", { task_name = var.task_definition.name image = var.task_definition.image container_port = var.task_definition.container_port @@ -20,19 +25,9 @@ data "template_file" "this" { aws_region = var.aws_region aws_logs_group = var.task_definition.aws_logs_group network_mode = "awsvpc" - env_vars = jsonencode(var.task_definition.env_vars) - secret_vars = jsonencode(var.task_definition.secret_vars) - } -} - -resource "aws_ecs_task_definition" "this" { - family = var.task_definition.family - execution_role_arn = var.iam_role_ecs_task_execution_role.arn - network_mode = "awsvpc" - requires_compatibilities = ["FARGATE"] - cpu = var.fargate_cpu - memory = var.fargate_memory - container_definitions = data.template_file.this.rendered + env_vars = jsonencode(var.task_definition.env_vars) + secret_vars = jsonencode(var.task_definition.secret_vars) + }) } resource "aws_ecs_service" "this" { diff --git a/terraform/deployment/modules/vpc/main.tf b/terraform/deployment/modules/vpc/main.tf index 733aa04..21099ce 100644 --- a/terraform/deployment/modules/vpc/main.tf +++ b/terraform/deployment/modules/vpc/main.tf @@ -26,7 +26,7 @@ resource "aws_subnet" "public" { vpc_id = aws_vpc.main.id availability_zone = data.aws_availability_zones.available.names[count.index] - cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 4, count.index) + cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 4, count.index) ipv6_cidr_block = cidrsubnet(aws_vpc.main.ipv6_cidr_block, 8, count.index) tags = { @@ -57,12 +57,12 @@ resource "aws_subnet" "private" { resource "aws_eip" "nat" { count = var.private_subnet_count - vpc = true + domain = "vpc" tags = { - Name = "server-benchmarks-eip" - Role = "private" - VPC = aws_vpc.main.id + Name = "server-benchmarks-eip" + Role = "private" + VPC = aws_vpc.main.id Subnet = element(aws_subnet.public.*.id, count.index) } } @@ -71,12 +71,12 @@ resource "aws_nat_gateway" "ngw" { count = var.private_subnet_count allocation_id = element(aws_eip.nat.*.id, count.index) - subnet_id = element(aws_subnet.public.*.id, count.index) + subnet_id = element(aws_subnet.public.*.id, count.index) tags = { - Name = "server-benchmarks-ngw" - Role = "private" - VPC = aws_vpc.main.id + Name = "server-benchmarks-ngw" + Role = "private" + VPC = aws_vpc.main.id Subnet = element(aws_subnet.public.*.id, count.index) } @@ -101,7 +101,7 @@ resource "aws_route" "public" { resource "aws_route_table_association" "public" { count = var.public_subnet_count - subnet_id = element(aws_subnet.public.*.id, count.index) + subnet_id = element(aws_subnet.public.*.id, count.index) route_table_id = aws_route_table.public.id } @@ -111,23 +111,23 @@ resource "aws_route_table" "private" { vpc_id = aws_vpc.main.id tags = { - Name = "server-benchmarks-private-rt" - Role = "private" - VPC = aws_vpc.main.id + Name = "server-benchmarks-private-rt" + Role = "private" + VPC = aws_vpc.main.id Subnet = element(aws_subnet.private.*.id, count.index) } } resource "aws_route" "private" { count = var.private_subnet_count - route_table_id = element(aws_route_table.private.*.id, count.index) + route_table_id = element(aws_route_table.private.*.id, count.index) destination_cidr_block = "0.0.0.0/0" - nat_gateway_id = element(aws_nat_gateway.ngw.*.id, count.index) + nat_gateway_id = element(aws_nat_gateway.ngw.*.id, count.index) } # Explicitly associate the newly created route tables to the private subnets (so they don't default to the main route table) resource "aws_route_table_association" "private" { - count = var.private_subnet_count - subnet_id = element(aws_subnet.private.*.id, count.index) + count = var.private_subnet_count + subnet_id = element(aws_subnet.private.*.id, count.index) route_table_id = element(aws_route_table.private.*.id, count.index) } \ No newline at end of file diff --git a/terraform/deployment/variables.tf b/terraform/deployment/variables.tf index b6d4d5b..c88a948 100644 --- a/terraform/deployment/variables.tf +++ b/terraform/deployment/variables.tf @@ -1,6 +1,7 @@ ################################################################################ # General AWS Configuration ################################################################################ + variable "aws_region" { description = "The AWS region things are created in" default = "eu-west-1" @@ -9,14 +10,4 @@ variable "aws_region" { variable "aws_profile" { description = "The AWS profile name" default = "arconsis" -} - -variable "cidr_block" { - description = "Network IP range" - default = "10.0.0.0/16" -} - -variable "image_tag" { - description = "Defines image tag used for all containers" - default = "latest" } \ No newline at end of file