diff --git a/examples/ecs-service-discovery/.terraform.lock.hcl b/examples/ecs-service-discovery/.terraform.lock.hcl index 0a5bd22..3c9d488 100644 --- a/examples/ecs-service-discovery/.terraform.lock.hcl +++ b/examples/ecs-service-discovery/.terraform.lock.hcl @@ -2,88 +2,84 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "4.67.0" - constraints = "~> 4.0" + version = "5.90.1" + constraints = "~> 5.0" hashes = [ - "h1:P43vwcDPG99x5WBbmqwUPgfJrfXf6/ucAIbGlRb7k1w=", - "h1:dCRc4GqsyfqHEMjgtlM1EympBcgTmcTkWaJmtd91+KA=", - "zh:0843017ecc24385f2b45f2c5fce79dc25b258e50d516877b3affee3bef34f060", - "zh:19876066cfa60de91834ec569a6448dab8c2518b8a71b5ca870b2444febddac6", - "zh:24995686b2ad88c1ffaa242e36eee791fc6070e6144f418048c4ce24d0ba5183", - "zh:4a002990b9f4d6d225d82cb2fb8805789ffef791999ee5d9cb1fef579aeff8f1", - "zh:559a2b5ace06b878c6de3ecf19b94fbae3512562f7a51e930674b16c2f606e29", - "zh:6a07da13b86b9753b95d4d8218f6dae874cf34699bca1470d6effbb4dee7f4b7", - "zh:768b3bfd126c3b77dc975c7c0e5db3207e4f9997cf41aa3385c63206242ba043", - "zh:7be5177e698d4b547083cc738b977742d70ed68487ce6f49ecd0c94dbf9d1362", - "zh:8b562a818915fb0d85959257095251a05c76f3467caa3ba95c583ba5fe043f9b", + "h1:UOHmE27LtoyisllsyLEb+10+wpvHDgXBPYdMn5kKfdA=", + "zh:0d6179ec7c086049fd2f2d4cb2680edd238083ea7119e19c64921b2af395e1df", + "zh:208155e7d989941506deadabbd06159fac44fbddd557657b941d2ba0679ba9ce", + "zh:5adca7faa3c2031a9d6e8e3415c78160ae0bad6f568ef612b3201b12e29aa515", + "zh:81361ab0c3dee037d8e8a4dcf23d97d7392d7f00832b57d6c0fe8efbc8755589", + "zh:85b28e3ffe5e2f6a6640bf8ee544d6bb1dc1716d8c446927436ddd83b92ce0ae", + "zh:85e38aa50a4e16c1bf10eea7b35fac87fd0bc46cf8b68b7986f4041b69edf893", + "zh:9709b10fcb6e36c4347de1c26784209351865342a759f4b83a05e6e4e0db401f", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:9c385d03a958b54e2afd5279cd8c7cbdd2d6ca5c7d6a333e61092331f38af7cf", - "zh:b3ca45f2821a89af417787df8289cb4314b273d29555ad3b2a5ab98bb4816b3b", - "zh:da3c317f1db2469615ab40aa6baba63b5643bae7110ff855277a1fb9d8eb4f2c", - "zh:dc6430622a8dc5cdab359a8704aec81d3825ea1d305bbb3bbd032b1c6adfae0c", - "zh:fac0d2ddeadf9ec53da87922f666e1e73a603a611c57bcbc4b86ac2821619b1d", + "zh:a62d5b7c7af67a2e4866899a405e2f6b82e85fd808b294f3704bdcb20fd640ac", + "zh:a84e56d4b4707e70c4322736171493e69f227de016649e0261b121695b6dea21", + "zh:a8c493907a4e0cf4a57b29a8695a6ca517407ff0b68b3005e39ad682606307e8", + "zh:c1938aaccc47c0723ae58c4a3a29fc0e114debeb251d4cd1fe8914d09a373615", + "zh:d57a0c8084c0982ce4b0dcd39afe3c4d64a6ed9729d3599a765611e5fcab45e9", + "zh:f2f21480e0ea5b58a14d285428f539ceedc38e317904272f4b4917130c49e549", + "zh:f6880a07727ed6698c46e9634b7e0438ebae7a0365a6eb98586d3d6465ae10be", ] } provider "registry.terraform.io/hashicorp/cloudinit" { - version = "2.3.4" + version = "2.3.6" constraints = "~> 2.0" hashes = [ - "h1:/Ty/HXg0Bti5T+Zk6XvhwEHyKGiOV5LzCrbLiekjuLU=", - "h1:cVIIhnXweOHavu1uV2bdKScTjLbM1WnKM/25wqYBJWo=", - "zh:09f1f1e1d232da96fbf9513b0fb5263bc2fe9bee85697aa15d40bb93835efbeb", - "zh:381e74b90d7a038c3a8dcdcc2ce8c72d6b86da9f208a27f4b98cabe1a1032773", - "zh:398eb321949e28c4c5f7c52e9b1f922a10d0b2b073b7db04cb69318d24ffc5a9", - "zh:4a425679614a8f0fe440845828794e609b35af17db59134c4f9e56d61e979813", - "zh:4d955d8608ece4984c9f1dacda2a59fdb4ea6b0243872f049b388181aab8c80a", + "h1:afnqn3XPnO40laFt+SVHPPKsg1j3HXT0VAO0xBVvmrY=", + "zh:1321b5ddede56be3f9b35bf75d7cda79adcb357fad62eb8677b6595e0baaa6cd", + "zh:265d66e61b9cd16ca1182ebf094cc0a08fb3687e8193a1dbac6899b16c237151", + "zh:3875c3a20e082ac55d5ff24bcaf7133ebc90c7f999fd0fb37cf0f0003474c94c", + "zh:68ce41ccd07757c451682703840cae1ec270ed5275cd491bbf8279782dfcbb73", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:a48fbee1d58d55a1f4c92c2f38c83a37c8b2f2701ed1a3c926cefb0801fa446a", - "zh:b748fe6631b16a1dafd35a09377c3bffa89552af584cf95f47568b6cd31fc241", - "zh:d4b931f7a54603fa4692a2ec6e498b95464babd2be072bed5c7c2e140a280d99", - "zh:f1c9337fcfe3a7be39d179eb7986c22a979cfb2c587c05f1b3b83064f41785c5", - "zh:f58fc57edd1ee3250a28943cd84de3e4b744cdb52df0356a53403fc240240636", - "zh:f5f50de0923ff530b03e1bca0ac697534d61bb3e5fc7f60e13becb62229097a9", + "zh:8dca3bb3f85ff8ac4d1b3f93975dcb751ed788396c56ebf0c3737ce1a4c60492", + "zh:9339bdaa99939291cedf543861353c8e7171ec5231c0dfacaa9bdb3338978dab", + "zh:a8510c2256e9a78697910bb5542aeca457c81225ea88130335f6d14a36a36c74", + "zh:af7ed71b8fceb60a5e3b7fa663be171e0bd41bb0af30e0e1f06a004c7b584e4a", + "zh:bc9de0f921b69d07f5fc1ea65f8af71d8d1a7053aafb500788b30bfce64b8fbe", + "zh:bccd0a49f161a91660d7d30dd6b389e6820f29752ccf351f10a3297c96973823", + "zh:c69321caca20009abead617f888a67aca990276cb7388b738b19157b88749190", ] } provider "registry.terraform.io/hashicorp/random" { - version = "3.6.2" + version = "3.7.1" constraints = "~> 3.0" hashes = [ - "h1:R5qdQjKzOU16TziCN1vR3Exr/B+8WGK80glLTT4ZCPk=", - "h1:wmG0QFjQ2OfyPy6BB7mQ57WtoZZGGV07uAPQeDmIrAE=", - "zh:0ef01a4f81147b32c1bea3429974d4d104bbc4be2ba3cfa667031a8183ef88ec", - "zh:1bcd2d8161e89e39886119965ef0f37fcce2da9c1aca34263dd3002ba05fcb53", - "zh:37c75d15e9514556a5f4ed02e1548aaa95c0ecd6ff9af1119ac905144c70c114", - "zh:4210550a767226976bc7e57d988b9ce48f4411fa8a60cd74a6b246baf7589dad", - "zh:562007382520cd4baa7320f35e1370ffe84e46ed4e2071fdc7e4b1a9b1f8ae9b", - "zh:5efb9da90f665e43f22c2e13e0ce48e86cae2d960aaf1abf721b497f32025916", - "zh:6f71257a6b1218d02a573fc9bff0657410404fb2ef23bc66ae8cd968f98d5ff6", + "h1:t152MY0tQH4a8fLzTtEWx70ITd3azVOrFDn/pQblbto=", + "zh:3193b89b43bf5805493e290374cdda5132578de6535f8009547c8b5d7a351585", + "zh:3218320de4be943e5812ed3de995946056db86eb8d03aa3f074e0c7316599bef", + "zh:419861805a37fa443e7d63b69fb3279926ccf98a79d256c422d5d82f0f387d1d", + "zh:4df9bd9d839b8fc11a3b8098a604b9b46e2235eb65ef15f4432bde0e175f9ca6", + "zh:5814be3f9c9cc39d2955d6f083bae793050d75c572e70ca11ccceb5517ced6b1", + "zh:63c6548a06de1231c8ee5570e42ca09c4b3db336578ded39b938f2156f06dd2e", + "zh:697e434c6bdee0502cc3deb098263b8dcd63948e8a96d61722811628dce2eba1", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:9647e18f221380a85f2f0ab387c68fdafd58af6193a932417299cdcae4710150", - "zh:bb6297ce412c3c2fa9fec726114e5e0508dd2638cad6a0cb433194930c97a544", - "zh:f83e925ed73ff8a5ef6e3608ad9225baa5376446349572c2449c0c0b3cf184b7", - "zh:fbef0781cb64de76b1df1ca11078aecba7800d82fd4a956302734999cfd9a4af", + "zh:a0b8e44927e6327852bbfdc9d408d802569367f1e22a95bcdd7181b1c3b07601", + "zh:b7d3af018683ef22794eea9c218bc72d7c35a2b3ede9233b69653b3c782ee436", + "zh:d63b911d618a6fe446c65bfc21e793a7663e934b2fef833d42d3ccd38dd8d68d", + "zh:fa985cd0b11e6d651f47cff3055f0a9fd085ec190b6dbe99bf5448174434cdea", ] } provider "registry.terraform.io/hashicorp/tls" { - version = "4.0.5" + version = "4.0.6" constraints = "~> 4.0" hashes = [ - "h1:e4LBdJoZJNOQXPWgOAG0UuPBVhCStu98PieNlqJTmeU=", - "h1:yLqz+skP3+EbU3yyvw8JqzflQTKDQGsC9QyZAg+S4dg=", - "zh:01cfb11cb74654c003f6d4e32bbef8f5969ee2856394a96d127da4949c65153e", - "zh:0472ea1574026aa1e8ca82bb6df2c40cd0478e9336b7a8a64e652119a2fa4f32", - "zh:1a8ddba2b1550c5d02003ea5d6cdda2eef6870ece86c5619f33edd699c9dc14b", - "zh:1e3bb505c000adb12cdf60af5b08f0ed68bc3955b0d4d4a126db5ca4d429eb4a", - "zh:6636401b2463c25e03e68a6b786acf91a311c78444b1dc4f97c539f9f78de22a", - "zh:76858f9d8b460e7b2a338c477671d07286b0d287fd2d2e3214030ae8f61dd56e", - "zh:a13b69fb43cb8746793b3069c4d897bb18f454290b496f19d03c3387d1c9a2dc", - "zh:a90ca81bb9bb509063b736842250ecff0f886a91baae8de65c8430168001dad9", - "zh:c4de401395936e41234f1956ebadbd2ed9f414e6908f27d578614aaa529870d4", - "zh:c657e121af8fde19964482997f0de2d5173217274f6997e16389e7707ed8ece8", - "zh:d68b07a67fbd604c38ec9733069fbf23441436fecf554de6c75c032f82e1ef19", + "h1:n3M50qfWfRSpQV9Pwcvuse03pEizqrmYEryxKky4so4=", + "zh:10de0d8af02f2e578101688fd334da3849f56ea91b0d9bd5b1f7a243417fdda8", + "zh:37fc01f8b2bc9d5b055dc3e78bfd1beb7c42cfb776a4c81106e19c8911366297", + "zh:4578ca03d1dd0b7f572d96bd03f744be24c726bfd282173d54b100fd221608bb", + "zh:6c475491d1250050765a91a493ef330adc24689e8837a0f07da5a0e1269e11c1", + "zh:81bde94d53cdababa5b376bbc6947668be4c45ab655de7aa2e8e4736dfd52509", + "zh:abdce260840b7b050c4e401d4f75c7a199fafe58a8b213947a258f75ac18b3e8", + "zh:b754cebfc5184873840f16a642a7c9ef78c34dc246a8ae29e056c79939963c7a", + "zh:c928b66086078f9917aef0eec15982f2e337914c5c4dbc31dd4741403db7eb18", + "zh:cded27bee5f24de6f2ee0cfd1df46a7f88e84aaffc2ecbf3ff7094160f193d50", + "zh:d65eb3867e8f69aaf1b8bb53bd637c99c6b649ba3db16ded50fa9a01076d1a27", + "zh:ecb0c8b528c7a619fa71852bb3fb5c151d47576c5aab2bf3af4db52588722eeb", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } diff --git a/examples/ecs-service-discovery/README.md b/examples/ecs-service-discovery/README.md index 1da3e00..bde0ec4 100644 --- a/examples/ecs-service-discovery/README.md +++ b/examples/ecs-service-discovery/README.md @@ -19,17 +19,17 @@ service_name-port_name:port | Name | Version | |------|---------| -| [aws](#provider\_aws) | 4.67.0 | -| [random](#provider\_random) | 3.6.2 | +| [aws](#provider\_aws) | 5.90.1 | ## Modules | Name | Source | Version | |------|--------|---------| -| [cluster](#module\_cluster) | ../../modules/ecs-cluster | n/a | | [lb](#module\_lb) | ../../modules/lb/alb | n/a | | [public\_subnets](#module\_public\_subnets) | ../../modules/vpc-public-subnet | n/a | -| [service](#module\_service) | ../../modules/ecs-service | n/a | +| [secrets](#module\_secrets) | ../../modules/ssm/parameters | n/a | +| [static\_cluster](#module\_static\_cluster) | ../../modules/ecs-cluster-static | n/a | +| [static\_service](#module\_static\_service) | ../../modules/ecs-service-managed | n/a | | [vpc](#module\_vpc) | ../../modules/vpc | n/a | ## Resources @@ -38,7 +38,6 @@ service_name-port_name:port |------|------| | [aws_alb_listener.http](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/alb_listener) | resource | | [aws_service_discovery_http_namespace.local](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/service_discovery_http_namespace) | resource | -| [random_id.example](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | ## Outputs diff --git a/examples/ecs-service-discovery/main.tf b/examples/ecs-service-discovery/main.tf index 84ede86..0c231e5 100644 --- a/examples/ecs-service-discovery/main.tf +++ b/examples/ecs-service-discovery/main.tf @@ -6,18 +6,12 @@ locals { } } -resource "random_id" "example" { - byte_length = 2 - - prefix = "discovery-" -} - # network module "vpc" { source = "../../modules/vpc" - name = random_id.example.hex + name = "discovery" cidr = "10.0.0.0/16" } @@ -43,7 +37,7 @@ module "public_subnets" { module "lb" { source = "../../modules/lb/alb" - name = random_id.example.hex + name = "discovery" vpc_id = module.vpc.id subnet_ids = module.public_subnets.ids force_https = false @@ -64,54 +58,128 @@ resource "aws_service_discovery_http_namespace" "local" { # cluster -module "cluster" { - source = "../../modules/ecs-cluster" +# module "cluster" { +# source = "../../modules/ecs-cluster" + +# context = { +# namespace = "selleo" +# stage = "dev" +# name = "example" +# } + +# name_prefix = random_id.example.hex +# vpc_id = module.vpc.id +# subnet_ids = module.public_subnets.ids +# instance_type = "t3.nano" +# lb_security_group_id = module.lb.security_group_id + +# autoscaling_group = { +# min_size = 1 +# max_size = 5 +# desired_capacity = 1 +# } +# } + +module "static_cluster" { + source = "../../modules/ecs-cluster-static" context = { namespace = "selleo" stage = "dev" name = "example" } - - name_prefix = random_id.example.hex + name_prefix = "static-cluster" vpc_id = module.vpc.id subnet_ids = module.public_subnets.ids instance_type = "t3.nano" lb_security_group_id = module.lb.security_group_id - autoscaling_group = { - min_size = 1 - max_size = 5 - desired_capacity = 1 + instances = { + "search1" = { + ip = "10.0.1.1" + subnet_id = module.public_subnets.ids[0] + } } } -module "service" { - source = "../../modules/ecs-service" +# module "service" { +# source = "../../modules/ecs-service" + +# context = { +# namespace = "selleo" +# stage = "dev" +# name = "example" +# } + +# name = random_id.example.hex +# vpc_id = module.vpc.id +# subnet_ids = module.public_subnets.ids +# cluster_id = module.cluster.id +# desired_count = 1 + +# tcp_ports = [ +# { +# name = "http" +# host = 0 +# container = 3000 +# } +# ] + +# service_discovery = { +# arn = aws_service_discovery_http_namespace.local.arn +# name = "app" +# } +# } + +module "secrets" { + source = "../../modules/ssm/parameters" - context = { - namespace = "selleo" - stage = "dev" - name = "example" + context = local.context + + path = "/example/meilisearch" + secrets = { + # See other config: https://www.meilisearch.com/docs/learn/self_hosted/configure_meilisearch_at_launch + MEILI_NO_ANALYTICS = "1" + MEILI_ENV = "production" + MEILI_DB_PATH = "/meili/data" + MEILI_MASTER_KEY = "c4379222ecf8533c6004153a31294d30ca481813a9e43284c85a8e3aeb96da19" # ❗️ THIS SHOULD BE EDITABLE SECRET, NOT HARDCODED - this only for example } +} + +module "static_service" { + source = "../../modules/ecs-service-managed" - name = random_id.example.hex + context = local.context + + name = "meilisearch" vpc_id = module.vpc.id - subnet_ids = module.public_subnets.ids - cluster_id = module.cluster.id + subnet_ids = module.public_subnets.ids[0] + cluster_id = module.static_cluster.id desired_count = 1 tcp_ports = [ { name = "http" host = 0 - container = 3000 + container = 7700 } ] + image = "getmeili/meilisearch:v1.13" + limits = { + mem_min = 100 + mem_max = 400 + cpu = 500 + } + # volumes = [ + # { + # name = "$pwd/meili_data" + # path = "/meili_data" + # } + # ] service_discovery = { arn = aws_service_discovery_http_namespace.local.arn - name = "app" + name = "meilisearch" } } diff --git a/modules/ecs-cluster-static/README.md b/modules/ecs-cluster-static/README.md new file mode 100644 index 0000000..8762288 --- /dev/null +++ b/modules/ecs-cluster-static/README.md @@ -0,0 +1,94 @@ + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | ~> 1.0 | +| [aws](#requirement\_aws) | ~> 5.0 | +| [cloudinit](#requirement\_cloudinit) | ~> 2.0 | +| [random](#requirement\_random) | ~> 3.0 | +| [tls](#requirement\_tls) | ~> 4.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | ~> 5.0 | +| [cloudinit](#provider\_cloudinit) | ~> 2.0 | +| [random](#provider\_random) | ~> 3.0 | +| [tls](#provider\_tls) | ~> 4.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_ecs_cluster.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_cluster) | resource | +| [aws_iam_group.deployment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_group) | resource | +| [aws_iam_group_policy_attachment.deployment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_group_policy_attachment) | resource | +| [aws_iam_instance_profile.instance_profile](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | +| [aws_iam_policy.deployment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.instance_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy.ecs_instance](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.efs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_instance.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance) | resource | +| [aws_key_pair.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/key_pair) | resource | +| [aws_launch_template.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/launch_template) | resource | +| [aws_placement_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/placement_group) | resource | +| [aws_security_group.instance_sg](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_security_group_rule.allow_all_outbound_ec2_instance](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.allow_ssh](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.ephemeral_port_range](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [random_id.prefix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | +| [tls_private_key.this](https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/private_key) | resource | +| [aws_ami.ecs_optimized](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | +| [aws_iam_policy_document.deployment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.ecs_instance](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.efs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.instance_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [cloudinit_config.this](https://registry.terraform.io/providers/hashicorp/cloudinit/latest/docs/data-sources/config) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [allow\_ssh](#input\_allow\_ssh) | Allow SSH port in SG. | `bool` | `false` | no | +| [ami](#input\_ami) | Image ID for Autoscaling group. If left blank, latest ECS-optimized version will be used. | `string` | `""` | no | +| [associate\_public\_ip\_address](#input\_associate\_public\_ip\_address) | Associate a public ip address with an instance in a VPC. | `bool` | `true` | no | +| [autoscaling\_group](#input\_autoscaling\_group) | Autoscaling group configuration. |
object({
min_size = number
max_size = number
desired_capacity = number
})
|
{
"desired_capacity": 0,
"max_size": 5,
"min_size": 0
}
| no | +| [cloudinit\_parts](#input\_cloudinit\_parts) | Parts for cloud-init config that are added to the final MIME document. |
list(object({
content = string
filename = string
content_type = string
}))
| `[]` | no | +| [cloudinit\_scripts](#input\_cloudinit\_scripts) | Shell scripts added to cloud-init. | `list(string)` | `[]` | no | +| [context](#input\_context) | Project context. |
object({
namespace = string
stage = string
name = string
})
| n/a | yes | +| [ecs\_loglevel](#input\_ecs\_loglevel) | ECS Cluster log level. | `string` | `"info"` | no | +| [efs](#input\_efs) | EFS volume to mount to ECS |
object({
arn = string
})
| `null` | no | +| [enable\_container\_insights](#input\_enable\_container\_insights) | Enable container insights for the cluster. | `bool` | `true` | no | +| [instance\_type](#input\_instance\_type) | EC2 instance type i.e. t3.medium. | `string` | n/a | yes | +| [instances](#input\_instances) | Instances to create in the cluster. |
map(object({
ip = string
subnet_id = string
}))
| n/a | yes | +| [lb\_security\_group\_id](#input\_lb\_security\_group\_id) | Load balancer security group id. | `string` | n/a | yes | +| [name\_prefix](#input\_name\_prefix) | Name prefix (hyphen suffix should be skipped). | `string` | n/a | yes | +| [placement\_group](#input\_placement\_group) | Placement group strategy. |
object({
strategy = string
spread_level = string
})
|
{
"spread_level": "rack",
"strategy": "spread"
}
| no | +| [protect\_from\_scale\_in](#input\_protect\_from\_scale\_in) | If protect from scale in is enabled, newly launched instances will be protected from scale in by default. | `bool` | `false` | no | +| [root\_block\_configuration](#input\_root\_block\_configuration) | Configuration for root block device block. |
object({
volume_type = string
volume_size = number
})
|
{
"volume_size": 30,
"volume_type": "gp2"
}
| no | +| [security\_groups](#input\_security\_groups) | List of security groups attached to launch configuration. | `list(string)` | `[]` | no | +| [ssh\_cidr\_ipv4](#input\_ssh\_cidr\_ipv4) | IPv4 CIDR block that will be granted access to SSH on ECS instances. | `list(string)` | `[]` | no | +| [ssh\_cidr\_ipv6](#input\_ssh\_cidr\_ipv6) | IPv6 CIDR block that will be granted access to SSH on ECS instances. | `list(string)` | `[]` | no | +| [ssm\_tag\_key](#input\_ssm\_tag\_key) | Tag key to add for SSM access | `string` | `"ssm.group"` | no | +| [ssm\_tag\_value](#input\_ssm\_tag\_value) | Tag value to add for SSM access | `string` | `"true"` | no | +| [subnet\_ids](#input\_subnet\_ids) | List of AWS subent IDs for Autoscaling group. | `list(string)` | n/a | yes | +| [tags](#input\_tags) | Additional tags attached to resources. | `map(string)` | `{}` | no | +| [vpc\_id](#input\_vpc\_id) | AWS VPC id. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [autoscaling\_group](#output\_autoscaling\_group) | Autoscaling Group data. | +| [deployment\_group](#output\_deployment\_group) | Deployment group name | +| [deployment\_group\_arn](#output\_deployment\_group\_arn) | Deployment group ARN | +| [id](#output\_id) | ECS cluster ID (contains randomized suffix). | +| [instance\_role](#output\_instance\_role) | IAM role that is attached to EC2 instances. | +| [instance\_security\_group\_id](#output\_instance\_security\_group\_id) | ID of the security group attached to an instance. | +| [key\_name](#output\_key\_name) | Key pair name for SSH access. | +| [name](#output\_name) | ECS cluster name. | +| [prefix](#output\_prefix) | Random prefix to use for associated resources. | +| [private\_key\_pem](#output\_private\_key\_pem) | Private key in PEM format. | + \ No newline at end of file diff --git a/modules/ecs-cluster-static/data.tf b/modules/ecs-cluster-static/data.tf new file mode 100644 index 0000000..80cb94e --- /dev/null +++ b/modules/ecs-cluster-static/data.tf @@ -0,0 +1,10 @@ +data "aws_ami" "ecs_optimized" { + owners = ["amazon"] + + most_recent = true + + filter { + name = "name" + values = ["amzn2-ami-ecs-hvm-*-x86_64-ebs"] + } +} diff --git a/modules/ecs-cluster-static/iam.tf b/modules/ecs-cluster-static/iam.tf new file mode 100644 index 0000000..a88c494 --- /dev/null +++ b/modules/ecs-cluster-static/iam.tf @@ -0,0 +1,48 @@ +resource "aws_iam_role" "instance_role" { + name = "${random_id.prefix.hex}-cluster-instance" + assume_role_policy = data.aws_iam_policy_document.instance_role.json + + tags = merge(local.tags, { "resource.group" = "identity" }) +} + +data "aws_iam_policy_document" "instance_role" { + statement { + effect = "Allow" + actions = [ + "sts:AssumeRole", + ] + + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +resource "aws_iam_instance_profile" "instance_profile" { + name = "${random_id.prefix.hex}-cluster-instance" + role = aws_iam_role.instance_role.name +} + +resource "aws_iam_role_policy" "ecs_instance" { + name = "${random_id.prefix.hex}-ecs-instance" + role = aws_iam_role.instance_role.name + policy = data.aws_iam_policy_document.ecs_instance.json +} + +data "aws_iam_policy_document" "ecs_instance" { + statement { + effect = "Allow" + actions = [ + "ecs:RegisterContainerInstance", + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:Submit*", + "ecs:StartTelemetrySession", + "ecs:TagResource", + ] + + resources = ["*"] + } +} diff --git a/modules/ecs-cluster-static/main.tf b/modules/ecs-cluster-static/main.tf new file mode 100644 index 0000000..f5d03ba --- /dev/null +++ b/modules/ecs-cluster-static/main.tf @@ -0,0 +1,255 @@ +locals { + ami = var.ami == "" ? data.aws_ami.ecs_optimized.id : var.ami + + tags = merge({ + "context.namespace" = var.context.namespace + "context.stage" = var.context.stage + "context.name" = var.context.name + "Name" = random_id.prefix.hex + }, var.tags) +} + +resource "random_id" "prefix" { + byte_length = 3 + prefix = "${var.name_prefix}-" +} + +resource "aws_launch_template" "this" { + name_prefix = "${random_id.prefix.hex}-" + image_id = local.ami + instance_type = var.instance_type + key_name = aws_key_pair.this.key_name + + block_device_mappings { + device_name = "/dev/xvda" + + ebs { + volume_type = var.root_block_configuration.volume_type + volume_size = var.root_block_configuration.volume_size + } + } + + network_interfaces { + associate_public_ip_address = var.associate_public_ip_address + security_groups = concat([aws_security_group.instance_sg.id], var.security_groups) + } + + iam_instance_profile { + name = aws_iam_instance_profile.instance_profile.name + } + + user_data = data.cloudinit_config.this.rendered +} + +resource "aws_ecs_cluster" "this" { + name = random_id.prefix.hex + + setting { + name = "containerInsights" + value = var.enable_container_insights ? "enabled" : "disabled" + } + + tags = merge(local.tags, { "resource.group" = "compute" }) +} + +resource "aws_placement_group" "this" { + name = random_id.prefix.hex + strategy = var.placement_group.strategy # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-groups.html + spread_level = var.placement_group.spread_level + + tags = merge(local.tags, { "resource.group" = "compute" }) +} + +resource "aws_instance" "this" { + for_each = var.instances + + launch_template { + id = aws_launch_template.this.id + version = "$Latest" + } + private_ip = each.value.ip + subnet_id = each.value.subnet_id + placement_group = aws_placement_group.this.id + + tags = merge(local.tags, { + "resource.group" = "compute" + "${var.ssm_tag_key}" = var.ssm_tag_value + Name = "${random_id.prefix.hex}-${each.key}" + }) + + lifecycle { + create_before_destroy = false + ignore_changes = [] + } +} + +resource "aws_security_group" "instance_sg" { + description = "Controls direct access to application instances" + vpc_id = var.vpc_id + name = "${random_id.prefix.hex}-instance" + + tags = merge(local.tags, { "resource.group" = "network" }) +} + +resource "aws_security_group_rule" "ephemeral_port_range" { + description = "Allow dynamic port mapping for ECS" + type = "ingress" + from_port = 1 + to_port = 65535 + protocol = "tcp" + source_security_group_id = var.lb_security_group_id + security_group_id = aws_security_group.instance_sg.id +} + +resource "aws_security_group_rule" "allow_all_outbound_ec2_instance" { + description = "Allow outgoing traffic" + type = "egress" + from_port = 0 + to_port = 0 + protocol = "all" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.instance_sg.id +} + +resource "aws_iam_role_policy" "efs" { + count = var.efs == null ? 0 : 1 + + name = "${random_id.prefix.hex}-efs" + role = aws_iam_role.instance_role.name + policy = data.aws_iam_policy_document.efs[count.index].json +} + +data "aws_iam_policy_document" "efs" { + count = var.efs == null ? 0 : 1 + + statement { + effect = "Allow" + actions = [ + "elasticfilesystem:DescribeMountTargets", + "elasticfilesystem:DescribeAccessPoints", + "elasticfilesystem:DescribeFileSystems", + "elasticfilesystem:DescribeTags", + ] + + resources = [ + var.efs.arn + ] + } + + statement { + effect = "Allow" + actions = [ + "ec2:DescribeAvailabilityZones", + ] + + resources = ["*"] + } +} + + +data "cloudinit_config" "this" { + gzip = true + base64_encode = true + + part { + filename = "ecs-init.sh" + content_type = "text/x-shellscript" + content = templatefile("${path.module}/scripts/user_data.sh.tpl", + { + ecs_cluster = aws_ecs_cluster.this.name, + ecs_loglevel = var.ecs_loglevel, + ecs_tags = jsonencode(merge(var.tags, { + "Name" = random_id.prefix.hex, + "ssm.group" = var.ssm_tag_value, + })) + } + ) + } + + dynamic "part" { + for_each = var.efs == null ? [] : ["efs"] + + content { + filename = "efs.sh" + content = <> /etc/ecs/ecs.config +ECS_CLUSTER=${ecs_cluster} +ECS_LOGLEVEL=${ecs_loglevel} +ECS_CONTAINER_INSTANCE_TAGS=${ecs_tags} +CONFIG diff --git a/modules/ecs-cluster-static/variables.tf b/modules/ecs-cluster-static/variables.tf new file mode 100644 index 0000000..cc44c5c --- /dev/null +++ b/modules/ecs-cluster-static/variables.tf @@ -0,0 +1,186 @@ +# required + +variable "context" { + description = "Project context." + + type = object({ + namespace = string + stage = string + name = string + }) +} + +variable "instances" { + type = map(object({ + ip = string + subnet_id = string + })) + description = "Instances to create in the cluster." +} + +variable "name_prefix" { + type = string + description = "Name prefix (hyphen suffix should be skipped)." +} + +variable "vpc_id" { + type = string + description = "AWS VPC id." +} + +variable "subnet_ids" { + type = list(string) + description = "List of AWS subent IDs for Autoscaling group." +} + +variable "instance_type" { + type = string + description = "EC2 instance type i.e. t3.medium." +} + +variable "lb_security_group_id" { + type = string + description = "Load balancer security group id." +} + +# optional + +variable "autoscaling_group" { + type = object({ + min_size = number + max_size = number + desired_capacity = number + }) + description = "Autoscaling group configuration." + default = { + min_size = 0 + max_size = 5 + desired_capacity = 0 + } +} + +variable "tags" { + type = map(string) + description = "Additional tags attached to resources." + default = {} +} + +variable "ssm_tag_key" { + type = string + description = "Tag key to add for SSM access" + default = "ssm.group" +} + +variable "ssm_tag_value" { + type = string + description = "Tag value to add for SSM access" + default = "true" +} + +variable "protect_from_scale_in" { + type = bool + description = "If protect from scale in is enabled, newly launched instances will be protected from scale in by default." + default = false +} + +variable "ami" { + type = string + description = "Image ID for Autoscaling group. If left blank, latest ECS-optimized version will be used." + default = "" +} + +variable "security_groups" { + type = list(string) + description = "List of security groups attached to launch configuration." + default = [] +} + +variable "cloudinit_parts" { + type = list(object({ + content = string + filename = string + content_type = string + })) + description = "Parts for cloud-init config that are added to the final MIME document." + default = [] +} + +variable "cloudinit_scripts" { + type = list(string) + description = "Shell scripts added to cloud-init." + default = [] +} + +variable "enable_container_insights" { + type = bool + description = "Enable container insights for the cluster." + default = true +} + +variable "ecs_loglevel" { + type = string + description = "ECS Cluster log level." + default = "info" + + validation { + condition = can(regex("^crit|error|warn|info|debug$", var.ecs_loglevel)) + error_message = "The ecs_loglevel must be one of crit, error, warn, info, debug." + } +} + +variable "associate_public_ip_address" { + type = bool + default = true + description = "Associate a public ip address with an instance in a VPC." +} + +variable "root_block_configuration" { + type = object({ + volume_type = string + volume_size = number + }) + default = { + volume_type = "gp2" + volume_size = 30 + } + description = "Configuration for root block device block." +} + +variable "placement_group" { + description = "Placement group strategy." + + type = object({ + strategy = string + spread_level = string + }) + default = { + strategy = "spread" + spread_level = "rack" + } +} + +variable "allow_ssh" { + description = "Allow SSH port in SG." + type = bool + default = false +} + +variable "efs" { + type = object({ + arn = string + }) + description = "EFS volume to mount to ECS" + default = null +} + +variable "ssh_cidr_ipv4" { + type = list(string) + description = "IPv4 CIDR block that will be granted access to SSH on ECS instances." + default = [] +} + +variable "ssh_cidr_ipv6" { + type = list(string) + description = "IPv6 CIDR block that will be granted access to SSH on ECS instances." + default = [] +} diff --git a/modules/ecs-cluster-static/versions.tf b/modules/ecs-cluster-static/versions.tf new file mode 100644 index 0000000..9ab9851 --- /dev/null +++ b/modules/ecs-cluster-static/versions.tf @@ -0,0 +1,25 @@ +terraform { + required_version = "~> 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + + cloudinit = { + source = "hashicorp/cloudinit" + version = "~> 2.0" + } + + random = { + source = "hashicorp/random" + version = "~> 3.0" + } + + tls = { + source = "hashicorp/tls" + version = "~> 4.0" + } + } +} diff --git a/modules/ecs-service-managed/README.md b/modules/ecs-service-managed/README.md new file mode 100644 index 0000000..05d226d --- /dev/null +++ b/modules/ecs-service-managed/README.md @@ -0,0 +1,91 @@ + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | ~> 1.0 | +| [aws](#requirement\_aws) | ~> 5.0 | +| [random](#requirement\_random) | ~> 3.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | ~> 5.0 | +| [random](#provider\_random) | ~> 3.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_alb_target_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/alb_target_group) | resource | +| [aws_cloudwatch_log_group.one_off](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_cloudwatch_log_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_ecs_service.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service) | resource | +| [aws_ecs_task_definition.one_off](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition) | resource | +| [aws_ecs_task_definition.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition) | resource | +| [aws_iam_group.deployment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_group) | resource | +| [aws_iam_group_policy_attachment.pass_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_group_policy_attachment) | resource | +| [aws_iam_group_policy_attachment.run_one_off_task](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_group_policy_attachment) | resource | +| [aws_iam_group_policy_attachment.update_service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_group_policy_attachment) | resource | +| [aws_iam_policy.deployment_run_one_off_task](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.pass_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.update_service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.task_execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.task_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy.cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.cloudwatch_one_off](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.ssm_get](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.task_execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.task_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [random_id.prefix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | +| [aws_caller_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.cloudwatch_one_off](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.pass_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.run_task](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.task_execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.task_execution_ssm_get](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.task_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.update_service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_region.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [cluster\_id](#input\_cluster\_id) | ECS Cluster id. | `string` | n/a | yes | +| [context](#input\_context) | Project context. |
object({
namespace = string
stage = string
name = string
})
| n/a | yes | +| [create\_alb\_target\_group](#input\_create\_alb\_target\_group) | Register service as targer for load balancer. | `bool` | `true` | no | +| [deployment\_maximum\_percent](#input\_deployment\_maximum\_percent) | Upper limit (as a percentage of the service's `desired_count`) of the number of running tasks that can be running in a service during a deployment. Not valid when using the `DAEMON` scheduling strategy. | `number` | `200` | no | +| [deployment\_minimum\_healthy\_percent](#input\_deployment\_minimum\_healthy\_percent) | Lower limit (as a percentage of the service's desiredCount) of the number of running tasks that must remain running and healthy in a service during a deployment. | `number` | `50` | no | +| [deregistration\_delay](#input\_deregistration\_delay) | Deregistration delay (draining time) from LB. | `number` | `30` | no | +| [desired\_count](#input\_desired\_count) | Desired task count. | `number` | n/a | yes | +| [efs](#input\_efs) | EFS volume to mount to ECS |
object({
id = string
mount_path = string
volume = string
})
| `null` | no | +| [enable\_execute\_command](#input\_enable\_execute\_command) | Allow to exec into containers. | `bool` | `true` | no | +| [health\_check](#input\_health\_check) | Health check config for ALB target group. |
object({
path = string
matcher = string
})
|
{
"matcher": "200",
"path": "/"
}
| no | +| [health\_check\_threshold](#input\_health\_check\_threshold) | Health check thresholds for ALB target group. |
object({
timeout = number
interval = number
healthy = number
unhealthy = number
})
|
{
"healthy": 3,
"interval": 15,
"timeout": 10,
"unhealthy": 3
}
| no | +| [image](#input\_image) | ECS Service image. | `string` | n/a | yes | +| [limits](#input\_limits) | Limits for the service. |
object({
mem_min = number
cpu = number
mem_max = number
})
| n/a | yes | +| [log\_retention\_in\_days](#input\_log\_retention\_in\_days) | Log retention in days for Cloudwatch. | `string` | `60` | no | +| [name](#input\_name) | ECS Service name. | `string` | n/a | yes | +| [one\_off\_commands](#input\_one\_off\_commands) | Set of commands that the tasks are created for. | `set(string)` | `[]` | no | +| [secrets](#input\_secrets) | Paths to secret. All secrets are read under the path. | `set(string)` | `[]` | no | +| [service\_discovery](#input\_service\_discovery) | Service discovery configuration. |
object({
arn = string
name = string
})
| `null` | no | +| [subnet\_ids](#input\_subnet\_ids) | List of AWS subent IDs for service. | `list(string)` | n/a | yes | +| [tags](#input\_tags) | Additional tags attached to resources. | `map(string)` | `{}` | no | +| [tcp\_ports](#input\_tcp\_ports) | Port mapping. Use 0 for dynamic host mapping. Fargate requires ports to be the same. |
list(
object({
name = string
host = number
container = number
})
)
| `[]` | no | +| [vpc\_id](#input\_vpc\_id) | VPC id. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [deployment\_group](#output\_deployment\_group) | Deployment group name | +| [deployment\_group\_arn](#output\_deployment\_group\_arn) | Deployment group ARN | +| [lb\_target\_group\_id](#output\_lb\_target\_group\_id) | ARN of the Target Group. | +| [name](#output\_name) | Service name. | +| [service\_id](#output\_service\_id) | ARN that identifies the service. | +| [task\_execution\_role\_id](#output\_task\_execution\_role\_id) | ECS task execution role ID | +| [task\_role\_id](#output\_task\_role\_id) | ECS task role ID | + \ No newline at end of file diff --git a/modules/ecs-service-managed/main.tf b/modules/ecs-service-managed/main.tf new file mode 100644 index 0000000..04de3a3 --- /dev/null +++ b/modules/ecs-service-managed/main.tf @@ -0,0 +1,492 @@ +data "aws_region" "this" {} +data "aws_caller_identity" "this" {} + +locals { + tags = merge({ + "context.namespace" = var.context.namespace + "context.stage" = var.context.stage + "context.name" = var.context.name + }, var.tags) + + # when no ports are specified, we assume this is a worker + is_worker = var.tcp_ports == [] ? true : false + # when LB is used ports must be specified + needs_lb = var.create_alb_target_group && !local.is_worker + + ordered_placement_strategy = [ + { + type = "spread" + field = "attribute:ecs.avaiability-zones" + }, + { + type = "spread" + field = "instanceId" + } + ] +} + +resource "random_id" "prefix" { + byte_length = 4 + prefix = "${var.name}-" +} + +resource "aws_cloudwatch_log_group" "this" { + name = "${var.context.namespace}/${var.context.stage}/${var.context.name}/ecs/${var.name}" + retention_in_days = var.log_retention_in_days + + tags = merge(local.tags, { "resource.group" = "log" }) +} + +resource "aws_cloudwatch_log_group" "one_off" { + for_each = var.one_off_commands + + name = "${var.context.namespace}/${var.context.stage}/${var.context.name}/ecs/${var.name}-${each.key}" + retention_in_days = var.log_retention_in_days + + tags = merge(local.tags, { "resource.group" = "log" }) +} + +resource "aws_ecs_task_definition" "this" { + family = random_id.prefix.hex + network_mode = "bridge" + requires_compatibilities = ["EC2"] + execution_role_arn = aws_iam_role.task_execution.arn + task_role_arn = aws_iam_role.task_role.arn + + container_definitions = jsonencode([ + { + essential = true, + memoryReservation = var.limits.mem_min + memory = var.limits.mem_max + cpu = var.limits.cpu + name = var.name + image = var.image + mountPoints = var.efs == null ? [] : [ + { + sourceVolume = var.efs.volume + containerPath = var.efs.mount_path + readOnly = false + } + ], + volumesFrom = [], + # workers do not need port mappings + portMappings = local.is_worker ? [] : [ + for port in var.tcp_ports : + { + containerPort = port.container, + hostPort = port.host, # fargate port must match container port + protocol = "tcp", + name = port.name, + } + ], + environment = [], + + logConfiguration = { + logDriver = "awslogs", + options = { + awslogs-group = aws_cloudwatch_log_group.this.name, + awslogs-region = data.aws_region.this.name, + awslogs-stream-prefix = "ecs", + }, + }, + } + ]) + + dynamic "volume" { + for_each = var.efs == null ? [] : [var.efs.volume] + + content { + name = var.efs.volume + + efs_volume_configuration { + file_system_id = var.efs.id + root_directory = "/" + # transit_encryption = "ENABLED" + # transit_encryption_port = 2999 + } + } + } + + tags = merge(local.tags, { "resource.group" = "compute" }) +} + +resource "aws_ecs_task_definition" "one_off" { + for_each = var.one_off_commands + + family = "${random_id.prefix.hex}-${each.key}" + network_mode = "bridge" + requires_compatibilities = ["EC2"] + execution_role_arn = aws_iam_role.task_execution.arn + task_role_arn = aws_iam_role.task_role.arn + + container_definitions = jsonencode([ + { + essential = true, + memoryReservation = 32 + memory = 64 + cpu = 64 + name = var.name + image = "busybox:latest" + command = ["sh", "-c", "echo 'Hi'"] + + logConfiguration = { + logDriver = "awslogs", + options = { + awslogs-group = aws_cloudwatch_log_group.one_off[each.key].name, + awslogs-region = data.aws_region.this.name, + awslogs-stream-prefix = "ecs", + }, + }, + } + ]) + + dynamic "volume" { + for_each = var.efs == null ? [] : [var.efs.volume] + + content { + name = var.efs.volume + + efs_volume_configuration { + file_system_id = var.efs.id + root_directory = "/" + # transit_encryption = "ENABLED" + # transit_encryption_port = 2999 + } + } + } + + tags = merge(local.tags, { "resource.group" = "compute" }) +} + +resource "aws_ecs_service" "this" { + name = var.name + cluster = var.cluster_id + task_definition = "${aws_ecs_task_definition.this.family}:${aws_ecs_task_definition.this.revision}" + enable_execute_command = var.enable_execute_command + + launch_type = "EC2" + + dynamic "load_balancer" { + for_each = local.needs_lb ? [1] : [] + + content { + target_group_arn = aws_alb_target_group.this[0].arn + container_name = var.name + container_port = var.tcp_ports[0].container + } + } + + desired_count = var.desired_count + deployment_minimum_healthy_percent = var.deployment_minimum_healthy_percent + deployment_maximum_percent = var.deployment_maximum_percent + + dynamic "ordered_placement_strategy" { + for_each = local.ordered_placement_strategy + + content { + type = ordered_placement_strategy.value.type + field = ordered_placement_strategy.value.field + } + } + + dynamic "service_connect_configuration" { + for_each = var.service_discovery == null ? [] : [var.service_discovery] + + content { + enabled = true + log_configuration { + log_driver = "awslogs" + options = { + awslogs-group = aws_cloudwatch_log_group.this.name, + awslogs-region = data.aws_region.this.name, + awslogs-stream-prefix = "ecs-connect", + } + } + namespace = service_connect_configuration.value.arn + + dynamic "service" { + for_each = var.tcp_ports + + content { + client_alias { + port = service.value.container + # dns_name = service_connect_configuration.value.name + } + discovery_name = "${service_connect_configuration.value.name}-${service.value.name}" + # ingress_port_override = "" # - (Optional) Port number for the Service Connect proxy to listen on. + port_name = service.value.name + } + } + } + } + + tags = merge(local.tags, { "resource.group" = "compute" }) +} + +resource "aws_iam_role" "task_role" { + name = "${random_id.prefix.hex}-task" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + }, + ] + }) + + tags = merge(local.tags, { "resource.group" = "identity" }) +} + +resource "aws_iam_role_policy" "task_role" { + name = "${random_id.prefix.hex}-task" + role = aws_iam_role.task_role.name + policy = data.aws_iam_policy_document.task_role.json +} + + +resource "aws_iam_role" "task_execution" { + name = "${random_id.prefix.hex}-task-execution" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + }, + ] + }) + + tags = merge(local.tags, { "resource.group" = "identity" }) +} + +resource "aws_iam_role_policy" "task_execution" { + name = "${random_id.prefix.hex}-task-execution" + role = aws_iam_role.task_execution.name + policy = data.aws_iam_policy_document.task_execution.json +} + +resource "aws_iam_role_policy" "ssm_get" { + count = length(var.secrets) == 0 ? 0 : 1 + + name = "${random_id.prefix.hex}-ssm-get" + role = aws_iam_role.task_execution.name + policy = data.aws_iam_policy_document.task_execution_ssm_get.json +} + +data "aws_iam_policy_document" "task_execution_ssm_get" { + statement { + sid = "GetSSMParams" + effect = "Allow" + actions = [ + "ssm:GetParameters", + ] + + resources = [ + for secret in var.secrets : + "arn:aws:ssm:${data.aws_region.this.name}:${data.aws_caller_identity.this.account_id}:parameter${secret}/*" + ] + } +} + +data "aws_iam_policy_document" "task_role" { + statement { + sid = "Task" + effect = "Allow" + actions = [ + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel", + ] + + resources = ["*"] + } +} + +data "aws_iam_policy_document" "task_execution" { + statement { + sid = "Task" + effect = "Allow" + actions = [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + ] + + resources = ["*"] + } +} + +resource "aws_iam_role_policy" "cloudwatch" { + name = "${random_id.prefix.hex}-role-cloudwatch-policy" + role = aws_iam_role.task_execution.id + policy = data.aws_iam_policy_document.cloudwatch.json +} + +resource "aws_iam_role_policy" "cloudwatch_one_off" { + for_each = var.one_off_commands + + name = "${random_id.prefix.hex}-${each.key}-cloudwatch-policy" + role = aws_iam_role.task_execution.id + policy = data.aws_iam_policy_document.cloudwatch_one_off[each.key].json +} + +data "aws_iam_policy_document" "cloudwatch" { + statement { + effect = "Allow" + actions = [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + + resources = ["${aws_cloudwatch_log_group.this.arn}:*"] + } +} + +data "aws_iam_policy_document" "cloudwatch_one_off" { + for_each = var.one_off_commands + + statement { + effect = "Allow" + actions = [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + + resources = ["${aws_cloudwatch_log_group.one_off[each.key].arn}:*"] + } +} + +resource "aws_alb_target_group" "this" { + count = local.needs_lb ? 1 : 0 + + name = random_id.prefix.hex + port = var.tcp_ports[0].container + protocol = "HTTP" + vpc_id = var.vpc_id + deregistration_delay = var.deregistration_delay # draining time + target_type = "instance" + + health_check { + path = var.health_check.path + protocol = "HTTP" + timeout = var.health_check_threshold.timeout + interval = var.health_check_threshold.interval + healthy_threshold = var.health_check_threshold.healthy + unhealthy_threshold = var.health_check_threshold.unhealthy + matcher = var.health_check.matcher + } + + tags = merge(local.tags, { "resource.group" = "network" }) +} + +# deployment group that can be attached to user deployer + +resource "aws_iam_group" "deployment" { + name = "${random_id.prefix.hex}-deployment" +} + +resource "aws_iam_group_policy_attachment" "update_service" { + group = aws_iam_group.deployment.name + policy_arn = aws_iam_policy.update_service.arn +} + +resource "aws_iam_group_policy_attachment" "pass_role" { + group = aws_iam_group.deployment.name + policy_arn = aws_iam_policy.pass_role.arn +} + +resource "aws_iam_group_policy_attachment" "run_one_off_task" { + for_each = var.one_off_commands + + group = aws_iam_group.deployment.name + policy_arn = aws_iam_policy.deployment_run_one_off_task[each.key].arn +} + +# policy for updating service + +resource "aws_iam_policy" "update_service" { + name = "${random_id.prefix.hex}-update-service" + policy = data.aws_iam_policy_document.update_service.json + + tags = merge(local.tags, { "resource.group" = "identity" }) +} + +data "aws_iam_policy_document" "update_service" { + statement { + actions = [ + "ecs:DescribeTaskDefinition", + "ecs:RegisterTaskDefinition", + ] + + resources = ["*"] + } + + statement { + actions = [ + "ecs:DescribeServices", + "ecs:UpdateService", + ] + + resources = [aws_ecs_service.this.id] + } + + statement { + actions = [ + "ecs:TagResource", + ] + + resources = [ + "arn:aws:ecs:${data.aws_region.this.id}:${data.aws_caller_identity.this.account_id}:task-definition/${random_id.prefix.hex}:*" + ] + } +} + +# policy for registering new task (IAM needs to pass role to task/execution role) + +resource "aws_iam_policy" "pass_role" { + name = "${random_id.prefix.hex}-pass-role" + policy = data.aws_iam_policy_document.pass_role.json + + tags = merge(local.tags, { "resource.group" = "identity" }) +} + +data "aws_iam_policy_document" "pass_role" { + statement { + actions = ["iam:PassRole", "iam:GetRole"] + + resources = [ + aws_iam_role.task_role.arn, + aws_iam_role.task_execution.arn + ] + } +} + +# policy for starting new one off task + +resource "aws_iam_policy" "deployment_run_one_off_task" { + for_each = var.one_off_commands + + name = "${random_id.prefix.hex}-one-off-run-${each.key}" + policy = data.aws_iam_policy_document.run_task[each.key].json +} + +data "aws_iam_policy_document" "run_task" { + for_each = var.one_off_commands + + statement { + actions = ["ecs:RunTask"] + + resources = [ + "arn:aws:ecs:${data.aws_region.this.id}:${data.aws_caller_identity.this.id}:task-definition/${random_id.prefix.hex}-${each.key}" + ] + } +} \ No newline at end of file diff --git a/modules/ecs-service-managed/outputs.tf b/modules/ecs-service-managed/outputs.tf new file mode 100644 index 0000000..580c875 --- /dev/null +++ b/modules/ecs-service-managed/outputs.tf @@ -0,0 +1,34 @@ +output "lb_target_group_id" { + value = try(aws_alb_target_group.this[0].id, null) + description = "ARN of the Target Group." +} + +output "name" { + value = var.name + description = "Service name." +} + +output "service_id" { + value = aws_ecs_service.this.id + description = "ARN that identifies the service." +} + +output "task_role_id" { + value = aws_iam_role.task_role.id + description = "ECS task role ID" +} + +output "task_execution_role_id" { + value = aws_iam_role.task_execution.id + description = "ECS task execution role ID" +} + +output "deployment_group" { + value = aws_iam_group.deployment.name + description = "Deployment group name" +} + +output "deployment_group_arn" { + value = aws_iam_group.deployment.arn + description = "Deployment group ARN" +} \ No newline at end of file diff --git a/modules/ecs-service-managed/variables.tf b/modules/ecs-service-managed/variables.tf new file mode 100644 index 0000000..db123de --- /dev/null +++ b/modules/ecs-service-managed/variables.tf @@ -0,0 +1,167 @@ +# required + +variable "context" { + description = "Project context." + + type = object({ + namespace = string + stage = string + name = string + }) +} + +variable "vpc_id" { + type = string + description = "VPC id." +} + +variable "subnet_ids" { + type = list(string) + description = "List of AWS subent IDs for service." +} + +variable "name" { + type = string + description = "ECS Service name." +} + +variable "cluster_id" { + type = string + description = "ECS Cluster id." +} + +variable "desired_count" { + type = number + description = "Desired task count." +} + +variable "image" { + type = string + description = "ECS Service image." +} + +variable "limits" { + type = object({ + mem_min = number + cpu = number + mem_max = number + }) + description = "Limits for the service." +} +# optional + +variable "tcp_ports" { + description = "Port mapping. Use 0 for dynamic host mapping. Fargate requires ports to be the same." + type = list( + object({ + name = string + host = number + container = number + }) + ) + default = [] +} + +variable "enable_execute_command" { + description = "Allow to exec into containers." + type = bool + default = true +} + +variable "create_alb_target_group" { + description = "Register service as targer for load balancer." + type = bool + default = true +} + +variable "secrets" { + description = "Paths to secret. All secrets are read under the path." + type = set(string) + default = [] +} + +variable "tags" { + type = map(string) + description = "Additional tags attached to resources." + default = {} +} + +variable "one_off_commands" { + type = set(string) + description = "Set of commands that the tasks are created for." + default = [] +} + +variable "health_check" { + type = object({ + path = string + matcher = string + }) + description = "Health check config for ALB target group." + default = { + path = "/" + matcher = "200" + } +} + +variable "health_check_threshold" { + type = object({ + timeout = number + interval = number + healthy = number + unhealthy = number + }) + description = "Health check thresholds for ALB target group." + default = { + timeout = 10 + interval = 15 + healthy = 3 + unhealthy = 3 + } +} + +variable "deregistration_delay" { + description = "Deregistration delay (draining time) from LB." + + type = number + default = 30 +} + +variable "deployment_minimum_healthy_percent" { + description = "Lower limit (as a percentage of the service's desiredCount) of the number of running tasks that must remain running and healthy in a service during a deployment." + + type = number + default = 50 +} + +variable "deployment_maximum_percent" { + description = "Upper limit (as a percentage of the service's `desired_count`) of the number of running tasks that can be running in a service during a deployment. Not valid when using the `DAEMON` scheduling strategy." + + type = number + default = 200 +} + +variable "log_retention_in_days" { + type = string + description = "Log retention in days for Cloudwatch." + default = 60 +} + +variable "efs" { + type = object({ + id = string + mount_path = string + volume = string + }) + description = "EFS volume to mount to ECS" + default = null +} + +variable "service_discovery" { + description = "Service discovery configuration." + type = object({ + arn = string + name = string + }) + default = null +} \ No newline at end of file diff --git a/modules/ecs-service-managed/versions.tf b/modules/ecs-service-managed/versions.tf new file mode 100644 index 0000000..39eecd6 --- /dev/null +++ b/modules/ecs-service-managed/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_version = "~> 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + + random = { + source = "hashicorp/random" + version = "~> 3.0" + } + } +}