diff --git a/2-simple-example/iac/.terraform.lock.hcl b/2-simple-example/iac/.terraform.lock.hcl new file mode 100644 index 0000000..cb02273 --- /dev/null +++ b/2-simple-example/iac/.terraform.lock.hcl @@ -0,0 +1,20 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/aws" { + version = "5.45.0" + constraints = ">= 4.0.0, >= 4.66.1, >= 5.30.0, >= 5.33.0" + hashes = [ + "h1:3zU3yp1SY+8vHAQvhfhYdPnFYQpFwXXXar+hOrnofzQ=", + "zh:1d71c406aeaf4ba762eb62e4595ab9c9f8da1a2c9b74bb4277c0acfd9678ae65", + "zh:3b00b13154eadedb37bca99bf7cbd556fa9472e6900c970effa17a270ee9f721", + "zh:6f264e8b70153925ac8abfa83ebffe2c2d5a27ab5557a6b16124269b08ac2441", + "zh:80f7d552faf5c43d7dc22c6c1f7e70557b9f01c67db07abbb0330d5d3fc0e464", + "zh:863a2a2e6ae5b42fc46b209d8f2761c882d46aca481a8c49ef221d290b4fd88e", + "zh:8e3bddeb2da7e6bcfd0b0221a083778d2f7fc5cd64f55de7d8d79bd1f7378bae", + "zh:c726104e46cd743bbf240101d7975f44091d893b6e97b46070df0041779b04d2", + "zh:db73a89b462fdd6eb6f32e6ed464430a895fc2e54fb629e8b99773fc32a6a7a8", + "zh:e35179b89eba358f521ffd4546345b4d0683ca3364a9deb8f3b7b4bf60be6f02", + "zh:e7b54a0faecd34a9c73729d1d1f0cfc1b8f56bae789f95987002616f1265ce72", + ] +} diff --git a/2-simple-example/iac/data.tf b/2-simple-example/iac/data.tf new file mode 100644 index 0000000..4ba5508 --- /dev/null +++ b/2-simple-example/iac/data.tf @@ -0,0 +1 @@ +data "aws_availability_zones" "available" {} diff --git a/2-simple-example/iac/ecs.tf b/2-simple-example/iac/ecs.tf new file mode 100644 index 0000000..6877e32 --- /dev/null +++ b/2-simple-example/iac/ecs.tf @@ -0,0 +1,292 @@ +################################################################################ +# Cluster +################################################################################ + +module "ecs_cluster" { + source = "./modules/cluster" + + cluster_name = local.name + + # Capacity provider + fargate_capacity_providers = { + FARGATE = { + default_capacity_provider_strategy = { + weight = 50 + base = 20 + } + } + FARGATE_SPOT = { + default_capacity_provider_strategy = { + weight = 50 + } + } + } + + tags = local.tags +} + +################################################################################ +# Service +################################################################################ + +module "ecs_service" { + source = "./modules/service" + + name = local.name + cluster_arn = module.ecs_cluster.arn + + cpu = 1024 + memory = 4096 + + # Enables ECS Exec + enable_execute_command = true + + # Container definition(s) + container_definitions = { + + fluent-bit = { + cpu = 512 + memory = 1024 + essential = true + image = nonsensitive(data.aws_ssm_parameter.fluentbit.value) + firelens_configuration = { + type = "fluentbit" + } + memory_reservation = 50 + user = "0" + } + + (local.container_name) = { + cpu = 512 + memory = 1024 + essential = true + image = "public.ecr.aws/aws-containers/ecsdemo-frontend:776fd50" + port_mappings = [ + { + name = local.container_name + containerPort = local.container_port + hostPort = local.container_port + protocol = "tcp" + } + ] + + # Example image used requires access to write to root filesystem + readonly_root_filesystem = false + + dependencies = [{ + containerName = "fluent-bit" + condition = "START" + }] + + enable_cloudwatch_logging = false + log_configuration = { + logDriver = "awsfirelens" + options = { + Name = "firehose" + region = local.region + delivery_stream = "my-stream" + log-driver-buffer-limit = "2097152" + } + } + + linux_parameters = { + capabilities = { + add = [] + drop = [ + "NET_RAW" + ] + } + } + + # Not required for fluent-bit, just an example + volumes_from = [{ + sourceContainer = "fluent-bit" + readOnly = false + }] + + memory_reservation = 100 + } + } + + service_connect_configuration = { + namespace = aws_service_discovery_http_namespace.this.arn + service = { + client_alias = { + port = local.container_port + dns_name = local.container_name + } + port_name = local.container_name + discovery_name = local.container_name + } + } + + load_balancer = { + service = { + target_group_arn = module.alb.target_groups["ex_ecs"].arn + container_name = local.container_name + container_port = local.container_port + } + } + + subnet_ids = module.vpc.private_subnets + security_group_rules = { + alb_ingress_3000 = { + type = "ingress" + from_port = local.container_port + to_port = local.container_port + protocol = "tcp" + description = "Service port" + source_security_group_id = module.alb.security_group_id + } + egress_all = { + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + } + + service_tags = { + "ServiceTag" = "Tag on service level" + } + + tags = local.tags +} + +################################################################################ +# Standalone Task Definition (w/o Service) +################################################################################ + +module "ecs_task_definition" { + source = "./modules/service" + + # Service + name = "${local.name}-standalone" + cluster_arn = module.ecs_cluster.arn + + # Task Definition + volume = { + ex-vol = {} + } + + runtime_platform = { + cpu_architecture = "ARM64" + operating_system_family = "LINUX" + } + + # Container definition(s) + container_definitions = { + al2023 = { + image = "public.ecr.aws/amazonlinux/amazonlinux:2023-minimal" + + mount_points = [ + { + sourceVolume = "ex-vol", + containerPath = "/var/www/ex-vol" + } + ] + + command = ["echo hello world"] + entrypoint = ["/usr/bin/sh", "-c"] + } + } + + subnet_ids = module.vpc.private_subnets + + security_group_rules = { + egress_all = { + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + } + + tags = local.tags +} + +################################################################################ +# Supporting Resources +################################################################################ + +data "aws_ssm_parameter" "fluentbit" { + name = "/aws/service/aws-for-fluent-bit/stable" +} + +resource "aws_service_discovery_http_namespace" "this" { + name = local.name + description = "CloudMap namespace for ${local.name}" + tags = local.tags +} + +module "alb" { + source = "terraform-aws-modules/alb/aws" + version = "~> 9.0" + + name = local.name + + load_balancer_type = "application" + + vpc_id = module.vpc.vpc_id + subnets = module.vpc.public_subnets + + # For example only + enable_deletion_protection = false + + # Security Group + security_group_ingress_rules = { + all_http = { + from_port = 80 + to_port = 80 + ip_protocol = "tcp" + cidr_ipv4 = "0.0.0.0/0" + } + } + security_group_egress_rules = { + all = { + ip_protocol = "-1" + cidr_ipv4 = module.vpc.vpc_cidr_block + } + } + + listeners = { + ex_http = { + port = 80 + protocol = "HTTP" + + forward = { + target_group_key = "ex_ecs" + } + } + } + + target_groups = { + ex_ecs = { + backend_protocol = "HTTP" + backend_port = local.container_port + target_type = "ip" + deregistration_delay = 5 + load_balancing_cross_zone_enabled = true + + health_check = { + enabled = true + healthy_threshold = 5 + interval = 30 + matcher = "200" + path = "/" + port = "traffic-port" + protocol = "HTTP" + timeout = 5 + unhealthy_threshold = 2 + } + + # There's nothing to attach here in this definition. Instead, + # ECS will attach the IPs of the tasks to this target group + create_attachment = false + } + } + + tags = local.tags +} diff --git a/2-simple-example/iac/locals.tf b/2-simple-example/iac/locals.tf new file mode 100644 index 0000000..316dda1 --- /dev/null +++ b/2-simple-example/iac/locals.tf @@ -0,0 +1,16 @@ +locals { + region = "eu-west-1" + name = "chat-app-demo" + + vpc_cidr = "10.0.0.0/16" + azs = slice(data.aws_availability_zones.available.names, 0, 3) + + container_name = "chat-app" + container_port = 3000 + + tags = { + Name = local.name + Example = local.name + Repository = "https://github.com/terraform-aws-modules/terraform-aws-ecs" + } +} diff --git a/2-simple-example/iac/main.tf b/2-simple-example/iac/main.tf new file mode 100644 index 0000000..5198d0a --- /dev/null +++ b/2-simple-example/iac/main.tf @@ -0,0 +1,4 @@ +provider "aws" { + region = var.region +} + diff --git a/2-simple-example/iac/modules/cluster/README.md b/2-simple-example/iac/modules/cluster/README.md new file mode 100644 index 0000000..44284f0 --- /dev/null +++ b/2-simple-example/iac/modules/cluster/README.md @@ -0,0 +1,214 @@ +# Amazon ECS Cluster Terraform Module + +Terraform module which creates Amazon ECS (Elastic Container Service) cluster resources on AWS. + +## Available Features + +- ECS cluster +- Fargate capacity providers +- EC2 AutoScaling Group capacity providers +- ECS Service w/ task definition, task set, and container definition support + +For more details see the [design doc](https://github.com/terraform-aws-modules/terraform-aws-ecs/blob/master/docs/README.md) + +## Usage + +### Fargate Capacity Providers + +```hcl +module "ecs_cluster" { + source = "terraform-aws-modules/ecs/aws//modules/cluster" + + cluster_name = "ecs-fargate" + + cluster_configuration = { + execute_command_configuration = { + logging = "OVERRIDE" + log_configuration = { + cloud_watch_log_group_name = "/aws/ecs/aws-ec2" + } + } + } + + fargate_capacity_providers = { + FARGATE = { + default_capacity_provider_strategy = { + weight = 50 + } + } + FARGATE_SPOT = { + default_capacity_provider_strategy = { + weight = 50 + } + } + } + + tags = { + Environment = "Development" + Project = "EcsEc2" + } +} +``` + +### EC2 Autoscaling Capacity Providers + +```hcl +module "ecs_cluster" { + source = "terraform-aws-modules/ecs/aws//modules/cluster" + + cluster_name = "ecs-ec2" + + cluster_configuration = { + execute_command_configuration = { + logging = "OVERRIDE" + log_configuration = { + cloud_watch_log_group_name = "/aws/ecs/aws-ec2" + } + } + } + + autoscaling_capacity_providers = { + one = { + auto_scaling_group_arn = "arn:aws:autoscaling:eu-west-1:012345678901:autoScalingGroup:08419a61:autoScalingGroupName/ecs-ec2-one-20220603194933774300000011" + managed_termination_protection = "ENABLED" + + managed_scaling = { + maximum_scaling_step_size = 5 + minimum_scaling_step_size = 1 + status = "ENABLED" + target_capacity = 60 + } + + default_capacity_provider_strategy = { + weight = 60 + base = 20 + } + } + two = { + auto_scaling_group_arn = "arn:aws:autoscaling:eu-west-1:012345678901:autoScalingGroup:08419a61:autoScalingGroupName/ecs-ec2-two-20220603194933774300000022" + managed_termination_protection = "ENABLED" + + managed_scaling = { + maximum_scaling_step_size = 15 + minimum_scaling_step_size = 5 + status = "ENABLED" + target_capacity = 90 + } + + default_capacity_provider_strategy = { + weight = 40 + } + } + } + + tags = { + Environment = "Development" + Project = "EcsEc2" + } +} +``` + +## Conditional Creation + +The following values are provided to toggle on/off creation of the associated resources as desired: + +```hcl +module "ecs_cluster" { + source = "terraform-aws-modules/ecs/aws//modules/cluster" + + # Disable creation of cluster and all resources + create = false + + # ... omitted +} +``` + +## Examples + +- [ECS Cluster Complete](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/complete) +- [ECS Cluster w/ EC2 Autoscaling Capacity Provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/ec2-autoscaling) +- [ECS Cluster w/ Fargate Capacity Provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/fargate) + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | >= 4.66.1 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.66.1 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudwatch_log_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_ecs_capacity_provider.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_capacity_provider) | resource | +| [aws_ecs_cluster.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_cluster) | resource | +| [aws_ecs_cluster_capacity_providers.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_cluster_capacity_providers) | resource | +| [aws_iam_policy.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.task_exec_additional](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_policy_document.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.task_exec_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [autoscaling\_capacity\_providers](#input\_autoscaling\_capacity\_providers) | Map of autoscaling capacity provider definitions to create for the cluster | `any` | `{}` | no | +| [cloudwatch\_log\_group\_kms\_key\_id](#input\_cloudwatch\_log\_group\_kms\_key\_id) | If a KMS Key ARN is set, this key will be used to encrypt the corresponding log group. Please be sure that the KMS Key has an appropriate key policy (https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html) | `string` | `null` | no | +| [cloudwatch\_log\_group\_name](#input\_cloudwatch\_log\_group\_name) | Custom name of CloudWatch Log Group for ECS cluster | `string` | `null` | no | +| [cloudwatch\_log\_group\_retention\_in\_days](#input\_cloudwatch\_log\_group\_retention\_in\_days) | Number of days to retain log events | `number` | `90` | no | +| [cloudwatch\_log\_group\_tags](#input\_cloudwatch\_log\_group\_tags) | A map of additional tags to add to the log group created | `map(string)` | `{}` | no | +| [cluster\_configuration](#input\_cluster\_configuration) | The execute command configuration for the cluster | `any` | `{}` | no | +| [cluster\_name](#input\_cluster\_name) | Name of the cluster (up to 255 letters, numbers, hyphens, and underscores) | `string` | `""` | no | +| [cluster\_service\_connect\_defaults](#input\_cluster\_service\_connect\_defaults) | Configures a default Service Connect namespace | `map(string)` | `{}` | no | +| [cluster\_settings](#input\_cluster\_settings) | List of configuration block(s) with cluster settings. For example, this can be used to enable CloudWatch Container Insights for a cluster | `any` |
[
{
"name": "containerInsights",
"value": "enabled"
}
]
| no | +| [create](#input\_create) | Determines whether resources will be created (affects all resources) | `bool` | `true` | no | +| [create\_cloudwatch\_log\_group](#input\_create\_cloudwatch\_log\_group) | Determines whether a log group is created by this module for the cluster logs. If not, AWS will automatically create one if logging is enabled | `bool` | `true` | no | +| [create\_task\_exec\_iam\_role](#input\_create\_task\_exec\_iam\_role) | Determines whether the ECS task definition IAM role should be created | `bool` | `false` | no | +| [create\_task\_exec\_policy](#input\_create\_task\_exec\_policy) | Determines whether the ECS task definition IAM policy should be created. This includes permissions included in AmazonECSTaskExecutionRolePolicy as well as access to secrets and SSM parameters | `bool` | `true` | no | +| [default\_capacity\_provider\_use\_fargate](#input\_default\_capacity\_provider\_use\_fargate) | Determines whether to use Fargate or autoscaling for default capacity provider strategy | `bool` | `true` | no | +| [fargate\_capacity\_providers](#input\_fargate\_capacity\_providers) | Map of Fargate capacity provider definitions to use for the cluster | `any` | `{}` | no | +| [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no | +| [task\_exec\_iam\_role\_description](#input\_task\_exec\_iam\_role\_description) | Description of the role | `string` | `null` | no | +| [task\_exec\_iam\_role\_name](#input\_task\_exec\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | +| [task\_exec\_iam\_role\_path](#input\_task\_exec\_iam\_role\_path) | IAM role path | `string` | `null` | no | +| [task\_exec\_iam\_role\_permissions\_boundary](#input\_task\_exec\_iam\_role\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [task\_exec\_iam\_role\_policies](#input\_task\_exec\_iam\_role\_policies) | Map of IAM role policy ARNs to attach to the IAM role | `map(string)` | `{}` | no | +| [task\_exec\_iam\_role\_tags](#input\_task\_exec\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no | +| [task\_exec\_iam\_role\_use\_name\_prefix](#input\_task\_exec\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`task_exec_iam_role_name`) is used as a prefix | `bool` | `true` | no | +| [task\_exec\_iam\_statements](#input\_task\_exec\_iam\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage | `any` | `{}` | no | +| [task\_exec\_secret\_arns](#input\_task\_exec\_secret\_arns) | List of SecretsManager secret ARNs the task execution role will be permitted to get/read | `list(string)` |
[
"arn:aws:secretsmanager:*:*:secret:*"
]
| no | +| [task\_exec\_ssm\_param\_arns](#input\_task\_exec\_ssm\_param\_arns) | List of SSM parameter ARNs the task execution role will be permitted to get/read | `list(string)` |
[
"arn:aws:ssm:*:*:parameter/*"
]
| no | + +## Outputs + +| Name | Description | +|------|-------------| +| [arn](#output\_arn) | ARN that identifies the cluster | +| [autoscaling\_capacity\_providers](#output\_autoscaling\_capacity\_providers) | Map of autoscaling capacity providers created and their attributes | +| [cloudwatch\_log\_group\_arn](#output\_cloudwatch\_log\_group\_arn) | ARN of CloudWatch log group created | +| [cloudwatch\_log\_group\_name](#output\_cloudwatch\_log\_group\_name) | Name of CloudWatch log group created | +| [cluster\_capacity\_providers](#output\_cluster\_capacity\_providers) | Map of cluster capacity providers attributes | +| [id](#output\_id) | ID that identifies the cluster | +| [name](#output\_name) | Name that identifies the cluster | +| [task\_exec\_iam\_role\_arn](#output\_task\_exec\_iam\_role\_arn) | Task execution IAM role ARN | +| [task\_exec\_iam\_role\_name](#output\_task\_exec\_iam\_role\_name) | Task execution IAM role name | +| [task\_exec\_iam\_role\_unique\_id](#output\_task\_exec\_iam\_role\_unique\_id) | Stable and unique string identifying the task execution IAM role | + + +## License + +Apache-2.0 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-ecs/blob/master/LICENSE). diff --git a/2-simple-example/iac/modules/cluster/main.tf b/2-simple-example/iac/modules/cluster/main.tf new file mode 100644 index 0000000..cecf7e4 --- /dev/null +++ b/2-simple-example/iac/modules/cluster/main.tf @@ -0,0 +1,324 @@ +################################################################################ +# Cluster +################################################################################ + +locals { + execute_command_configuration = { + logging = "OVERRIDE" + log_configuration = { + cloud_watch_log_group_name = try(aws_cloudwatch_log_group.this[0].name, null) + } + } +} + +resource "aws_ecs_cluster" "this" { + count = var.create ? 1 : 0 + + name = var.cluster_name + + dynamic "configuration" { + for_each = var.create_cloudwatch_log_group ? [var.cluster_configuration] : [] + + content { + dynamic "execute_command_configuration" { + for_each = try([merge(local.execute_command_configuration, configuration.value.execute_command_configuration)], [{}]) + + content { + kms_key_id = try(execute_command_configuration.value.kms_key_id, null) + logging = try(execute_command_configuration.value.logging, "DEFAULT") + + dynamic "log_configuration" { + for_each = try([execute_command_configuration.value.log_configuration], []) + + content { + cloud_watch_encryption_enabled = try(log_configuration.value.cloud_watch_encryption_enabled, null) + cloud_watch_log_group_name = try(log_configuration.value.cloud_watch_log_group_name, null) + s3_bucket_name = try(log_configuration.value.s3_bucket_name, null) + s3_bucket_encryption_enabled = try(log_configuration.value.s3_bucket_encryption_enabled, null) + s3_key_prefix = try(log_configuration.value.s3_key_prefix, null) + } + } + } + } + } + } + + dynamic "configuration" { + for_each = !var.create_cloudwatch_log_group && length(var.cluster_configuration) > 0 ? [var.cluster_configuration] : [] + + content { + dynamic "execute_command_configuration" { + for_each = try([configuration.value.execute_command_configuration], [{}]) + + content { + kms_key_id = try(execute_command_configuration.value.kms_key_id, null) + logging = try(execute_command_configuration.value.logging, "DEFAULT") + + dynamic "log_configuration" { + for_each = try([execute_command_configuration.value.log_configuration], []) + + content { + cloud_watch_encryption_enabled = try(log_configuration.value.cloud_watch_encryption_enabled, null) + cloud_watch_log_group_name = try(log_configuration.value.cloud_watch_log_group_name, null) + s3_bucket_name = try(log_configuration.value.s3_bucket_name, null) + s3_bucket_encryption_enabled = try(log_configuration.value.s3_bucket_encryption_enabled, null) + s3_key_prefix = try(log_configuration.value.s3_key_prefix, null) + } + } + } + } + } + } + + dynamic "service_connect_defaults" { + for_each = length(var.cluster_service_connect_defaults) > 0 ? [var.cluster_service_connect_defaults] : [] + + content { + namespace = service_connect_defaults.value.namespace + } + } + + dynamic "setting" { + for_each = flatten([var.cluster_settings]) + + content { + name = setting.value.name + value = setting.value.value + } + } + + tags = var.tags +} + +################################################################################ +# CloudWatch Log Group +################################################################################ +resource "aws_cloudwatch_log_group" "this" { + count = var.create && var.create_cloudwatch_log_group ? 1 : 0 + + name = try(coalesce(var.cloudwatch_log_group_name, "/aws/ecs/${var.cluster_name}"), "") + retention_in_days = var.cloudwatch_log_group_retention_in_days + kms_key_id = var.cloudwatch_log_group_kms_key_id + + tags = merge(var.tags, var.cloudwatch_log_group_tags) +} + +################################################################################ +# Cluster Capacity Providers +################################################################################ + +locals { + default_capacity_providers = merge( + { for k, v in var.fargate_capacity_providers : k => v if var.default_capacity_provider_use_fargate }, + { for k, v in var.autoscaling_capacity_providers : k => v if !var.default_capacity_provider_use_fargate } + ) +} + +resource "aws_ecs_cluster_capacity_providers" "this" { + count = var.create && length(merge(var.fargate_capacity_providers, var.autoscaling_capacity_providers)) > 0 ? 1 : 0 + + cluster_name = aws_ecs_cluster.this[0].name + capacity_providers = distinct(concat( + [for k, v in var.fargate_capacity_providers : try(v.name, k)], + [for k, v in var.autoscaling_capacity_providers : try(v.name, k)] + )) + + # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cluster-capacity-providers.html#capacity-providers-considerations + dynamic "default_capacity_provider_strategy" { + for_each = local.default_capacity_providers + iterator = strategy + + content { + capacity_provider = try(strategy.value.name, strategy.key) + base = try(strategy.value.default_capacity_provider_strategy.base, null) + weight = try(strategy.value.default_capacity_provider_strategy.weight, null) + } + } + + depends_on = [ + aws_ecs_capacity_provider.this + ] +} + +################################################################################ +# Capacity Provider - Autoscaling Group(s) +################################################################################ + +resource "aws_ecs_capacity_provider" "this" { + for_each = { for k, v in var.autoscaling_capacity_providers : k => v if var.create } + + name = try(each.value.name, each.key) + + auto_scaling_group_provider { + auto_scaling_group_arn = each.value.auto_scaling_group_arn + # When you use managed termination protection, you must also use managed scaling otherwise managed termination protection won't work + managed_termination_protection = length(try([each.value.managed_scaling], [])) == 0 ? "DISABLED" : try(each.value.managed_termination_protection, null) + + dynamic "managed_scaling" { + for_each = try([each.value.managed_scaling], []) + + content { + instance_warmup_period = try(managed_scaling.value.instance_warmup_period, null) + maximum_scaling_step_size = try(managed_scaling.value.maximum_scaling_step_size, null) + minimum_scaling_step_size = try(managed_scaling.value.minimum_scaling_step_size, null) + status = try(managed_scaling.value.status, null) + target_capacity = try(managed_scaling.value.target_capacity, null) + } + } + } + + tags = merge(var.tags, try(each.value.tags, {})) +} + +################################################################################ +# Task Execution - IAM Role +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html +################################################################################ + +locals { + task_exec_iam_role_name = try(coalesce(var.task_exec_iam_role_name, var.cluster_name), "") + + create_task_exec_iam_role = var.create && var.create_task_exec_iam_role + create_task_exec_policy = local.create_task_exec_iam_role && var.create_task_exec_policy +} + +data "aws_iam_policy_document" "task_exec_assume" { + count = local.create_task_exec_iam_role ? 1 : 0 + + statement { + sid = "ECSTaskExecutionAssumeRole" + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "task_exec" { + count = local.create_task_exec_iam_role ? 1 : 0 + + name = var.task_exec_iam_role_use_name_prefix ? null : local.task_exec_iam_role_name + name_prefix = var.task_exec_iam_role_use_name_prefix ? "${local.task_exec_iam_role_name}-" : null + path = var.task_exec_iam_role_path + description = coalesce(var.task_exec_iam_role_description, "Task execution role for ${var.cluster_name}") + + assume_role_policy = data.aws_iam_policy_document.task_exec_assume[0].json + permissions_boundary = var.task_exec_iam_role_permissions_boundary + force_detach_policies = true + + tags = merge(var.tags, var.task_exec_iam_role_tags) +} + +resource "aws_iam_role_policy_attachment" "task_exec_additional" { + for_each = { for k, v in var.task_exec_iam_role_policies : k => v if local.create_task_exec_iam_role } + + role = aws_iam_role.task_exec[0].name + policy_arn = each.value +} + +data "aws_iam_policy_document" "task_exec" { + count = local.create_task_exec_policy ? 1 : 0 + + # Pulled from AmazonECSTaskExecutionRolePolicy + statement { + sid = "Logs" + actions = [ + "logs:CreateLogStream", + "logs:PutLogEvents", + ] + resources = ["*"] + } + + # Pulled from AmazonECSTaskExecutionRolePolicy + statement { + sid = "ECR" + actions = [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + ] + resources = ["*"] + } + + dynamic "statement" { + for_each = length(var.task_exec_ssm_param_arns) > 0 ? [1] : [] + + content { + sid = "GetSSMParams" + actions = ["ssm:GetParameters"] + resources = var.task_exec_ssm_param_arns + } + } + + dynamic "statement" { + for_each = length(var.task_exec_secret_arns) > 0 ? [1] : [] + + content { + sid = "GetSecrets" + actions = ["secretsmanager:GetSecretValue"] + resources = var.task_exec_secret_arns + } + } + + dynamic "statement" { + for_each = var.task_exec_iam_statements + + content { + sid = try(statement.value.sid, null) + actions = try(statement.value.actions, null) + not_actions = try(statement.value.not_actions, null) + effect = try(statement.value.effect, null) + resources = try(statement.value.resources, null) + not_resources = try(statement.value.not_resources, null) + + dynamic "principals" { + for_each = try(statement.value.principals, []) + + content { + type = principals.value.type + identifiers = principals.value.identifiers + } + } + + dynamic "not_principals" { + for_each = try(statement.value.not_principals, []) + + content { + type = not_principals.value.type + identifiers = not_principals.value.identifiers + } + } + + dynamic "condition" { + for_each = try(statement.value.conditions, []) + + content { + test = condition.value.test + values = condition.value.values + variable = condition.value.variable + } + } + } + } +} + +resource "aws_iam_policy" "task_exec" { + count = local.create_task_exec_policy ? 1 : 0 + + name = var.task_exec_iam_role_use_name_prefix ? null : local.task_exec_iam_role_name + name_prefix = var.task_exec_iam_role_use_name_prefix ? "${local.task_exec_iam_role_name}-" : null + description = coalesce(var.task_exec_iam_role_description, "Task execution role IAM policy") + policy = data.aws_iam_policy_document.task_exec[0].json + + tags = merge(var.tags, var.task_exec_iam_role_tags) +} + +resource "aws_iam_role_policy_attachment" "task_exec" { + count = local.create_task_exec_policy ? 1 : 0 + + role = aws_iam_role.task_exec[0].name + policy_arn = aws_iam_policy.task_exec[0].arn +} diff --git a/2-simple-example/iac/modules/cluster/outputs.tf b/2-simple-example/iac/modules/cluster/outputs.tf new file mode 100644 index 0000000..4e05381 --- /dev/null +++ b/2-simple-example/iac/modules/cluster/outputs.tf @@ -0,0 +1,70 @@ +################################################################################ +# Cluster +################################################################################ + +output "arn" { + description = "ARN that identifies the cluster" + value = try(aws_ecs_cluster.this[0].arn, null) +} + +output "id" { + description = "ID that identifies the cluster" + value = try(aws_ecs_cluster.this[0].id, null) +} + +output "name" { + description = "Name that identifies the cluster" + value = try(aws_ecs_cluster.this[0].name, null) +} + +################################################################################ +# CloudWatch Log Group +################################################################################ + +output "cloudwatch_log_group_name" { + description = "Name of CloudWatch log group created" + value = try(aws_cloudwatch_log_group.this[0].name, null) +} + +output "cloudwatch_log_group_arn" { + description = "ARN of CloudWatch log group created" + value = try(aws_cloudwatch_log_group.this[0].arn, null) +} + +################################################################################ +# Cluster Capacity Providers +################################################################################ + +output "cluster_capacity_providers" { + description = "Map of cluster capacity providers attributes" + value = { for k, v in aws_ecs_cluster_capacity_providers.this : v.id => v } +} + +################################################################################ +# Capacity Provider - Autoscaling Group(s) +################################################################################ + +output "autoscaling_capacity_providers" { + description = "Map of autoscaling capacity providers created and their attributes" + value = aws_ecs_capacity_provider.this +} + +################################################################################ +# Task Execution - IAM Role +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html +################################################################################ + +output "task_exec_iam_role_name" { + description = "Task execution IAM role name" + value = try(aws_iam_role.task_exec[0].name, null) +} + +output "task_exec_iam_role_arn" { + description = "Task execution IAM role ARN" + value = try(aws_iam_role.task_exec[0].arn, null) +} + +output "task_exec_iam_role_unique_id" { + description = "Stable and unique string identifying the task execution IAM role" + value = try(aws_iam_role.task_exec[0].unique_id, null) +} diff --git a/2-simple-example/iac/modules/cluster/variables.tf b/2-simple-example/iac/modules/cluster/variables.tf new file mode 100644 index 0000000..6629743 --- /dev/null +++ b/2-simple-example/iac/modules/cluster/variables.tf @@ -0,0 +1,177 @@ +variable "create" { + description = "Determines whether resources will be created (affects all resources)" + type = bool + default = true +} + +variable "tags" { + description = "A map of tags to add to all resources" + type = map(string) + default = {} +} + +################################################################################ +# Cluster +################################################################################ + +variable "cluster_name" { + description = "Name of the cluster (up to 255 letters, numbers, hyphens, and underscores)" + type = string + default = "" +} + +variable "cluster_configuration" { + description = "The execute command configuration for the cluster" + type = any + default = {} +} + +variable "cluster_settings" { + description = "List of configuration block(s) with cluster settings. For example, this can be used to enable CloudWatch Container Insights for a cluster" + type = any + default = [ + { + name = "containerInsights" + value = "enabled" + } + ] +} + +variable "cluster_service_connect_defaults" { + description = "Configures a default Service Connect namespace" + type = map(string) + default = {} +} + +################################################################################ +# CloudWatch Log Group +################################################################################ + +variable "create_cloudwatch_log_group" { + description = "Determines whether a log group is created by this module for the cluster logs. If not, AWS will automatically create one if logging is enabled" + type = bool + default = true +} + +variable "cloudwatch_log_group_name" { + description = "Custom name of CloudWatch Log Group for ECS cluster" + type = string + default = null +} + +variable "cloudwatch_log_group_retention_in_days" { + description = "Number of days to retain log events" + type = number + default = 90 +} + +variable "cloudwatch_log_group_kms_key_id" { + description = "If a KMS Key ARN is set, this key will be used to encrypt the corresponding log group. Please be sure that the KMS Key has an appropriate key policy (https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html)" + type = string + default = null +} + +variable "cloudwatch_log_group_tags" { + description = "A map of additional tags to add to the log group created" + type = map(string) + default = {} +} + +################################################################################ +# Capacity Providers +################################################################################ + +variable "default_capacity_provider_use_fargate" { + description = "Determines whether to use Fargate or autoscaling for default capacity provider strategy" + type = bool + default = true +} + +variable "fargate_capacity_providers" { + description = "Map of Fargate capacity provider definitions to use for the cluster" + type = any + default = {} +} + +variable "autoscaling_capacity_providers" { + description = "Map of autoscaling capacity provider definitions to create for the cluster" + type = any + default = {} +} + +################################################################################ +# Task Execution - IAM Role +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html +################################################################################ + +variable "create_task_exec_iam_role" { + description = "Determines whether the ECS task definition IAM role should be created" + type = bool + default = false +} + +variable "task_exec_iam_role_name" { + description = "Name to use on IAM role created" + type = string + default = null +} + +variable "task_exec_iam_role_use_name_prefix" { + description = "Determines whether the IAM role name (`task_exec_iam_role_name`) is used as a prefix" + type = bool + default = true +} + +variable "task_exec_iam_role_path" { + description = "IAM role path" + type = string + default = null +} + +variable "task_exec_iam_role_description" { + description = "Description of the role" + type = string + default = null +} + +variable "task_exec_iam_role_permissions_boundary" { + description = "ARN of the policy that is used to set the permissions boundary for the IAM role" + type = string + default = null +} + +variable "task_exec_iam_role_tags" { + description = "A map of additional tags to add to the IAM role created" + type = map(string) + default = {} +} + +variable "task_exec_iam_role_policies" { + description = "Map of IAM role policy ARNs to attach to the IAM role" + type = map(string) + default = {} +} + +variable "create_task_exec_policy" { + description = "Determines whether the ECS task definition IAM policy should be created. This includes permissions included in AmazonECSTaskExecutionRolePolicy as well as access to secrets and SSM parameters" + type = bool + default = true +} + +variable "task_exec_ssm_param_arns" { + description = "List of SSM parameter ARNs the task execution role will be permitted to get/read" + type = list(string) + default = ["arn:aws:ssm:*:*:parameter/*"] +} + +variable "task_exec_secret_arns" { + description = "List of SecretsManager secret ARNs the task execution role will be permitted to get/read" + type = list(string) + default = ["arn:aws:secretsmanager:*:*:secret:*"] +} + +variable "task_exec_iam_statements" { + description = "A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage" + type = any + default = {} +} diff --git a/2-simple-example/iac/modules/cluster/versions.tf b/2-simple-example/iac/modules/cluster/versions.tf new file mode 100644 index 0000000..682191e --- /dev/null +++ b/2-simple-example/iac/modules/cluster/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.66.1" + } + } +} diff --git a/2-simple-example/iac/modules/container-definition/README.md b/2-simple-example/iac/modules/container-definition/README.md new file mode 100644 index 0000000..ada38d2 --- /dev/null +++ b/2-simple-example/iac/modules/container-definition/README.md @@ -0,0 +1,203 @@ +# Amazon ECS Container Definition Module + +Configuration in this directory creates an Amazon ECS container definition. + +The module defaults to creating and utilizing a CloudWatch log group. You can disable this behavior by setting `enable_cloudwatch_logging` = `false` - useful for scenarios where Firelens is used for log forwarding. + +For more details see the [design doc](https://github.com/terraform-aws-modules/terraform-aws-ecs/blob/master/docs/design.md) + +## Usage + +### Standard + +```hcl +module "ecs_container_definition" { + source = "terraform-aws-modules/ecs/aws//modules/container-definition" + + name = "example" + cpu = 512 + memory = 1024 + essential = true + image = "public.ecr.aws/aws-containers/ecsdemo-frontend:776fd50" + port_mappings = [ + { + name = "ecs-sample" + containerPort = 80 + protocol = "tcp" + } + ] + + # Example image used requires access to write to root filesystem + readonly_root_filesystem = false + + memory_reservation = 100 + + tags = { + Environment = "dev" + Terraform = "true" + } +} +``` + +### W/ Firelens + +```hcl +module "fluentbit_ecs_container_definition" { + source = "terraform-aws-modules/ecs/aws//modules/container-definition" + name = "fluent-bit" + + cpu = 512 + memory = 1024 + essential = true + image = "906394416424.dkr.ecr.us-west-2.amazonaws.com/aws-for-fluent-bit:stable" + firelens_configuration = { + type = "fluentbit" + } + memory_reservation = 50 + + tags = { + Environment = "dev" + Terraform = "true" + } +} + +module "example_ecs_container_definition" { + source = "terraform-aws-modules/ecs/aws//modules/container-definition" + + name = "example" + cpu = 512 + memory = 1024 + essential = true + image = "public.ecr.aws/aws-containers/ecsdemo-frontend:776fd50" + port_mappings = [ + { + name = "ecs-sample" + containerPort = 80 + protocol = "tcp" + } + ] + + # Example image used requires access to write to root filesystem + readonly_root_filesystem = false + + dependencies = [{ + containerName = "fluent-bit" + condition = "START" + }] + + enable_cloudwatch_logging = false + log_configuration = { + logDriver = "awsfirelens" + options = { + Name = "firehose" + region = "eu-west-1" + delivery_stream = "my-stream" + log-driver-buffer-limit = "2097152" + } + } + memory_reservation = 100 + + tags = { + Environment = "dev" + Terraform = "true" + } +} +``` + +## Examples + +- [ECS Cluster Complete](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/complete) +- [ECS Cluster w/ EC2 Autoscaling Capacity Provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/ec2-autoscaling) +- [ECS Cluster w/ Fargate Capacity Provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/fargate) + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | >= 4.66.1 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.66.1 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudwatch_log_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [cloudwatch\_log\_group\_kms\_key\_id](#input\_cloudwatch\_log\_group\_kms\_key\_id) | If a KMS Key ARN is set, this key will be used to encrypt the corresponding log group. Please be sure that the KMS Key has an appropriate key policy (https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html) | `string` | `null` | no | +| [cloudwatch\_log\_group\_name](#input\_cloudwatch\_log\_group\_name) | Custom name of CloudWatch log group for a service associated with the container definition | `string` | `null` | no | +| [cloudwatch\_log\_group\_retention\_in\_days](#input\_cloudwatch\_log\_group\_retention\_in\_days) | Number of days to retain log events. Default is 30 days | `number` | `30` | no | +| [cloudwatch\_log\_group\_use\_name\_prefix](#input\_cloudwatch\_log\_group\_use\_name\_prefix) | Determines whether the log group name should be used as a prefix | `bool` | `false` | no | +| [command](#input\_command) | The command that's passed to the container | `list(string)` | `[]` | no | +| [cpu](#input\_cpu) | The number of cpu units to reserve for the container. This is optional for tasks using Fargate launch type and the total amount of `cpu` of all containers in a task will need to be lower than the task-level cpu value | `number` | `null` | no | +| [create\_cloudwatch\_log\_group](#input\_create\_cloudwatch\_log\_group) | Determines whether a log group is created by this module. If not, AWS will automatically create one if logging is enabled | `bool` | `true` | no | +| [dependencies](#input\_dependencies) | The dependencies defined for container startup and shutdown. A container can contain multiple dependencies. When a dependency is defined for container startup, for container shutdown it is reversed. The condition can be one of START, COMPLETE, SUCCESS or HEALTHY |
list(object({
condition = string
containerName = string
}))
| `[]` | no | +| [disable\_networking](#input\_disable\_networking) | When this parameter is true, networking is disabled within the container | `bool` | `null` | no | +| [dns\_search\_domains](#input\_dns\_search\_domains) | Container DNS search domains. A list of DNS search domains that are presented to the container | `list(string)` | `[]` | no | +| [dns\_servers](#input\_dns\_servers) | Container DNS servers. This is a list of strings specifying the IP addresses of the DNS servers | `list(string)` | `[]` | no | +| [docker\_labels](#input\_docker\_labels) | A key/value map of labels to add to the container | `map(string)` | `{}` | no | +| [docker\_security\_options](#input\_docker\_security\_options) | A list of strings to provide custom labels for SELinux and AppArmor multi-level security systems. This field isn't valid for containers in tasks using the Fargate launch type | `list(string)` | `[]` | no | +| [enable\_cloudwatch\_logging](#input\_enable\_cloudwatch\_logging) | Determines whether CloudWatch logging is configured for this container definition. Set to `false` to use other logging drivers | `bool` | `true` | no | +| [enable\_execute\_command](#input\_enable\_execute\_command) | Specifies whether to enable Amazon ECS Exec for the tasks within the service | `bool` | `false` | no | +| [entrypoint](#input\_entrypoint) | The entry point that is passed to the container | `list(string)` | `[]` | no | +| [environment](#input\_environment) | The environment variables to pass to the container |
list(object({
name = string
value = string
}))
| `[]` | no | +| [environment\_files](#input\_environment\_files) | A list of files containing the environment variables to pass to a container |
list(object({
value = string
type = string
}))
| `[]` | no | +| [essential](#input\_essential) | If the `essential` parameter of a container is marked as `true`, and that container fails or stops for any reason, all other containers that are part of the task are stopped | `bool` | `null` | no | +| [extra\_hosts](#input\_extra\_hosts) | A list of hostnames and IP address mappings to append to the `/etc/hosts` file on the container |
list(object({
hostname = string
ipAddress = string
}))
| `[]` | no | +| [firelens\_configuration](#input\_firelens\_configuration) | The FireLens configuration for the container. This is used to specify and configure a log router for container logs. For more information, see [Custom Log Routing](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/using_firelens.html) in the Amazon Elastic Container Service Developer Guide | `any` | `{}` | no | +| [health\_check](#input\_health\_check) | The container health check command and associated configuration parameters for the container. See [HealthCheck](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_HealthCheck.html) | `any` | `{}` | no | +| [hostname](#input\_hostname) | The hostname to use for your container | `string` | `null` | no | +| [image](#input\_image) | The image used to start a container. This string is passed directly to the Docker daemon. By default, images in the Docker Hub registry are available. Other repositories are specified with either `repository-url/image:tag` or `repository-url/image@digest` | `string` | `null` | no | +| [interactive](#input\_interactive) | When this parameter is `true`, you can deploy containerized applications that require `stdin` or a `tty` to be allocated | `bool` | `false` | no | +| [links](#input\_links) | The links parameter allows containers to communicate with each other without the need for port mappings. This parameter is only supported if the network mode of a task definition is `bridge` | `list(string)` | `[]` | no | +| [linux\_parameters](#input\_linux\_parameters) | Linux-specific modifications that are applied to the container, such as Linux kernel capabilities. For more information see [KernelCapabilities](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_KernelCapabilities.html) | `any` | `{}` | no | +| [log\_configuration](#input\_log\_configuration) | The log configuration for the container. For more information see [LogConfiguration](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_LogConfiguration.html) | `any` | `{}` | no | +| [memory](#input\_memory) | The amount (in MiB) of memory to present to the container. If your container attempts to exceed the memory specified here, the container is killed. The total amount of memory reserved for all containers within a task must be lower than the task `memory` value, if one is specified | `number` | `null` | no | +| [memory\_reservation](#input\_memory\_reservation) | The soft limit (in MiB) of memory to reserve for the container. When system memory is under heavy contention, Docker attempts to keep the container memory to this soft limit. However, your container can consume more memory when it needs to, up to either the hard limit specified with the `memory` parameter (if applicable), or all of the available memory on the container instance | `number` | `null` | no | +| [mount\_points](#input\_mount\_points) | The mount points for data volumes in your container | `list(any)` | `[]` | no | +| [name](#input\_name) | The name of a container. If you're linking multiple containers together in a task definition, the name of one container can be entered in the links of another container to connect the containers. Up to 255 letters (uppercase and lowercase), numbers, underscores, and hyphens are allowed | `string` | `null` | no | +| [operating\_system\_family](#input\_operating\_system\_family) | The OS family for task | `string` | `"LINUX"` | no | +| [port\_mappings](#input\_port\_mappings) | The list of port mappings for the container. Port mappings allow containers to access ports on the host container instance to send or receive traffic. For task definitions that use the awsvpc network mode, only specify the containerPort. The hostPort can be left blank or it must be the same value as the containerPort | `list(any)` | `[]` | no | +| [privileged](#input\_privileged) | When this parameter is true, the container is given elevated privileges on the host container instance (similar to the root user) | `bool` | `false` | no | +| [pseudo\_terminal](#input\_pseudo\_terminal) | When this parameter is true, a `TTY` is allocated | `bool` | `false` | no | +| [readonly\_root\_filesystem](#input\_readonly\_root\_filesystem) | When this parameter is true, the container is given read-only access to its root file system | `bool` | `true` | no | +| [repository\_credentials](#input\_repository\_credentials) | Container repository credentials; required when using a private repo. This map currently supports a single key; "credentialsParameter", which should be the ARN of a Secrets Manager's secret holding the credentials | `map(string)` | `{}` | no | +| [resource\_requirements](#input\_resource\_requirements) | The type and amount of a resource to assign to a container. The only supported resource is a GPU |
list(object({
type = string
value = string
}))
| `[]` | no | +| [secrets](#input\_secrets) | The secrets to pass to the container. For more information, see [Specifying Sensitive Data](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html) in the Amazon Elastic Container Service Developer Guide |
list(object({
name = string
valueFrom = string
}))
| `[]` | no | +| [service](#input\_service) | The name of the service that the container definition is associated with | `string` | `""` | no | +| [start\_timeout](#input\_start\_timeout) | Time duration (in seconds) to wait before giving up on resolving dependencies for a container | `number` | `30` | no | +| [stop\_timeout](#input\_stop\_timeout) | Time duration (in seconds) to wait before the container is forcefully killed if it doesn't exit normally on its own | `number` | `120` | no | +| [system\_controls](#input\_system\_controls) | A list of namespaced kernel parameters to set in the container | `list(map(string))` | `[]` | no | +| [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no | +| [ulimits](#input\_ulimits) | A list of ulimits to set in the container. If a ulimit value is specified in a task definition, it overrides the default values set by Docker |
list(object({
hardLimit = number
name = string
softLimit = number
}))
| `[]` | no | +| [user](#input\_user) | The user to run as inside the container. Can be any of these formats: user, user:group, uid, uid:gid, user:gid, uid:group. The default (null) will use the container's configured `USER` directive or root if not set | `string` | `null` | no | +| [volumes\_from](#input\_volumes\_from) | Data volumes to mount from another container | `list(any)` | `[]` | no | +| [working\_directory](#input\_working\_directory) | The working directory to run commands inside the container | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [cloudwatch\_log\_group\_arn](#output\_cloudwatch\_log\_group\_arn) | ARN of CloudWatch log group created | +| [cloudwatch\_log\_group\_name](#output\_cloudwatch\_log\_group\_name) | Name of CloudWatch log group created | +| [container\_definition](#output\_container\_definition) | Container definition | + + +## License + +Apache-2.0 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-ecs/blob/master/LICENSE). diff --git a/2-simple-example/iac/modules/container-definition/main.tf b/2-simple-example/iac/modules/container-definition/main.tf new file mode 100644 index 0000000..682fc94 --- /dev/null +++ b/2-simple-example/iac/modules/container-definition/main.tf @@ -0,0 +1,83 @@ +data "aws_region" "current" {} + +locals { + is_not_windows = contains(["LINUX"], var.operating_system_family) + + log_group_name = try(coalesce(var.cloudwatch_log_group_name, "/aws/ecs/${var.service}/${var.name}"), "") + + log_configuration = merge( + { for k, v in { + logDriver = "awslogs", + options = { + awslogs-region = data.aws_region.current.name, + awslogs-group = try(aws_cloudwatch_log_group.this[0].name, ""), + awslogs-stream-prefix = "ecs" + }, + } : k => v if var.enable_cloudwatch_logging }, + var.log_configuration + ) + + linux_parameters = var.enable_execute_command ? merge({ "initProcessEnabled" : true }, var.linux_parameters) : merge({ "initProcessEnabled" : false }, var.linux_parameters) + + health_check = length(var.health_check) > 0 ? merge({ + interval = 30, + retries = 3, + timeout = 5 + }, var.health_check) : null + + definition = { + command = length(var.command) > 0 ? var.command : null + cpu = var.cpu + dependsOn = length(var.dependencies) > 0 ? var.dependencies : null # depends_on is a reserved word + disableNetworking = local.is_not_windows ? var.disable_networking : null + dnsSearchDomains = local.is_not_windows && length(var.dns_search_domains) > 0 ? var.dns_search_domains : null + dnsServers = local.is_not_windows && length(var.dns_servers) > 0 ? var.dns_servers : null + dockerLabels = length(var.docker_labels) > 0 ? var.docker_labels : null + dockerSecurityOptions = length(var.docker_security_options) > 0 ? var.docker_security_options : null + entrypoint = length(var.entrypoint) > 0 ? var.entrypoint : null + environment = var.environment + environmentFiles = length(var.environment_files) > 0 ? var.environment_files : null + essential = var.essential + extraHosts = local.is_not_windows && length(var.extra_hosts) > 0 ? var.extra_hosts : null + firelensConfiguration = length(var.firelens_configuration) > 0 ? var.firelens_configuration : null + healthCheck = local.health_check + hostname = var.hostname + image = var.image + interactive = var.interactive + links = local.is_not_windows && length(var.links) > 0 ? var.links : null + linuxParameters = local.is_not_windows && length(local.linux_parameters) > 0 ? local.linux_parameters : null + logConfiguration = length(local.log_configuration) > 0 ? local.log_configuration : null + memory = var.memory + memoryReservation = var.memory_reservation + mountPoints = var.mount_points + name = var.name + portMappings = var.port_mappings + privileged = local.is_not_windows ? var.privileged : null + pseudoTerminal = var.pseudo_terminal + readonlyRootFilesystem = local.is_not_windows ? var.readonly_root_filesystem : null + repositoryCredentials = length(var.repository_credentials) > 0 ? var.repository_credentials : null + resourceRequirements = length(var.resource_requirements) > 0 ? var.resource_requirements : null + secrets = length(var.secrets) > 0 ? var.secrets : null + startTimeout = var.start_timeout + stopTimeout = var.stop_timeout + systemControls = length(var.system_controls) > 0 ? var.system_controls : [] + ulimits = local.is_not_windows && length(var.ulimits) > 0 ? var.ulimits : null + user = local.is_not_windows ? var.user : null + volumesFrom = var.volumes_from + workingDirectory = var.working_directory + } + + # Strip out all null values, ECS API will provide defaults in place of null/empty values + container_definition = { for k, v in local.definition : k => v if v != null } +} + +resource "aws_cloudwatch_log_group" "this" { + count = var.create_cloudwatch_log_group && var.enable_cloudwatch_logging ? 1 : 0 + + name = var.cloudwatch_log_group_use_name_prefix ? null : local.log_group_name + name_prefix = var.cloudwatch_log_group_use_name_prefix ? "${local.log_group_name}-" : null + retention_in_days = var.cloudwatch_log_group_retention_in_days + kms_key_id = var.cloudwatch_log_group_kms_key_id + + tags = var.tags +} diff --git a/2-simple-example/iac/modules/container-definition/outputs.tf b/2-simple-example/iac/modules/container-definition/outputs.tf new file mode 100644 index 0000000..2f26967 --- /dev/null +++ b/2-simple-example/iac/modules/container-definition/outputs.tf @@ -0,0 +1,22 @@ +################################################################################ +# Container Definition +################################################################################ + +output "container_definition" { + description = "Container definition" + value = local.container_definition +} + +################################################################################ +# CloudWatch Log Group +################################################################################ + +output "cloudwatch_log_group_name" { + description = "Name of CloudWatch log group created" + value = try(aws_cloudwatch_log_group.this[0].name, null) +} + +output "cloudwatch_log_group_arn" { + description = "ARN of CloudWatch log group created" + value = try(aws_cloudwatch_log_group.this[0].arn, null) +} diff --git a/2-simple-example/iac/modules/container-definition/variables.tf b/2-simple-example/iac/modules/container-definition/variables.tf new file mode 100644 index 0000000..0f88b9d --- /dev/null +++ b/2-simple-example/iac/modules/container-definition/variables.tf @@ -0,0 +1,323 @@ +variable "operating_system_family" { + description = "The OS family for task" + type = string + default = "LINUX" +} + +################################################################################ +# Container Definition +################################################################################ + +variable "command" { + description = "The command that's passed to the container" + type = list(string) + default = [] +} + +variable "cpu" { + description = "The number of cpu units to reserve for the container. This is optional for tasks using Fargate launch type and the total amount of `cpu` of all containers in a task will need to be lower than the task-level cpu value" + type = number + default = null +} + +variable "dependencies" { + description = "The dependencies defined for container startup and shutdown. A container can contain multiple dependencies. When a dependency is defined for container startup, for container shutdown it is reversed. The condition can be one of START, COMPLETE, SUCCESS or HEALTHY" + type = list(object({ + condition = string + containerName = string + })) + default = [] +} + +variable "disable_networking" { + description = "When this parameter is true, networking is disabled within the container" + type = bool + default = null +} + +variable "dns_search_domains" { + description = "Container DNS search domains. A list of DNS search domains that are presented to the container" + type = list(string) + default = [] +} + +variable "dns_servers" { + description = "Container DNS servers. This is a list of strings specifying the IP addresses of the DNS servers" + type = list(string) + default = [] +} + +variable "docker_labels" { + description = "A key/value map of labels to add to the container" + type = map(string) + default = {} +} + +variable "docker_security_options" { + description = "A list of strings to provide custom labels for SELinux and AppArmor multi-level security systems. This field isn't valid for containers in tasks using the Fargate launch type" + type = list(string) + default = [] +} + +variable "enable_execute_command" { + description = "Specifies whether to enable Amazon ECS Exec for the tasks within the service" + type = bool + default = false +} + +variable "entrypoint" { + description = "The entry point that is passed to the container" + type = list(string) + default = [] +} + +variable "environment" { + description = "The environment variables to pass to the container" + type = list(object({ + name = string + value = string + })) + default = [] +} + +variable "environment_files" { + description = "A list of files containing the environment variables to pass to a container" + type = list(object({ + value = string + type = string + })) + default = [] +} + +variable "essential" { + description = "If the `essential` parameter of a container is marked as `true`, and that container fails or stops for any reason, all other containers that are part of the task are stopped" + type = bool + default = null +} + +variable "extra_hosts" { + description = "A list of hostnames and IP address mappings to append to the `/etc/hosts` file on the container" + type = list(object({ + hostname = string + ipAddress = string + })) + default = [] +} + +variable "firelens_configuration" { + description = "The FireLens configuration for the container. This is used to specify and configure a log router for container logs. For more information, see [Custom Log Routing](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/using_firelens.html) in the Amazon Elastic Container Service Developer Guide" + type = any + default = {} +} + +variable "health_check" { + description = "The container health check command and associated configuration parameters for the container. See [HealthCheck](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_HealthCheck.html)" + type = any + default = {} +} + +variable "hostname" { + description = "The hostname to use for your container" + type = string + default = null +} + +variable "image" { + description = "The image used to start a container. This string is passed directly to the Docker daemon. By default, images in the Docker Hub registry are available. Other repositories are specified with either `repository-url/image:tag` or `repository-url/image@digest`" + type = string + default = null +} + +variable "interactive" { + description = "When this parameter is `true`, you can deploy containerized applications that require `stdin` or a `tty` to be allocated" + type = bool + default = false +} + +variable "links" { + description = "The links parameter allows containers to communicate with each other without the need for port mappings. This parameter is only supported if the network mode of a task definition is `bridge`" + type = list(string) + default = [] +} + +variable "linux_parameters" { + description = "Linux-specific modifications that are applied to the container, such as Linux kernel capabilities. For more information see [KernelCapabilities](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_KernelCapabilities.html)" + type = any + default = {} +} + +variable "log_configuration" { + description = "The log configuration for the container. For more information see [LogConfiguration](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_LogConfiguration.html)" + type = any + default = {} +} + +variable "memory" { + description = "The amount (in MiB) of memory to present to the container. If your container attempts to exceed the memory specified here, the container is killed. The total amount of memory reserved for all containers within a task must be lower than the task `memory` value, if one is specified" + type = number + default = null +} + +variable "memory_reservation" { + description = "The soft limit (in MiB) of memory to reserve for the container. When system memory is under heavy contention, Docker attempts to keep the container memory to this soft limit. However, your container can consume more memory when it needs to, up to either the hard limit specified with the `memory` parameter (if applicable), or all of the available memory on the container instance" + type = number + default = null +} + +variable "mount_points" { + description = "The mount points for data volumes in your container" + type = list(any) + default = [] +} + +variable "name" { + description = "The name of a container. If you're linking multiple containers together in a task definition, the name of one container can be entered in the links of another container to connect the containers. Up to 255 letters (uppercase and lowercase), numbers, underscores, and hyphens are allowed" + type = string + default = null +} + +variable "port_mappings" { + description = "The list of port mappings for the container. Port mappings allow containers to access ports on the host container instance to send or receive traffic. For task definitions that use the awsvpc network mode, only specify the containerPort. The hostPort can be left blank or it must be the same value as the containerPort" + type = list(any) + default = [] +} + +variable "privileged" { + description = "When this parameter is true, the container is given elevated privileges on the host container instance (similar to the root user)" + type = bool + default = false +} + +variable "pseudo_terminal" { + description = "When this parameter is true, a `TTY` is allocated" + type = bool + default = false +} + +variable "readonly_root_filesystem" { + description = "When this parameter is true, the container is given read-only access to its root file system" + type = bool + default = true +} + +variable "repository_credentials" { + description = "Container repository credentials; required when using a private repo. This map currently supports a single key; \"credentialsParameter\", which should be the ARN of a Secrets Manager's secret holding the credentials" + type = map(string) + default = {} +} + +variable "resource_requirements" { + description = "The type and amount of a resource to assign to a container. The only supported resource is a GPU" + type = list(object({ + type = string + value = string + })) + default = [] +} + +variable "secrets" { + description = "The secrets to pass to the container. For more information, see [Specifying Sensitive Data](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html) in the Amazon Elastic Container Service Developer Guide" + type = list(object({ + name = string + valueFrom = string + })) + default = [] +} + +variable "start_timeout" { + description = "Time duration (in seconds) to wait before giving up on resolving dependencies for a container" + type = number + default = 30 +} + +variable "stop_timeout" { + description = "Time duration (in seconds) to wait before the container is forcefully killed if it doesn't exit normally on its own" + type = number + default = 120 +} + +variable "system_controls" { + description = "A list of namespaced kernel parameters to set in the container" + type = list(map(string)) + default = [] +} + +variable "ulimits" { + description = "A list of ulimits to set in the container. If a ulimit value is specified in a task definition, it overrides the default values set by Docker" + type = list(object({ + hardLimit = number + name = string + softLimit = number + })) + default = [] +} + +variable "user" { + description = "The user to run as inside the container. Can be any of these formats: user, user:group, uid, uid:gid, user:gid, uid:group. The default (null) will use the container's configured `USER` directive or root if not set" + type = string + default = null +} + +variable "volumes_from" { + description = "Data volumes to mount from another container" + type = list(any) + default = [] +} + +variable "working_directory" { + description = "The working directory to run commands inside the container" + type = string + default = null +} + +################################################################################ +# CloudWatch Log Group +################################################################################ + +variable "service" { + description = "The name of the service that the container definition is associated with" + type = string + default = "" +} + +variable "enable_cloudwatch_logging" { + description = "Determines whether CloudWatch logging is configured for this container definition. Set to `false` to use other logging drivers" + type = bool + default = true +} + +variable "create_cloudwatch_log_group" { + description = "Determines whether a log group is created by this module. If not, AWS will automatically create one if logging is enabled" + type = bool + default = true +} + +variable "cloudwatch_log_group_name" { + description = "Custom name of CloudWatch log group for a service associated with the container definition" + type = string + default = null +} + +variable "cloudwatch_log_group_use_name_prefix" { + description = "Determines whether the log group name should be used as a prefix" + type = bool + default = false +} + +variable "cloudwatch_log_group_retention_in_days" { + description = "Number of days to retain log events. Default is 30 days" + type = number + default = 30 +} + +variable "cloudwatch_log_group_kms_key_id" { + description = "If a KMS Key ARN is set, this key will be used to encrypt the corresponding log group. Please be sure that the KMS Key has an appropriate key policy (https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html)" + type = string + default = null +} + +variable "tags" { + description = "A map of tags to add to all resources" + type = map(string) + default = {} +} diff --git a/2-simple-example/iac/modules/container-definition/versions.tf b/2-simple-example/iac/modules/container-definition/versions.tf new file mode 100644 index 0000000..682191e --- /dev/null +++ b/2-simple-example/iac/modules/container-definition/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.66.1" + } + } +} diff --git a/2-simple-example/iac/modules/service/README.md b/2-simple-example/iac/modules/service/README.md new file mode 100644 index 0000000..2101fd3 --- /dev/null +++ b/2-simple-example/iac/modules/service/README.md @@ -0,0 +1,358 @@ +# Amazon ECS Service Module + +Configuration in this directory creates an Amazon ECS Service and associated resources. + +Some notable configurations to be aware of when using this module: +1. `desired_count`/`scale` is always ignored; the module is designed to utilize autoscaling by default (though it can be disabled) +2. The default configuration is intended for `FARGATE` use + +For more details see the [design doc](https://github.com/terraform-aws-modules/terraform-aws-ecs/blob/master/docs/README.md) + +### Logging + +Please refer to [FireLens examples repository](https://github.com/aws-samples/amazon-ecs-firelens-examples) for logging configuration examples for FireLens on Amazon ECS and AWS Fargate. + +## Usage + +```hcl +module "ecs_service" { + source = "terraform-aws-modules/ecs/aws//modules/service" + + name = "example" + cluster_arn = "arn:aws:ecs:us-west-2:123456789012:cluster/default" + + cpu = 1024 + memory = 4096 + + # Container definition(s) + container_definitions = { + + fluent-bit = { + cpu = 512 + memory = 1024 + essential = true + image = "906394416424.dkr.ecr.us-west-2.amazonaws.com/aws-for-fluent-bit:stable" + firelens_configuration = { + type = "fluentbit" + } + memory_reservation = 50 + } + + ecs-sample = { + cpu = 512 + memory = 1024 + essential = true + image = "public.ecr.aws/aws-containers/ecsdemo-frontend:776fd50" + port_mappings = [ + { + name = "ecs-sample" + containerPort = 80 + protocol = "tcp" + } + ] + + # Example image used requires access to write to root filesystem + readonly_root_filesystem = false + + dependencies = [{ + containerName = "fluent-bit" + condition = "START" + }] + + enable_cloudwatch_logging = false + log_configuration = { + logDriver = "awsfirelens" + options = { + Name = "firehose" + region = "eu-west-1" + delivery_stream = "my-stream" + log-driver-buffer-limit = "2097152" + } + } + memory_reservation = 100 + } + } + + service_connect_configuration = { + namespace = "example" + service = { + client_alias = { + port = 80 + dns_name = "ecs-sample" + } + port_name = "ecs-sample" + discovery_name = "ecs-sample" + } + } + + load_balancer = { + service = { + target_group_arn = "arn:aws:elasticloadbalancing:eu-west-1:1234567890:targetgroup/bluegreentarget1/209a844cd01825a4" + container_name = "ecs-sample" + container_port = 80 + } + } + + subnet_ids = ["subnet-abcde012", "subnet-bcde012a", "subnet-fghi345a"] + security_group_rules = { + alb_ingress_3000 = { + type = "ingress" + from_port = 80 + to_port = 80 + protocol = "tcp" + description = "Service port" + source_security_group_id = "sg-12345678" + } + egress_all = { + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + } + + tags = { + Environment = "dev" + Terraform = "true" + } +} +``` + +## Conditional Creation + +The following values are provided to toggle on/off creation of the associated resources as desired: + +```hcl +module "ecs_service" { + source = "terraform-aws-modules/ecs/aws//modules/service" + + # Disable creation of service and all resources + create = false + + # Enable ECS Exec + enable_execute_command = true + + # Disable creation of the service IAM role; `iam_role_arn` should be provided + create_iam_role = false + + # Disable creation of the task definition; `task_definition_arn` should be provided + create_task_definition = false + + # Disable creation of the task execution IAM role; `task_exec_iam_role_arn` should be provided + create_task_exec_iam_role = false + + # Disable creation of the task execution IAM role policy + create_task_exec_policy = false + + # Disable creation of the tasks IAM role; `tasks_iam_role_arn` should be provided + create_tasks_iam_role = false + + # Disable creation of the service security group + create_security_group = false + + # ... omitted +} +``` + +## Examples + +- [ECS Cluster Complete](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/complete) +- [ECS Cluster w/ EC2 Autoscaling Capacity Provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/ec2-autoscaling) +- [ECS Cluster w/ Fargate Capacity Provider](https://github.com/terraform-aws-modules/terraform-aws-ecs/tree/master/examples/fargate) + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | >= 4.66.1 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.66.1 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [container\_definition](#module\_container\_definition) | ../container-definition | n/a | + +## Resources + +| Name | Type | +|------|------| +| [aws_appautoscaling_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/appautoscaling_policy) | resource | +| [aws_appautoscaling_scheduled_action.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/appautoscaling_scheduled_action) | resource | +| [aws_appautoscaling_target.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/appautoscaling_target) | resource | +| [aws_ecs_service.ignore_task_definition](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service) | resource | +| [aws_ecs_service.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service) | resource | +| [aws_ecs_task_definition.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition) | resource | +| [aws_ecs_task_set.ignore_task_definition](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_set) | resource | +| [aws_ecs_task_set.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_set) | resource | +| [aws_iam_policy.service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.tasks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy.tasks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy_attachment.service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.task_exec_additional](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.tasks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_security_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_security_group_rule.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_ecs_task_definition.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ecs_task_definition) | data source | +| [aws_iam_policy_document.service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.service_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.task_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.task_exec_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.tasks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.tasks_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | +| [aws_subnet.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [alarms](#input\_alarms) | Information about the CloudWatch alarms | `any` | `{}` | no | +| [assign\_public\_ip](#input\_assign\_public\_ip) | Assign a public IP address to the ENI (Fargate launch type only) | `bool` | `false` | no | +| [autoscaling\_max\_capacity](#input\_autoscaling\_max\_capacity) | Maximum number of tasks to run in your service | `number` | `10` | no | +| [autoscaling\_min\_capacity](#input\_autoscaling\_min\_capacity) | Minimum number of tasks to run in your service | `number` | `1` | no | +| [autoscaling\_policies](#input\_autoscaling\_policies) | Map of autoscaling policies to create for the service | `any` |
{
"cpu": {
"policy_type": "TargetTrackingScaling",
"target_tracking_scaling_policy_configuration": {
"predefined_metric_specification": {
"predefined_metric_type": "ECSServiceAverageCPUUtilization"
}
}
},
"memory": {
"policy_type": "TargetTrackingScaling",
"target_tracking_scaling_policy_configuration": {
"predefined_metric_specification": {
"predefined_metric_type": "ECSServiceAverageMemoryUtilization"
}
}
}
}
| no | +| [autoscaling\_scheduled\_actions](#input\_autoscaling\_scheduled\_actions) | Map of autoscaling scheduled actions to create for the service | `any` | `{}` | no | +| [capacity\_provider\_strategy](#input\_capacity\_provider\_strategy) | Capacity provider strategies to use for the service. Can be one or more | `any` | `{}` | no | +| [cluster\_arn](#input\_cluster\_arn) | ARN of the ECS cluster where the resources will be provisioned | `string` | `""` | no | +| [container\_definition\_defaults](#input\_container\_definition\_defaults) | A map of default values for [container definitions](http://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerDefinition.html) created by `container_definitions` | `any` | `{}` | no | +| [container\_definitions](#input\_container\_definitions) | A map of valid [container definitions](http://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerDefinition.html). Please note that you should only provide values that are part of the container definition document | `any` | `{}` | no | +| [cpu](#input\_cpu) | Number of cpu units used by the task. If the `requires_compatibilities` is `FARGATE` this field is required | `number` | `1024` | no | +| [create](#input\_create) | Determines whether resources will be created (affects all resources) | `bool` | `true` | no | +| [create\_iam\_role](#input\_create\_iam\_role) | Determines whether the ECS service IAM role should be created | `bool` | `true` | no | +| [create\_security\_group](#input\_create\_security\_group) | Determines if a security group is created | `bool` | `true` | no | +| [create\_service](#input\_create\_service) | Determines whether service resource will be created (set to `false` in case you want to create task definition only) | `bool` | `true` | no | +| [create\_task\_definition](#input\_create\_task\_definition) | Determines whether to create a task definition or use existing/provided | `bool` | `true` | no | +| [create\_task\_exec\_iam\_role](#input\_create\_task\_exec\_iam\_role) | Determines whether the ECS task definition IAM role should be created | `bool` | `true` | no | +| [create\_task\_exec\_policy](#input\_create\_task\_exec\_policy) | Determines whether the ECS task definition IAM policy should be created. This includes permissions included in AmazonECSTaskExecutionRolePolicy as well as access to secrets and SSM parameters | `bool` | `true` | no | +| [create\_tasks\_iam\_role](#input\_create\_tasks\_iam\_role) | Determines whether the ECS tasks IAM role should be created | `bool` | `true` | no | +| [deployment\_circuit\_breaker](#input\_deployment\_circuit\_breaker) | Configuration block for deployment circuit breaker | `any` | `{}` | no | +| [deployment\_controller](#input\_deployment\_controller) | Configuration block for deployment controller configuration | `any` | `{}` | 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 | `number` | `200` | no | +| [deployment\_minimum\_healthy\_percent](#input\_deployment\_minimum\_healthy\_percent) | Lower limit (as a percentage of the service's `desired_count`) of the number of running tasks that must remain running and healthy in a service during a deployment | `number` | `66` | no | +| [desired\_count](#input\_desired\_count) | Number of instances of the task definition to place and keep running | `number` | `1` | no | +| [enable\_autoscaling](#input\_enable\_autoscaling) | Determines whether to enable autoscaling for the service | `bool` | `true` | no | +| [enable\_ecs\_managed\_tags](#input\_enable\_ecs\_managed\_tags) | Specifies whether to enable Amazon ECS managed tags for the tasks within the service | `bool` | `true` | no | +| [enable\_execute\_command](#input\_enable\_execute\_command) | Specifies whether to enable Amazon ECS Exec for the tasks within the service | `bool` | `false` | no | +| [ephemeral\_storage](#input\_ephemeral\_storage) | The amount of ephemeral storage to allocate for the task. This parameter is used to expand the total amount of ephemeral storage available, beyond the default amount, for tasks hosted on AWS Fargate | `any` | `{}` | no | +| [external\_id](#input\_external\_id) | The external ID associated with the task set | `string` | `null` | no | +| [family](#input\_family) | A unique name for your task definition | `string` | `null` | no | +| [force\_delete](#input\_force\_delete) | Whether to allow deleting the task set without waiting for scaling down to 0 | `bool` | `null` | no | +| [force\_new\_deployment](#input\_force\_new\_deployment) | Enable to force a new task deployment of the service. This can be used to update tasks to use a newer Docker image with same image/tag combination, roll Fargate tasks onto a newer platform version, or immediately deploy `ordered_placement_strategy` and `placement_constraints` updates | `bool` | `true` | no | +| [health\_check\_grace\_period\_seconds](#input\_health\_check\_grace\_period\_seconds) | Seconds to ignore failing load balancer health checks on newly instantiated tasks to prevent premature shutdown, up to 2147483647. Only valid for services configured to use load balancers | `number` | `null` | no | +| [iam\_role\_arn](#input\_iam\_role\_arn) | Existing IAM role ARN | `string` | `null` | no | +| [iam\_role\_description](#input\_iam\_role\_description) | Description of the role | `string` | `null` | no | +| [iam\_role\_name](#input\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | +| [iam\_role\_path](#input\_iam\_role\_path) | IAM role path | `string` | `null` | no | +| [iam\_role\_permissions\_boundary](#input\_iam\_role\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [iam\_role\_statements](#input\_iam\_role\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage | `any` | `{}` | no | +| [iam\_role\_tags](#input\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no | +| [iam\_role\_use\_name\_prefix](#input\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`iam_role_name`) is used as a prefix | `bool` | `true` | no | +| [ignore\_task\_definition\_changes](#input\_ignore\_task\_definition\_changes) | Whether changes to service `task_definition` changes should be ignored | `bool` | `false` | no | +| [inference\_accelerator](#input\_inference\_accelerator) | Configuration block(s) with Inference Accelerators settings | `any` | `{}` | no | +| [ipc\_mode](#input\_ipc\_mode) | IPC resource namespace to be used for the containers in the task The valid values are `host`, `task`, and `none` | `string` | `null` | no | +| [launch\_type](#input\_launch\_type) | Launch type on which to run your service. The valid values are `EC2`, `FARGATE`, and `EXTERNAL`. Defaults to `FARGATE` | `string` | `"FARGATE"` | no | +| [load\_balancer](#input\_load\_balancer) | Configuration block for load balancers | `any` | `{}` | no | +| [memory](#input\_memory) | Amount (in MiB) of memory used by the task. If the `requires_compatibilities` is `FARGATE` this field is required | `number` | `2048` | no | +| [name](#input\_name) | Name of the service (up to 255 letters, numbers, hyphens, and underscores) | `string` | `null` | no | +| [network\_mode](#input\_network\_mode) | Docker networking mode to use for the containers in the task. Valid values are `none`, `bridge`, `awsvpc`, and `host` | `string` | `"awsvpc"` | no | +| [ordered\_placement\_strategy](#input\_ordered\_placement\_strategy) | Service level strategy rules that are taken into consideration during task placement. List from top to bottom in order of precedence | `any` | `{}` | no | +| [pid\_mode](#input\_pid\_mode) | Process namespace to use for the containers in the task. The valid values are `host` and `task` | `string` | `null` | no | +| [placement\_constraints](#input\_placement\_constraints) | Configuration block for rules that are taken into consideration during task placement (up to max of 10). This is set at the service, see `task_definition_placement_constraints` for setting at the task definition | `any` | `{}` | no | +| [platform\_version](#input\_platform\_version) | Platform version on which to run your service. Only applicable for `launch_type` set to `FARGATE`. Defaults to `LATEST` | `string` | `null` | no | +| [propagate\_tags](#input\_propagate\_tags) | Specifies whether to propagate the tags from the task definition or the service to the tasks. The valid values are `SERVICE` and `TASK_DEFINITION` | `string` | `null` | no | +| [proxy\_configuration](#input\_proxy\_configuration) | Configuration block for the App Mesh proxy | `any` | `{}` | no | +| [requires\_compatibilities](#input\_requires\_compatibilities) | Set of launch types required by the task. The valid values are `EC2` and `FARGATE` | `list(string)` |
[
"FARGATE"
]
| no | +| [runtime\_platform](#input\_runtime\_platform) | Configuration block for `runtime_platform` that containers in your task may use | `any` |
{
"cpu_architecture": "X86_64",
"operating_system_family": "LINUX"
}
| no | +| [scale](#input\_scale) | A floating-point percentage of the desired number of tasks to place and keep running in the task set | `any` | `{}` | no | +| [scheduling\_strategy](#input\_scheduling\_strategy) | Scheduling strategy to use for the service. The valid values are `REPLICA` and `DAEMON`. Defaults to `REPLICA` | `string` | `null` | no | +| [security\_group\_description](#input\_security\_group\_description) | Description of the security group created | `string` | `null` | no | +| [security\_group\_ids](#input\_security\_group\_ids) | List of security groups to associate with the task or service | `list(string)` | `[]` | no | +| [security\_group\_name](#input\_security\_group\_name) | Name to use on security group created | `string` | `null` | no | +| [security\_group\_rules](#input\_security\_group\_rules) | Security group rules to add to the security group created | `any` | `{}` | no | +| [security\_group\_tags](#input\_security\_group\_tags) | A map of additional tags to add to the security group created | `map(string)` | `{}` | no | +| [security\_group\_use\_name\_prefix](#input\_security\_group\_use\_name\_prefix) | Determines whether the security group name (`security_group_name`) is used as a prefix | `bool` | `true` | no | +| [service\_connect\_configuration](#input\_service\_connect\_configuration) | The ECS Service Connect configuration for this service to discover and connect to services, and be discovered by, and connected from, other services within a namespace | `any` | `{}` | no | +| [service\_registries](#input\_service\_registries) | Service discovery registries for the service | `any` | `{}` | no | +| [service\_tags](#input\_service\_tags) | A map of additional tags to add to the service | `map(string)` | `{}` | no | +| [skip\_destroy](#input\_skip\_destroy) | If true, the task is not deleted when the service is deleted | `bool` | `null` | no | +| [subnet\_ids](#input\_subnet\_ids) | List of subnets to associate with the task or service | `list(string)` | `[]` | no | +| [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no | +| [task\_definition\_arn](#input\_task\_definition\_arn) | Existing task definition ARN. Required when `create_task_definition` is `false` | `string` | `null` | no | +| [task\_definition\_placement\_constraints](#input\_task\_definition\_placement\_constraints) | Configuration block for rules that are taken into consideration during task placement (up to max of 10). This is set at the task definition, see `placement_constraints` for setting at the service | `any` | `{}` | no | +| [task\_exec\_iam\_role\_arn](#input\_task\_exec\_iam\_role\_arn) | Existing IAM role ARN | `string` | `null` | no | +| [task\_exec\_iam\_role\_description](#input\_task\_exec\_iam\_role\_description) | Description of the role | `string` | `null` | no | +| [task\_exec\_iam\_role\_max\_session\_duration](#input\_task\_exec\_iam\_role\_max\_session\_duration) | Maximum session duration (in seconds) for ECS task execution role. Default is 3600. | `number` | `null` | no | +| [task\_exec\_iam\_role\_name](#input\_task\_exec\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | +| [task\_exec\_iam\_role\_path](#input\_task\_exec\_iam\_role\_path) | IAM role path | `string` | `null` | no | +| [task\_exec\_iam\_role\_permissions\_boundary](#input\_task\_exec\_iam\_role\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [task\_exec\_iam\_role\_policies](#input\_task\_exec\_iam\_role\_policies) | Map of IAM role policy ARNs to attach to the IAM role | `map(string)` | `{}` | no | +| [task\_exec\_iam\_role\_tags](#input\_task\_exec\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no | +| [task\_exec\_iam\_role\_use\_name\_prefix](#input\_task\_exec\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`task_exec_iam_role_name`) is used as a prefix | `bool` | `true` | no | +| [task\_exec\_iam\_statements](#input\_task\_exec\_iam\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage | `any` | `{}` | no | +| [task\_exec\_secret\_arns](#input\_task\_exec\_secret\_arns) | List of SecretsManager secret ARNs the task execution role will be permitted to get/read | `list(string)` |
[
"arn:aws:secretsmanager:*:*:secret:*"
]
| no | +| [task\_exec\_ssm\_param\_arns](#input\_task\_exec\_ssm\_param\_arns) | List of SSM parameter ARNs the task execution role will be permitted to get/read | `list(string)` |
[
"arn:aws:ssm:*:*:parameter/*"
]
| no | +| [task\_tags](#input\_task\_tags) | A map of additional tags to add to the task definition/set created | `map(string)` | `{}` | no | +| [tasks\_iam\_role\_arn](#input\_tasks\_iam\_role\_arn) | Existing IAM role ARN | `string` | `null` | no | +| [tasks\_iam\_role\_description](#input\_tasks\_iam\_role\_description) | Description of the role | `string` | `null` | no | +| [tasks\_iam\_role\_name](#input\_tasks\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | +| [tasks\_iam\_role\_path](#input\_tasks\_iam\_role\_path) | IAM role path | `string` | `null` | no | +| [tasks\_iam\_role\_permissions\_boundary](#input\_tasks\_iam\_role\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [tasks\_iam\_role\_policies](#input\_tasks\_iam\_role\_policies) | Map of IAM role policy ARNs to attach to the IAM role | `map(string)` | `{}` | no | +| [tasks\_iam\_role\_statements](#input\_tasks\_iam\_role\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage | `any` | `{}` | no | +| [tasks\_iam\_role\_tags](#input\_tasks\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no | +| [tasks\_iam\_role\_use\_name\_prefix](#input\_tasks\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`tasks_iam_role_name`) is used as a prefix | `bool` | `true` | no | +| [timeouts](#input\_timeouts) | Create, update, and delete timeout configurations for the service | `map(string)` | `{}` | no | +| [triggers](#input\_triggers) | Map of arbitrary keys and values that, when changed, will trigger an in-place update (redeployment). Useful with `timestamp()` | `any` | `{}` | no | +| [volume](#input\_volume) | Configuration block for volumes that containers in your task may use | `any` | `{}` | no | +| [wait\_for\_steady\_state](#input\_wait\_for\_steady\_state) | If true, Terraform will wait for the service to reach a steady state before continuing. Default is `false` | `bool` | `null` | no | +| [wait\_until\_stable](#input\_wait\_until\_stable) | Whether terraform should wait until the task set has reached `STEADY_STATE` | `bool` | `null` | no | +| [wait\_until\_stable\_timeout](#input\_wait\_until\_stable\_timeout) | Wait timeout for task set to reach `STEADY_STATE`. Valid time units include `ns`, `us` (or µs), `ms`, `s`, `m`, and `h`. Default `10m` | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [autoscaling\_policies](#output\_autoscaling\_policies) | Map of autoscaling policies and their attributes | +| [autoscaling\_scheduled\_actions](#output\_autoscaling\_scheduled\_actions) | Map of autoscaling scheduled actions and their attributes | +| [container\_definitions](#output\_container\_definitions) | Container definitions | +| [iam\_role\_arn](#output\_iam\_role\_arn) | Service IAM role ARN | +| [iam\_role\_name](#output\_iam\_role\_name) | Service IAM role name | +| [iam\_role\_unique\_id](#output\_iam\_role\_unique\_id) | Stable and unique string identifying the service IAM role | +| [id](#output\_id) | ARN that identifies the service | +| [name](#output\_name) | Name of the service | +| [security\_group\_arn](#output\_security\_group\_arn) | Amazon Resource Name (ARN) of the security group | +| [security\_group\_id](#output\_security\_group\_id) | ID of the security group | +| [task\_definition\_arn](#output\_task\_definition\_arn) | Full ARN of the Task Definition (including both `family` and `revision`) | +| [task\_definition\_family](#output\_task\_definition\_family) | The unique name of the task definition | +| [task\_definition\_family\_revision](#output\_task\_definition\_family\_revision) | The family and revision (family:revision) of the task definition | +| [task\_definition\_revision](#output\_task\_definition\_revision) | Revision of the task in a particular family | +| [task\_exec\_iam\_role\_arn](#output\_task\_exec\_iam\_role\_arn) | Task execution IAM role ARN | +| [task\_exec\_iam\_role\_name](#output\_task\_exec\_iam\_role\_name) | Task execution IAM role name | +| [task\_exec\_iam\_role\_unique\_id](#output\_task\_exec\_iam\_role\_unique\_id) | Stable and unique string identifying the task execution IAM role | +| [task\_set\_arn](#output\_task\_set\_arn) | The Amazon Resource Name (ARN) that identifies the task set | +| [task\_set\_id](#output\_task\_set\_id) | The ID of the task set | +| [task\_set\_stability\_status](#output\_task\_set\_stability\_status) | The stability status. This indicates whether the task set has reached a steady state | +| [task\_set\_status](#output\_task\_set\_status) | The status of the task set | +| [tasks\_iam\_role\_arn](#output\_tasks\_iam\_role\_arn) | Tasks IAM role ARN | +| [tasks\_iam\_role\_name](#output\_tasks\_iam\_role\_name) | Tasks IAM role name | +| [tasks\_iam\_role\_unique\_id](#output\_tasks\_iam\_role\_unique\_id) | Stable and unique string identifying the tasks IAM role | + + +## License + +Apache-2.0 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-ecs/blob/master/LICENSE). diff --git a/2-simple-example/iac/modules/service/main.tf b/2-simple-example/iac/modules/service/main.tf new file mode 100644 index 0000000..b244f7b --- /dev/null +++ b/2-simple-example/iac/modules/service/main.tf @@ -0,0 +1,1354 @@ +data "aws_region" "current" {} +data "aws_partition" "current" {} +data "aws_caller_identity" "current" {} + +locals { + account_id = data.aws_caller_identity.current.account_id + partition = data.aws_partition.current.partition + region = data.aws_region.current.name +} + +################################################################################ +# Service +################################################################################ + +locals { + # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-external.html + is_external_deployment = try(var.deployment_controller.type, null) == "EXTERNAL" + is_daemon = var.scheduling_strategy == "DAEMON" + is_fargate = var.launch_type == "FARGATE" + + # Flattened `network_configuration` + network_configuration = { + assign_public_ip = var.assign_public_ip + security_groups = flatten(concat([try(aws_security_group.this[0].id, [])], var.security_group_ids)) + subnets = var.subnet_ids + } + + create_service = var.create && var.create_service +} + +resource "aws_ecs_service" "this" { + count = local.create_service && !var.ignore_task_definition_changes ? 1 : 0 + + dynamic "alarms" { + for_each = length(var.alarms) > 0 ? [var.alarms] : [] + + content { + alarm_names = alarms.value.alarm_names + enable = try(alarms.value.enable, true) + rollback = try(alarms.value.rollback, true) + } + } + + dynamic "capacity_provider_strategy" { + # Set by task set if deployment controller is external + for_each = { for k, v in var.capacity_provider_strategy : k => v if !local.is_external_deployment } + + content { + base = try(capacity_provider_strategy.value.base, null) + capacity_provider = capacity_provider_strategy.value.capacity_provider + weight = try(capacity_provider_strategy.value.weight, null) + } + } + + cluster = var.cluster_arn + + dynamic "deployment_circuit_breaker" { + for_each = length(var.deployment_circuit_breaker) > 0 ? [var.deployment_circuit_breaker] : [] + + content { + enable = deployment_circuit_breaker.value.enable + rollback = deployment_circuit_breaker.value.rollback + } + } + + dynamic "deployment_controller" { + for_each = length(var.deployment_controller) > 0 ? [var.deployment_controller] : [] + + content { + type = try(deployment_controller.value.type, null) + } + } + + deployment_maximum_percent = local.is_daemon || local.is_external_deployment ? null : var.deployment_maximum_percent + deployment_minimum_healthy_percent = local.is_daemon || local.is_external_deployment ? null : var.deployment_minimum_healthy_percent + desired_count = local.is_daemon || local.is_external_deployment ? null : var.desired_count + enable_ecs_managed_tags = var.enable_ecs_managed_tags + enable_execute_command = var.enable_execute_command + force_new_deployment = local.is_external_deployment ? null : var.force_new_deployment + health_check_grace_period_seconds = var.health_check_grace_period_seconds + iam_role = local.iam_role_arn + launch_type = local.is_external_deployment || length(var.capacity_provider_strategy) > 0 ? null : var.launch_type + + dynamic "load_balancer" { + # Set by task set if deployment controller is external + for_each = { for k, v in var.load_balancer : k => v if !local.is_external_deployment } + + content { + container_name = load_balancer.value.container_name + container_port = load_balancer.value.container_port + elb_name = try(load_balancer.value.elb_name, null) + target_group_arn = try(load_balancer.value.target_group_arn, null) + } + } + + name = var.name + + dynamic "network_configuration" { + # Set by task set if deployment controller is external + for_each = var.network_mode == "awsvpc" && !local.is_external_deployment ? [local.network_configuration] : [] + + content { + assign_public_ip = network_configuration.value.assign_public_ip + security_groups = network_configuration.value.security_groups + subnets = network_configuration.value.subnets + } + } + + dynamic "ordered_placement_strategy" { + for_each = var.ordered_placement_strategy + + content { + field = try(ordered_placement_strategy.value.field, null) + type = ordered_placement_strategy.value.type + } + } + + dynamic "placement_constraints" { + for_each = var.placement_constraints + + content { + expression = try(placement_constraints.value.expression, null) + type = placement_constraints.value.type + } + } + + # Set by task set if deployment controller is external + platform_version = local.is_fargate && !local.is_external_deployment ? var.platform_version : null + scheduling_strategy = local.is_fargate ? "REPLICA" : var.scheduling_strategy + + dynamic "service_connect_configuration" { + for_each = length(var.service_connect_configuration) > 0 ? [var.service_connect_configuration] : [] + + content { + enabled = try(service_connect_configuration.value.enabled, true) + + dynamic "log_configuration" { + for_each = try([service_connect_configuration.value.log_configuration], []) + + content { + log_driver = try(log_configuration.value.log_driver, null) + options = try(log_configuration.value.options, null) + + dynamic "secret_option" { + for_each = try(log_configuration.value.secret_option, []) + + content { + name = secret_option.value.name + value_from = secret_option.value.value_from + } + } + } + } + + namespace = lookup(service_connect_configuration.value, "namespace", null) + + dynamic "service" { + for_each = try([service_connect_configuration.value.service], []) + + content { + + dynamic "client_alias" { + for_each = try([service.value.client_alias], []) + + content { + dns_name = try(client_alias.value.dns_name, null) + port = client_alias.value.port + } + } + + discovery_name = try(service.value.discovery_name, null) + ingress_port_override = try(service.value.ingress_port_override, null) + port_name = service.value.port_name + } + } + } + } + + dynamic "service_registries" { + # Set by task set if deployment controller is external + for_each = length(var.service_registries) > 0 ? [{ for k, v in var.service_registries : k => v if !local.is_external_deployment }] : [] + + content { + container_name = try(service_registries.value.container_name, null) + container_port = try(service_registries.value.container_port, null) + port = try(service_registries.value.port, null) + registry_arn = service_registries.value.registry_arn + } + } + + task_definition = local.task_definition + triggers = var.triggers + wait_for_steady_state = var.wait_for_steady_state + + propagate_tags = var.propagate_tags + tags = merge(var.tags, var.service_tags) + + timeouts { + create = try(var.timeouts.create, null) + update = try(var.timeouts.update, null) + delete = try(var.timeouts.delete, null) + } + + depends_on = [aws_iam_role_policy_attachment.service] + + lifecycle { + ignore_changes = [ + desired_count, # Always ignored + ] + } +} + +################################################################################ +# Service - Ignore `task_definition` +################################################################################ + +resource "aws_ecs_service" "ignore_task_definition" { + count = local.create_service && var.ignore_task_definition_changes ? 1 : 0 + + dynamic "alarms" { + for_each = length(var.alarms) > 0 ? [var.alarms] : [] + + content { + alarm_names = alarms.value.alarm_names + enable = try(alarms.value.enable, true) + rollback = try(alarms.value.rollback, true) + } + } + + dynamic "capacity_provider_strategy" { + # Set by task set if deployment controller is external + for_each = { for k, v in var.capacity_provider_strategy : k => v if !local.is_external_deployment } + + content { + base = try(capacity_provider_strategy.value.base, null) + capacity_provider = capacity_provider_strategy.value.capacity_provider + weight = try(capacity_provider_strategy.value.weight, null) + } + } + + cluster = var.cluster_arn + + dynamic "deployment_circuit_breaker" { + for_each = length(var.deployment_circuit_breaker) > 0 ? [var.deployment_circuit_breaker] : [] + + content { + enable = deployment_circuit_breaker.value.enable + rollback = deployment_circuit_breaker.value.rollback + } + } + + dynamic "deployment_controller" { + for_each = length(var.deployment_controller) > 0 ? [var.deployment_controller] : [] + + content { + type = try(deployment_controller.value.type, null) + } + } + + deployment_maximum_percent = local.is_daemon || local.is_external_deployment ? null : var.deployment_maximum_percent + deployment_minimum_healthy_percent = local.is_daemon || local.is_external_deployment ? null : var.deployment_minimum_healthy_percent + desired_count = local.is_daemon || local.is_external_deployment ? null : var.desired_count + enable_ecs_managed_tags = var.enable_ecs_managed_tags + enable_execute_command = var.enable_execute_command + force_new_deployment = local.is_external_deployment ? null : var.force_new_deployment + health_check_grace_period_seconds = var.health_check_grace_period_seconds + iam_role = local.iam_role_arn + launch_type = local.is_external_deployment || length(var.capacity_provider_strategy) > 0 ? null : var.launch_type + + dynamic "load_balancer" { + # Set by task set if deployment controller is external + for_each = { for k, v in var.load_balancer : k => v if !local.is_external_deployment } + + content { + container_name = load_balancer.value.container_name + container_port = load_balancer.value.container_port + elb_name = try(load_balancer.value.elb_name, null) + target_group_arn = try(load_balancer.value.target_group_arn, null) + } + } + + name = var.name + + dynamic "network_configuration" { + # Set by task set if deployment controller is external + for_each = var.network_mode == "awsvpc" ? [{ for k, v in local.network_configuration : k => v if !local.is_external_deployment }] : [] + + content { + assign_public_ip = network_configuration.value.assign_public_ip + security_groups = network_configuration.value.security_groups + subnets = network_configuration.value.subnets + } + } + + dynamic "ordered_placement_strategy" { + for_each = var.ordered_placement_strategy + + content { + field = try(ordered_placement_strategy.value.field, null) + type = ordered_placement_strategy.value.type + } + } + + dynamic "placement_constraints" { + for_each = var.placement_constraints + + content { + expression = try(placement_constraints.value.expression, null) + type = placement_constraints.value.type + } + } + + # Set by task set if deployment controller is external + platform_version = local.is_fargate && !local.is_external_deployment ? var.platform_version : null + scheduling_strategy = local.is_fargate ? "REPLICA" : var.scheduling_strategy + + dynamic "service_connect_configuration" { + for_each = length(var.service_connect_configuration) > 0 ? [var.service_connect_configuration] : [] + + content { + enabled = try(service_connect_configuration.value.enabled, true) + + dynamic "log_configuration" { + for_each = try([service_connect_configuration.value.log_configuration], []) + + content { + log_driver = try(log_configuration.value.log_driver, null) + options = try(log_configuration.value.options, null) + + dynamic "secret_option" { + for_each = try(log_configuration.value.secret_option, []) + + content { + name = secret_option.value.name + value_from = secret_option.value.value_from + } + } + } + } + + namespace = lookup(service_connect_configuration.value, "namespace", null) + + dynamic "service" { + for_each = try([service_connect_configuration.value.service], []) + + content { + + dynamic "client_alias" { + for_each = try([service.value.client_alias], []) + + content { + dns_name = try(client_alias.value.dns_name, null) + port = client_alias.value.port + } + } + + discovery_name = try(service.value.discovery_name, null) + ingress_port_override = try(service.value.ingress_port_override, null) + port_name = service.value.port_name + } + } + } + } + + dynamic "service_registries" { + # Set by task set if deployment controller is external + for_each = length(var.service_registries) > 0 ? [{ for k, v in var.service_registries : k => v if !local.is_external_deployment }] : [] + + content { + container_name = try(service_registries.value.container_name, null) + container_port = try(service_registries.value.container_port, null) + port = try(service_registries.value.port, null) + registry_arn = service_registries.value.registry_arn + } + } + + task_definition = local.task_definition + triggers = var.triggers + wait_for_steady_state = var.wait_for_steady_state + + propagate_tags = var.propagate_tags + tags = var.tags + + timeouts { + create = try(var.timeouts.create, null) + update = try(var.timeouts.update, null) + delete = try(var.timeouts.delete, null) + } + + depends_on = [aws_iam_role_policy_attachment.service] + + lifecycle { + ignore_changes = [ + desired_count, # Always ignored + task_definition, + load_balancer, + ] + } +} + +################################################################################ +# Service - IAM Role +################################################################################ + +locals { + # Role is not required if task definition uses `awsvpc` network mode or if a load balancer is not used + needs_iam_role = var.network_mode != "awsvpc" && length(var.load_balancer) > 0 + create_iam_role = var.create && var.create_iam_role && local.needs_iam_role + iam_role_arn = local.needs_iam_role ? try(aws_iam_role.service[0].arn, var.iam_role_arn) : null + + iam_role_name = try(coalesce(var.iam_role_name, var.name), "") +} + +data "aws_iam_policy_document" "service_assume" { + count = local.create_iam_role ? 1 : 0 + + statement { + sid = "ECSServiceAssumeRole" + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ecs.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "service" { + count = local.create_iam_role ? 1 : 0 + + name = var.iam_role_use_name_prefix ? null : local.iam_role_name + name_prefix = var.iam_role_use_name_prefix ? "${local.iam_role_name}-" : null + path = var.iam_role_path + description = var.iam_role_description + + assume_role_policy = data.aws_iam_policy_document.service_assume[0].json + permissions_boundary = var.iam_role_permissions_boundary + force_detach_policies = true + + tags = merge(var.tags, var.iam_role_tags) +} + +data "aws_iam_policy_document" "service" { + count = local.create_iam_role ? 1 : 0 + + statement { + sid = "ECSService" + resources = ["*"] + + actions = [ + "ec2:Describe*", + "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", + "elasticloadbalancing:DeregisterTargets", + "elasticloadbalancing:Describe*", + "elasticloadbalancing:RegisterInstancesWithLoadBalancer", + "elasticloadbalancing:RegisterTargets" + ] + } + + dynamic "statement" { + for_each = var.iam_role_statements + + content { + sid = try(statement.value.sid, null) + actions = try(statement.value.actions, null) + not_actions = try(statement.value.not_actions, null) + effect = try(statement.value.effect, null) + resources = try(statement.value.resources, null) + not_resources = try(statement.value.not_resources, null) + + dynamic "principals" { + for_each = try(statement.value.principals, []) + + content { + type = principals.value.type + identifiers = principals.value.identifiers + } + } + + dynamic "not_principals" { + for_each = try(statement.value.not_principals, []) + + content { + type = not_principals.value.type + identifiers = not_principals.value.identifiers + } + } + + dynamic "condition" { + for_each = try(statement.value.conditions, []) + + content { + test = condition.value.test + values = condition.value.values + variable = condition.value.variable + } + } + } + } +} + +resource "aws_iam_policy" "service" { + count = local.create_iam_role ? 1 : 0 + + name = var.iam_role_use_name_prefix ? null : local.iam_role_name + name_prefix = var.iam_role_use_name_prefix ? "${local.iam_role_name}-" : null + description = coalesce(var.iam_role_description, "ECS service policy that allows Amazon ECS to make calls to your load balancer on your behalf") + policy = data.aws_iam_policy_document.service[0].json + + tags = merge(var.tags, var.iam_role_tags) +} + +resource "aws_iam_role_policy_attachment" "service" { + count = local.create_iam_role ? 1 : 0 + + role = aws_iam_role.service[0].name + policy_arn = aws_iam_policy.service[0].arn +} + +################################################################################ +# Container Definition +################################################################################ + +module "container_definition" { + source = "../container-definition" + + for_each = { for k, v in var.container_definitions : k => v if local.create_task_definition && try(v.create, true) } + + operating_system_family = try(var.runtime_platform.operating_system_family, "LINUX") + + # Container Definition + command = try(each.value.command, var.container_definition_defaults.command, []) + cpu = try(each.value.cpu, var.container_definition_defaults.cpu, null) + dependencies = try(each.value.dependencies, var.container_definition_defaults.dependencies, []) # depends_on is a reserved word + disable_networking = try(each.value.disable_networking, var.container_definition_defaults.disable_networking, null) + dns_search_domains = try(each.value.dns_search_domains, var.container_definition_defaults.dns_search_domains, []) + dns_servers = try(each.value.dns_servers, var.container_definition_defaults.dns_servers, []) + docker_labels = try(each.value.docker_labels, var.container_definition_defaults.docker_labels, {}) + docker_security_options = try(each.value.docker_security_options, var.container_definition_defaults.docker_security_options, []) + enable_execute_command = try(each.value.enable_execute_command, var.container_definition_defaults.enable_execute_command, var.enable_execute_command) + entrypoint = try(each.value.entrypoint, var.container_definition_defaults.entrypoint, []) + environment = try(each.value.environment, var.container_definition_defaults.environment, []) + environment_files = try(each.value.environment_files, var.container_definition_defaults.environment_files, []) + essential = try(each.value.essential, var.container_definition_defaults.essential, null) + extra_hosts = try(each.value.extra_hosts, var.container_definition_defaults.extra_hosts, []) + firelens_configuration = try(each.value.firelens_configuration, var.container_definition_defaults.firelens_configuration, {}) + health_check = try(each.value.health_check, var.container_definition_defaults.health_check, {}) + hostname = try(each.value.hostname, var.container_definition_defaults.hostname, null) + image = try(each.value.image, var.container_definition_defaults.image, null) + interactive = try(each.value.interactive, var.container_definition_defaults.interactive, false) + links = try(each.value.links, var.container_definition_defaults.links, []) + linux_parameters = try(each.value.linux_parameters, var.container_definition_defaults.linux_parameters, {}) + log_configuration = try(each.value.log_configuration, var.container_definition_defaults.log_configuration, {}) + memory = try(each.value.memory, var.container_definition_defaults.memory, null) + memory_reservation = try(each.value.memory_reservation, var.container_definition_defaults.memory_reservation, null) + mount_points = try(each.value.mount_points, var.container_definition_defaults.mount_points, []) + name = try(each.value.name, each.key) + port_mappings = try(each.value.port_mappings, var.container_definition_defaults.port_mappings, []) + privileged = try(each.value.privileged, var.container_definition_defaults.privileged, false) + pseudo_terminal = try(each.value.pseudo_terminal, var.container_definition_defaults.pseudo_terminal, false) + readonly_root_filesystem = try(each.value.readonly_root_filesystem, var.container_definition_defaults.readonly_root_filesystem, true) + repository_credentials = try(each.value.repository_credentials, var.container_definition_defaults.repository_credentials, {}) + resource_requirements = try(each.value.resource_requirements, var.container_definition_defaults.resource_requirements, []) + secrets = try(each.value.secrets, var.container_definition_defaults.secrets, []) + start_timeout = try(each.value.start_timeout, var.container_definition_defaults.start_timeout, 30) + stop_timeout = try(each.value.stop_timeout, var.container_definition_defaults.stop_timeout, 120) + system_controls = try(each.value.system_controls, var.container_definition_defaults.system_controls, []) + ulimits = try(each.value.ulimits, var.container_definition_defaults.ulimits, []) + user = try(each.value.user, var.container_definition_defaults.user, 0) + volumes_from = try(each.value.volumes_from, var.container_definition_defaults.volumes_from, []) + working_directory = try(each.value.working_directory, var.container_definition_defaults.working_directory, null) + + # CloudWatch Log Group + service = var.name + enable_cloudwatch_logging = try(each.value.enable_cloudwatch_logging, var.container_definition_defaults.enable_cloudwatch_logging, true) + create_cloudwatch_log_group = try(each.value.create_cloudwatch_log_group, var.container_definition_defaults.create_cloudwatch_log_group, true) + cloudwatch_log_group_name = try(each.value.cloudwatch_log_group_name, var.container_definition_defaults.cloudwatch_log_group_name, null) + cloudwatch_log_group_use_name_prefix = try(each.value.cloudwatch_log_group_use_name_prefix, var.container_definition_defaults.cloudwatch_log_group_use_name_prefix, false) + cloudwatch_log_group_retention_in_days = try(each.value.cloudwatch_log_group_retention_in_days, var.container_definition_defaults.cloudwatch_log_group_retention_in_days, 14) + cloudwatch_log_group_kms_key_id = try(each.value.cloudwatch_log_group_kms_key_id, var.container_definition_defaults.cloudwatch_log_group_kms_key_id, null) + + tags = var.tags +} + +################################################################################ +# Task Definition +################################################################################ + +locals { + create_task_definition = var.create && var.create_task_definition + + # This allows us to query both the existing as well as Terraform's state and get + # and get the max version of either source, useful for when external resources + # update the container definition + max_task_def_revision = local.create_task_definition ? max(aws_ecs_task_definition.this[0].revision, data.aws_ecs_task_definition.this[0].revision) : 0 + task_definition = local.create_task_definition ? "${aws_ecs_task_definition.this[0].family}:${local.max_task_def_revision}" : var.task_definition_arn +} + +# This allows us to query both the existing as well as Terraform's state and get +# and get the max version of either source, useful for when external resources +# update the container definition +data "aws_ecs_task_definition" "this" { + count = local.create_task_definition ? 1 : 0 + + task_definition = aws_ecs_task_definition.this[0].family + + depends_on = [ + # Needs to exist first on first deployment + aws_ecs_task_definition.this + ] +} + +resource "aws_ecs_task_definition" "this" { + count = local.create_task_definition ? 1 : 0 + + # Convert map of maps to array of maps before JSON encoding + container_definitions = jsonencode([for k, v in module.container_definition : v.container_definition]) + cpu = var.cpu + + dynamic "ephemeral_storage" { + for_each = length(var.ephemeral_storage) > 0 ? [var.ephemeral_storage] : [] + + content { + size_in_gib = ephemeral_storage.value.size_in_gib + } + } + + execution_role_arn = try(aws_iam_role.task_exec[0].arn, var.task_exec_iam_role_arn) + family = coalesce(var.family, var.name) + + dynamic "inference_accelerator" { + for_each = var.inference_accelerator + + content { + device_name = inference_accelerator.value.device_name + device_type = inference_accelerator.value.device_type + } + } + + ipc_mode = var.ipc_mode + memory = var.memory + network_mode = var.network_mode + pid_mode = var.pid_mode + + dynamic "placement_constraints" { + for_each = var.task_definition_placement_constraints + + content { + expression = try(placement_constraints.value.expression, null) + type = placement_constraints.value.type + } + } + + dynamic "proxy_configuration" { + for_each = length(var.proxy_configuration) > 0 ? [var.proxy_configuration] : [] + + content { + container_name = proxy_configuration.value.container_name + properties = try(proxy_configuration.value.properties, null) + type = try(proxy_configuration.value.type, null) + } + } + + requires_compatibilities = var.requires_compatibilities + + dynamic "runtime_platform" { + for_each = length(var.runtime_platform) > 0 ? [var.runtime_platform] : [] + + content { + cpu_architecture = try(runtime_platform.value.cpu_architecture, null) + operating_system_family = try(runtime_platform.value.operating_system_family, null) + } + } + + skip_destroy = var.skip_destroy + task_role_arn = try(aws_iam_role.tasks[0].arn, var.tasks_iam_role_arn) + + dynamic "volume" { + for_each = var.volume + + content { + dynamic "docker_volume_configuration" { + for_each = try([volume.value.docker_volume_configuration], []) + + content { + autoprovision = try(docker_volume_configuration.value.autoprovision, null) + driver = try(docker_volume_configuration.value.driver, null) + driver_opts = try(docker_volume_configuration.value.driver_opts, null) + labels = try(docker_volume_configuration.value.labels, null) + scope = try(docker_volume_configuration.value.scope, null) + } + } + + dynamic "efs_volume_configuration" { + for_each = try([volume.value.efs_volume_configuration], []) + + content { + dynamic "authorization_config" { + for_each = try([efs_volume_configuration.value.authorization_config], []) + + content { + access_point_id = try(authorization_config.value.access_point_id, null) + iam = try(authorization_config.value.iam, null) + } + } + + file_system_id = efs_volume_configuration.value.file_system_id + root_directory = try(efs_volume_configuration.value.root_directory, null) + transit_encryption = try(efs_volume_configuration.value.transit_encryption, null) + transit_encryption_port = try(efs_volume_configuration.value.transit_encryption_port, null) + } + } + + dynamic "fsx_windows_file_server_volume_configuration" { + for_each = try([volume.value.fsx_windows_file_server_volume_configuration], []) + + content { + dynamic "authorization_config" { + for_each = try([fsx_windows_file_server_volume_configuration.value.authorization_config], []) + + content { + credentials_parameter = authorization_config.value.credentials_parameter + domain = authorization_config.value.domain + } + } + + file_system_id = fsx_windows_file_server_volume_configuration.value.file_system_id + root_directory = fsx_windows_file_server_volume_configuration.value.root_directory + } + } + + host_path = try(volume.value.host_path, null) + name = try(volume.value.name, volume.key) + } + } + + tags = merge(var.tags, var.task_tags) + + lifecycle { + create_before_destroy = true + } +} + +################################################################################ +# Task Execution - IAM Role +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html +################################################################################ + +locals { + task_exec_iam_role_name = try(coalesce(var.task_exec_iam_role_name, var.name), "") + + create_task_exec_iam_role = local.create_task_definition && var.create_task_exec_iam_role + create_task_exec_policy = local.create_task_exec_iam_role && var.create_task_exec_policy +} + +data "aws_iam_policy_document" "task_exec_assume" { + count = local.create_task_exec_iam_role ? 1 : 0 + + statement { + sid = "ECSTaskExecutionAssumeRole" + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "task_exec" { + count = local.create_task_exec_iam_role ? 1 : 0 + + name = var.task_exec_iam_role_use_name_prefix ? null : local.task_exec_iam_role_name + name_prefix = var.task_exec_iam_role_use_name_prefix ? "${local.task_exec_iam_role_name}-" : null + path = var.task_exec_iam_role_path + description = coalesce(var.task_exec_iam_role_description, "Task execution role for ${local.task_exec_iam_role_name}") + + assume_role_policy = data.aws_iam_policy_document.task_exec_assume[0].json + max_session_duration = var.task_exec_iam_role_max_session_duration + permissions_boundary = var.task_exec_iam_role_permissions_boundary + force_detach_policies = true + + tags = merge(var.tags, var.task_exec_iam_role_tags) +} + +resource "aws_iam_role_policy_attachment" "task_exec_additional" { + for_each = { for k, v in var.task_exec_iam_role_policies : k => v if local.create_task_exec_iam_role } + + role = aws_iam_role.task_exec[0].name + policy_arn = each.value +} + +data "aws_iam_policy_document" "task_exec" { + count = local.create_task_exec_policy ? 1 : 0 + + # Pulled from AmazonECSTaskExecutionRolePolicy + statement { + sid = "Logs" + actions = [ + "logs:CreateLogStream", + "logs:PutLogEvents", + ] + resources = ["*"] + } + + # Pulled from AmazonECSTaskExecutionRolePolicy + statement { + sid = "ECR" + actions = [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + ] + resources = ["*"] + } + + dynamic "statement" { + for_each = length(var.task_exec_ssm_param_arns) > 0 ? [1] : [] + + content { + sid = "GetSSMParams" + actions = ["ssm:GetParameters"] + resources = var.task_exec_ssm_param_arns + } + } + + dynamic "statement" { + for_each = length(var.task_exec_secret_arns) > 0 ? [1] : [] + + content { + sid = "GetSecrets" + actions = ["secretsmanager:GetSecretValue"] + resources = var.task_exec_secret_arns + } + } + + dynamic "statement" { + for_each = var.task_exec_iam_statements + + content { + sid = try(statement.value.sid, null) + actions = try(statement.value.actions, null) + not_actions = try(statement.value.not_actions, null) + effect = try(statement.value.effect, null) + resources = try(statement.value.resources, null) + not_resources = try(statement.value.not_resources, null) + + dynamic "principals" { + for_each = try(statement.value.principals, []) + + content { + type = principals.value.type + identifiers = principals.value.identifiers + } + } + + dynamic "not_principals" { + for_each = try(statement.value.not_principals, []) + + content { + type = not_principals.value.type + identifiers = not_principals.value.identifiers + } + } + + dynamic "condition" { + for_each = try(statement.value.conditions, []) + + content { + test = condition.value.test + values = condition.value.values + variable = condition.value.variable + } + } + } + } +} + +resource "aws_iam_policy" "task_exec" { + count = local.create_task_exec_policy ? 1 : 0 + + name = var.task_exec_iam_role_use_name_prefix ? null : local.task_exec_iam_role_name + name_prefix = var.task_exec_iam_role_use_name_prefix ? "${local.task_exec_iam_role_name}-" : null + description = coalesce(var.task_exec_iam_role_description, "Task execution role IAM policy") + policy = data.aws_iam_policy_document.task_exec[0].json + + tags = merge(var.tags, var.task_exec_iam_role_tags) +} + +resource "aws_iam_role_policy_attachment" "task_exec" { + count = local.create_task_exec_policy ? 1 : 0 + + role = aws_iam_role.task_exec[0].name + policy_arn = aws_iam_policy.task_exec[0].arn +} + +################################################################################ +# Tasks - IAM role +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html +################################################################################ + +locals { + tasks_iam_role_name = try(coalesce(var.tasks_iam_role_name, var.name), "") + create_tasks_iam_role = local.create_task_definition && var.create_tasks_iam_role +} + +data "aws_iam_policy_document" "tasks_assume" { + count = local.create_tasks_iam_role ? 1 : 0 + + statement { + sid = "ECSTasksAssumeRole" + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + + # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html#create_task_iam_policy_and_role + condition { + test = "ArnLike" + variable = "aws:SourceArn" + values = ["arn:${local.partition}:ecs:${local.region}:${local.account_id}:*"] + } + + condition { + test = "StringEquals" + variable = "aws:SourceAccount" + values = [local.account_id] + } + } +} + +resource "aws_iam_role" "tasks" { + count = local.create_tasks_iam_role ? 1 : 0 + + name = var.tasks_iam_role_use_name_prefix ? null : local.tasks_iam_role_name + name_prefix = var.tasks_iam_role_use_name_prefix ? "${local.tasks_iam_role_name}-" : null + path = var.tasks_iam_role_path + description = var.tasks_iam_role_description + + assume_role_policy = data.aws_iam_policy_document.tasks_assume[0].json + permissions_boundary = var.tasks_iam_role_permissions_boundary + force_detach_policies = true + + tags = merge(var.tags, var.tasks_iam_role_tags) +} + +resource "aws_iam_role_policy_attachment" "tasks" { + for_each = { for k, v in var.tasks_iam_role_policies : k => v if local.create_tasks_iam_role } + + role = aws_iam_role.tasks[0].name + policy_arn = each.value +} + +data "aws_iam_policy_document" "tasks" { + count = local.create_tasks_iam_role && (length(var.tasks_iam_role_statements) > 0 || var.enable_execute_command) ? 1 : 0 + + dynamic "statement" { + for_each = var.enable_execute_command ? [1] : [] + + content { + sid = "ECSExec" + actions = [ + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel", + ] + resources = ["*"] + } + } + + dynamic "statement" { + for_each = var.tasks_iam_role_statements + + content { + sid = try(statement.value.sid, null) + actions = try(statement.value.actions, null) + not_actions = try(statement.value.not_actions, null) + effect = try(statement.value.effect, null) + resources = try(statement.value.resources, null) + not_resources = try(statement.value.not_resources, null) + + dynamic "principals" { + for_each = try(statement.value.principals, []) + + content { + type = principals.value.type + identifiers = principals.value.identifiers + } + } + + dynamic "not_principals" { + for_each = try(statement.value.not_principals, []) + + content { + type = not_principals.value.type + identifiers = not_principals.value.identifiers + } + } + + dynamic "condition" { + for_each = try(statement.value.conditions, []) + + content { + test = condition.value.test + values = condition.value.values + variable = condition.value.variable + } + } + } + } +} + +resource "aws_iam_role_policy" "tasks" { + count = local.create_tasks_iam_role && (length(var.tasks_iam_role_statements) > 0 || var.enable_execute_command) ? 1 : 0 + + name = var.tasks_iam_role_use_name_prefix ? null : local.tasks_iam_role_name + name_prefix = var.tasks_iam_role_use_name_prefix ? "${local.tasks_iam_role_name}-" : null + policy = data.aws_iam_policy_document.tasks[0].json + role = aws_iam_role.tasks[0].id +} + +################################################################################ +# Task Set +################################################################################ + +resource "aws_ecs_task_set" "this" { + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskset.html + count = local.create_task_definition && local.is_external_deployment && !var.ignore_task_definition_changes ? 1 : 0 + + service = try(aws_ecs_service.this[0].id, aws_ecs_service.ignore_task_definition[0].id) + cluster = var.cluster_arn + external_id = var.external_id + task_definition = local.task_definition + + dynamic "network_configuration" { + for_each = var.network_mode == "awsvpc" ? [local.network_configuration] : [] + + content { + assign_public_ip = network_configuration.value.assign_public_ip + security_groups = network_configuration.value.security_groups + subnets = network_configuration.value.subnets + } + } + + dynamic "load_balancer" { + for_each = var.load_balancer + + content { + load_balancer_name = try(load_balancer.value.load_balancer_name, null) + target_group_arn = try(load_balancer.value.target_group_arn, null) + container_name = load_balancer.value.container_name + container_port = try(load_balancer.value.container_port, null) + } + } + + dynamic "service_registries" { + for_each = length(var.service_registries) > 0 ? [var.service_registries] : [] + + content { + container_name = try(service_registries.value.container_name, null) + container_port = try(service_registries.value.container_port, null) + port = try(service_registries.value.port, null) + registry_arn = service_registries.value.registry_arn + } + } + + launch_type = length(var.capacity_provider_strategy) > 0 ? null : var.launch_type + + dynamic "capacity_provider_strategy" { + for_each = var.capacity_provider_strategy + + content { + base = try(capacity_provider_strategy.value.base, null) + capacity_provider = capacity_provider_strategy.value.capacity_provider + weight = try(capacity_provider_strategy.value.weight, null) + } + } + + platform_version = local.is_fargate ? var.platform_version : null + + dynamic "scale" { + for_each = length(var.scale) > 0 ? [var.scale] : [] + + content { + unit = try(scale.value.unit, null) + value = try(scale.value.value, null) + } + } + + force_delete = var.force_delete + wait_until_stable = var.wait_until_stable + wait_until_stable_timeout = var.wait_until_stable_timeout + + tags = merge(var.tags, var.task_tags) + + lifecycle { + ignore_changes = [ + scale, # Always ignored + ] + } +} + +################################################################################ +# Task Set - Ignore `task_definition` +################################################################################ + +resource "aws_ecs_task_set" "ignore_task_definition" { + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskset.html + count = local.create_task_definition && local.is_external_deployment && var.ignore_task_definition_changes ? 1 : 0 + + service = try(aws_ecs_service.this[0].id, aws_ecs_service.ignore_task_definition[0].id) + cluster = var.cluster_arn + external_id = var.external_id + task_definition = local.task_definition + + dynamic "network_configuration" { + for_each = var.network_mode == "awsvpc" ? [local.network_configuration] : [] + + content { + assign_public_ip = network_configuration.value.assign_public_ip + security_groups = network_configuration.value.security_groups + subnets = network_configuration.value.subnets + } + } + + dynamic "load_balancer" { + for_each = var.load_balancer + + content { + load_balancer_name = try(load_balancer.value.load_balancer_name, null) + target_group_arn = try(load_balancer.value.target_group_arn, null) + container_name = load_balancer.value.container_name + container_port = try(load_balancer.value.container_port, null) + } + } + + dynamic "service_registries" { + for_each = length(var.service_registries) > 0 ? [var.service_registries] : [] + + content { + container_name = try(service_registries.value.container_name, null) + container_port = try(service_registries.value.container_port, null) + port = try(service_registries.value.port, null) + registry_arn = service_registries.value.registry_arn + } + } + + launch_type = length(var.capacity_provider_strategy) > 0 ? null : var.launch_type + + dynamic "capacity_provider_strategy" { + for_each = var.capacity_provider_strategy + + content { + base = try(capacity_provider_strategy.value.base, null) + capacity_provider = capacity_provider_strategy.value.capacity_provider + weight = try(capacity_provider_strategy.value.weight, null) + } + } + + platform_version = local.is_fargate ? var.platform_version : null + + dynamic "scale" { + for_each = length(var.scale) > 0 ? [var.scale] : [] + + content { + unit = try(scale.value.unit, null) + value = try(scale.value.value, null) + } + } + + force_delete = var.force_delete + wait_until_stable = var.wait_until_stable + wait_until_stable_timeout = var.wait_until_stable_timeout + + tags = merge(var.tags, var.task_tags) + + lifecycle { + ignore_changes = [ + scale, # Always ignored + task_definition, + ] + } +} + +################################################################################ +# Autoscaling +################################################################################ + +locals { + enable_autoscaling = local.create_service && var.enable_autoscaling && !local.is_daemon + + cluster_name = element(split("/", var.cluster_arn), 1) +} + +resource "aws_appautoscaling_target" "this" { + count = local.enable_autoscaling ? 1 : 0 + + # Desired needs to be between or equal to min/max + min_capacity = min(var.autoscaling_min_capacity, var.desired_count) + max_capacity = max(var.autoscaling_max_capacity, var.desired_count) + + resource_id = "service/${local.cluster_name}/${try(aws_ecs_service.this[0].name, aws_ecs_service.ignore_task_definition[0].name)}" + scalable_dimension = "ecs:service:DesiredCount" + service_namespace = "ecs" + tags = var.tags +} + +resource "aws_appautoscaling_policy" "this" { + for_each = { for k, v in var.autoscaling_policies : k => v if local.enable_autoscaling } + + name = try(each.value.name, each.key) + policy_type = try(each.value.policy_type, "TargetTrackingScaling") + resource_id = aws_appautoscaling_target.this[0].resource_id + scalable_dimension = aws_appautoscaling_target.this[0].scalable_dimension + service_namespace = aws_appautoscaling_target.this[0].service_namespace + + dynamic "step_scaling_policy_configuration" { + for_each = try([each.value.step_scaling_policy_configuration], []) + + content { + adjustment_type = try(step_scaling_policy_configuration.value.adjustment_type, null) + cooldown = try(step_scaling_policy_configuration.value.cooldown, null) + metric_aggregation_type = try(step_scaling_policy_configuration.value.metric_aggregation_type, null) + min_adjustment_magnitude = try(step_scaling_policy_configuration.value.min_adjustment_magnitude, null) + + dynamic "step_adjustment" { + for_each = try(step_scaling_policy_configuration.value.step_adjustment, []) + + content { + metric_interval_lower_bound = try(step_adjustment.value.metric_interval_lower_bound, null) + metric_interval_upper_bound = try(step_adjustment.value.metric_interval_upper_bound, null) + scaling_adjustment = try(step_adjustment.value.scaling_adjustment, null) + } + } + } + } + + dynamic "target_tracking_scaling_policy_configuration" { + for_each = try(each.value.policy_type, null) == "TargetTrackingScaling" ? try([each.value.target_tracking_scaling_policy_configuration], []) : [] + + content { + dynamic "customized_metric_specification" { + for_each = try([target_tracking_scaling_policy_configuration.value.customized_metric_specification], []) + + content { + dynamic "dimensions" { + for_each = try(customized_metric_specification.value.dimensions, []) + + content { + name = dimensions.value.name + value = dimensions.value.value + } + } + + metric_name = customized_metric_specification.value.metric_name + namespace = customized_metric_specification.value.namespace + statistic = customized_metric_specification.value.statistic + unit = try(customized_metric_specification.value.unit, null) + } + } + + disable_scale_in = try(target_tracking_scaling_policy_configuration.value.disable_scale_in, null) + + dynamic "predefined_metric_specification" { + for_each = try([target_tracking_scaling_policy_configuration.value.predefined_metric_specification], []) + + content { + predefined_metric_type = predefined_metric_specification.value.predefined_metric_type + resource_label = try(predefined_metric_specification.value.resource_label, null) + } + } + + scale_in_cooldown = try(target_tracking_scaling_policy_configuration.value.scale_in_cooldown, 300) + scale_out_cooldown = try(target_tracking_scaling_policy_configuration.value.scale_out_cooldown, 60) + target_value = try(target_tracking_scaling_policy_configuration.value.target_value, 75) + } + } +} + +resource "aws_appautoscaling_scheduled_action" "this" { + for_each = { for k, v in var.autoscaling_scheduled_actions : k => v if local.enable_autoscaling } + + name = try(each.value.name, each.key) + service_namespace = aws_appautoscaling_target.this[0].service_namespace + resource_id = aws_appautoscaling_target.this[0].resource_id + scalable_dimension = aws_appautoscaling_target.this[0].scalable_dimension + + scalable_target_action { + min_capacity = each.value.min_capacity + max_capacity = each.value.max_capacity + } + + schedule = each.value.schedule + start_time = try(each.value.start_time, null) + end_time = try(each.value.end_time, null) + timezone = try(each.value.timezone, null) +} + +################################################################################ +# Security Group +################################################################################ + +locals { + create_security_group = var.create && var.create_security_group && var.network_mode == "awsvpc" + security_group_name = try(coalesce(var.security_group_name, var.name), "") +} + +data "aws_subnet" "this" { + count = local.create_security_group ? 1 : 0 + + id = element(var.subnet_ids, 0) +} + +resource "aws_security_group" "this" { + count = local.create_security_group ? 1 : 0 + + name = var.security_group_use_name_prefix ? null : local.security_group_name + name_prefix = var.security_group_use_name_prefix ? "${local.security_group_name}-" : null + description = var.security_group_description + vpc_id = data.aws_subnet.this[0].vpc_id + + tags = merge( + var.tags, + { "Name" = local.security_group_name }, + var.security_group_tags + ) + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_security_group_rule" "this" { + for_each = { for k, v in var.security_group_rules : k => v if local.create_security_group } + + # Required + security_group_id = aws_security_group.this[0].id + protocol = each.value.protocol + from_port = each.value.from_port + to_port = each.value.to_port + type = each.value.type + + # Optional + description = lookup(each.value, "description", null) + cidr_blocks = lookup(each.value, "cidr_blocks", null) + ipv6_cidr_blocks = lookup(each.value, "ipv6_cidr_blocks", null) + prefix_list_ids = lookup(each.value, "prefix_list_ids", null) + self = lookup(each.value, "self", null) + source_security_group_id = lookup(each.value, "source_security_group_id", null) +} diff --git a/2-simple-example/iac/modules/service/outputs.tf b/2-simple-example/iac/modules/service/outputs.tf new file mode 100644 index 0000000..1eaa851 --- /dev/null +++ b/2-simple-example/iac/modules/service/outputs.tf @@ -0,0 +1,157 @@ +################################################################################ +# Service +################################################################################ + +output "id" { + description = "ARN that identifies the service" + value = try(aws_ecs_service.this[0].id, aws_ecs_service.ignore_task_definition[0].id, null) +} + +output "name" { + description = "Name of the service" + value = try(aws_ecs_service.this[0].name, aws_ecs_service.ignore_task_definition[0].name, null) +} + +################################################################################ +# IAM Role +################################################################################ + +output "iam_role_name" { + description = "Service IAM role name" + value = try(aws_iam_role.service[0].name, null) +} + +output "iam_role_arn" { + description = "Service IAM role ARN" + value = try(aws_iam_role.service[0].arn, var.iam_role_arn) +} + +output "iam_role_unique_id" { + description = "Stable and unique string identifying the service IAM role" + value = try(aws_iam_role.service[0].unique_id, null) +} + +################################################################################ +# Container Definition +################################################################################ + +output "container_definitions" { + description = "Container definitions" + value = module.container_definition +} + +################################################################################ +# Task Definition +################################################################################ + +output "task_definition_arn" { + description = "Full ARN of the Task Definition (including both `family` and `revision`)" + value = try(aws_ecs_task_definition.this[0].arn, var.task_definition_arn) +} + +output "task_definition_revision" { + description = "Revision of the task in a particular family" + value = try(aws_ecs_task_definition.this[0].revision, null) +} + +output "task_definition_family" { + description = "The unique name of the task definition" + value = try(aws_ecs_task_definition.this[0].family, null) +} + +output "task_definition_family_revision" { + description = "The family and revision (family:revision) of the task definition" + value = "${try(aws_ecs_task_definition.this[0].family, "")}:${local.max_task_def_revision}" +} + +################################################################################ +# Task Execution - IAM Role +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html +################################################################################ + +output "task_exec_iam_role_name" { + description = "Task execution IAM role name" + value = try(aws_iam_role.task_exec[0].name, null) +} + +output "task_exec_iam_role_arn" { + description = "Task execution IAM role ARN" + value = try(aws_iam_role.task_exec[0].arn, var.task_exec_iam_role_arn) +} + +output "task_exec_iam_role_unique_id" { + description = "Stable and unique string identifying the task execution IAM role" + value = try(aws_iam_role.task_exec[0].unique_id, null) +} + +################################################################################ +# Tasks - IAM role +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html +################################################################################ + +output "tasks_iam_role_name" { + description = "Tasks IAM role name" + value = try(aws_iam_role.tasks[0].name, null) +} + +output "tasks_iam_role_arn" { + description = "Tasks IAM role ARN" + value = try(aws_iam_role.tasks[0].arn, var.tasks_iam_role_arn) +} + +output "tasks_iam_role_unique_id" { + description = "Stable and unique string identifying the tasks IAM role" + value = try(aws_iam_role.tasks[0].unique_id, null) +} + +################################################################################ +# Task Set +################################################################################ + +output "task_set_id" { + description = "The ID of the task set" + value = try(aws_ecs_task_set.this[0].task_set_id, aws_ecs_task_set.ignore_task_definition[0].task_set_id, null) +} + +output "task_set_arn" { + description = "The Amazon Resource Name (ARN) that identifies the task set" + value = try(aws_ecs_task_set.this[0].arn, aws_ecs_task_set.ignore_task_definition[0].arn, null) +} + +output "task_set_stability_status" { + description = "The stability status. This indicates whether the task set has reached a steady state" + value = try(aws_ecs_task_set.this[0].stability_status, aws_ecs_task_set.ignore_task_definition[0].stability_status, null) +} + +output "task_set_status" { + description = "The status of the task set" + value = try(aws_ecs_task_set.this[0].status, aws_ecs_task_set.ignore_task_definition[0].status, null) +} + +################################################################################ +# Autoscaling +################################################################################ + +output "autoscaling_policies" { + description = "Map of autoscaling policies and their attributes" + value = aws_appautoscaling_policy.this +} + +output "autoscaling_scheduled_actions" { + description = "Map of autoscaling scheduled actions and their attributes" + value = aws_appautoscaling_scheduled_action.this +} + +################################################################################ +# Security Group +################################################################################ + +output "security_group_arn" { + description = "Amazon Resource Name (ARN) of the security group" + value = try(aws_security_group.this[0].arn, null) +} + +output "security_group_id" { + description = "ID of the security group" + value = try(aws_security_group.this[0].id, null) +} diff --git a/2-simple-example/iac/modules/service/variables.tf b/2-simple-example/iac/modules/service/variables.tf new file mode 100644 index 0000000..1c542e1 --- /dev/null +++ b/2-simple-example/iac/modules/service/variables.tf @@ -0,0 +1,661 @@ +variable "create" { + description = "Determines whether resources will be created (affects all resources)" + type = bool + default = true +} + +variable "create_service" { + description = "Determines whether service resource will be created (set to `false` in case you want to create task definition only)" + type = bool + default = true +} + +variable "tags" { + description = "A map of tags to add to all resources" + type = map(string) + default = {} +} + +################################################################################ +# Service +################################################################################ + +variable "ignore_task_definition_changes" { + description = "Whether changes to service `task_definition` changes should be ignored" + type = bool + default = false +} + +variable "alarms" { + description = "Information about the CloudWatch alarms" + type = any + default = {} +} + +variable "capacity_provider_strategy" { + description = "Capacity provider strategies to use for the service. Can be one or more" + type = any + default = {} +} + +variable "cluster_arn" { + description = "ARN of the ECS cluster where the resources will be provisioned" + type = string + default = "" +} + +variable "deployment_circuit_breaker" { + description = "Configuration block for deployment circuit breaker" + type = any + default = {} +} + +variable "deployment_controller" { + description = "Configuration block for deployment controller configuration" + type = any + default = {} +} + +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" + type = number + default = 200 +} + +variable "deployment_minimum_healthy_percent" { + description = "Lower limit (as a percentage of the service's `desired_count`) of the number of running tasks that must remain running and healthy in a service during a deployment" + type = number + default = 66 +} + +variable "desired_count" { + description = "Number of instances of the task definition to place and keep running" + type = number + default = 1 +} + +variable "enable_ecs_managed_tags" { + description = "Specifies whether to enable Amazon ECS managed tags for the tasks within the service" + type = bool + default = true +} + +variable "enable_execute_command" { + description = "Specifies whether to enable Amazon ECS Exec for the tasks within the service" + type = bool + default = false +} + +variable "force_new_deployment" { + description = "Enable to force a new task deployment of the service. This can be used to update tasks to use a newer Docker image with same image/tag combination, roll Fargate tasks onto a newer platform version, or immediately deploy `ordered_placement_strategy` and `placement_constraints` updates" + type = bool + default = true +} + +variable "health_check_grace_period_seconds" { + description = "Seconds to ignore failing load balancer health checks on newly instantiated tasks to prevent premature shutdown, up to 2147483647. Only valid for services configured to use load balancers" + type = number + default = null +} + +variable "launch_type" { + description = "Launch type on which to run your service. The valid values are `EC2`, `FARGATE`, and `EXTERNAL`. Defaults to `FARGATE`" + type = string + default = "FARGATE" +} + +variable "load_balancer" { + description = "Configuration block for load balancers" + type = any + default = {} +} + +variable "name" { + description = "Name of the service (up to 255 letters, numbers, hyphens, and underscores)" + type = string + default = null +} + +variable "assign_public_ip" { + description = "Assign a public IP address to the ENI (Fargate launch type only)" + type = bool + default = false +} + +variable "security_group_ids" { + description = "List of security groups to associate with the task or service" + type = list(string) + default = [] +} + +variable "subnet_ids" { + description = "List of subnets to associate with the task or service" + type = list(string) + default = [] +} + +variable "ordered_placement_strategy" { + description = "Service level strategy rules that are taken into consideration during task placement. List from top to bottom in order of precedence" + type = any + default = {} +} + +variable "placement_constraints" { + description = "Configuration block for rules that are taken into consideration during task placement (up to max of 10). This is set at the service, see `task_definition_placement_constraints` for setting at the task definition" + type = any + default = {} +} + +variable "platform_version" { + description = "Platform version on which to run your service. Only applicable for `launch_type` set to `FARGATE`. Defaults to `LATEST`" + type = string + default = null +} + +variable "propagate_tags" { + description = "Specifies whether to propagate the tags from the task definition or the service to the tasks. The valid values are `SERVICE` and `TASK_DEFINITION`" + type = string + default = null +} + +variable "scheduling_strategy" { + description = "Scheduling strategy to use for the service. The valid values are `REPLICA` and `DAEMON`. Defaults to `REPLICA`" + type = string + default = null +} + +variable "service_connect_configuration" { + description = "The ECS Service Connect configuration for this service to discover and connect to services, and be discovered by, and connected from, other services within a namespace" + type = any + default = {} +} + +variable "service_registries" { + description = "Service discovery registries for the service" + type = any + default = {} +} + +variable "timeouts" { + description = "Create, update, and delete timeout configurations for the service" + type = map(string) + default = {} +} + +variable "triggers" { + description = "Map of arbitrary keys and values that, when changed, will trigger an in-place update (redeployment). Useful with `timestamp()`" + type = any + default = {} +} + +variable "wait_for_steady_state" { + description = "If true, Terraform will wait for the service to reach a steady state before continuing. Default is `false`" + type = bool + default = null +} + +variable "service_tags" { + description = "A map of additional tags to add to the service" + type = map(string) + default = {} +} + +################################################################################ +# Service - IAM Role +################################################################################ + +variable "create_iam_role" { + description = "Determines whether the ECS service IAM role should be created" + type = bool + default = true +} + +variable "iam_role_arn" { + description = "Existing IAM role ARN" + type = string + default = null +} + +variable "iam_role_name" { + description = "Name to use on IAM role created" + type = string + default = null +} + +variable "iam_role_use_name_prefix" { + description = "Determines whether the IAM role name (`iam_role_name`) is used as a prefix" + type = bool + default = true +} + +variable "iam_role_path" { + description = "IAM role path" + type = string + default = null +} + +variable "iam_role_description" { + description = "Description of the role" + type = string + default = null +} + +variable "iam_role_permissions_boundary" { + description = "ARN of the policy that is used to set the permissions boundary for the IAM role" + type = string + default = null +} + +variable "iam_role_tags" { + description = "A map of additional tags to add to the IAM role created" + type = map(string) + default = {} +} + +variable "iam_role_statements" { + description = "A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage" + type = any + default = {} +} + +################################################################################ +# Task Definition +################################################################################ + +variable "create_task_definition" { + description = "Determines whether to create a task definition or use existing/provided" + type = bool + default = true +} + +variable "task_definition_arn" { + description = "Existing task definition ARN. Required when `create_task_definition` is `false`" + type = string + default = null +} + +variable "container_definitions" { + description = "A map of valid [container definitions](http://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerDefinition.html). Please note that you should only provide values that are part of the container definition document" + type = any + default = {} +} + +variable "container_definition_defaults" { + description = "A map of default values for [container definitions](http://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerDefinition.html) created by `container_definitions`" + type = any + default = {} +} + +variable "cpu" { + description = "Number of cpu units used by the task. If the `requires_compatibilities` is `FARGATE` this field is required" + type = number + default = 1024 +} + +variable "ephemeral_storage" { + description = "The amount of ephemeral storage to allocate for the task. This parameter is used to expand the total amount of ephemeral storage available, beyond the default amount, for tasks hosted on AWS Fargate" + type = any + default = {} +} + +variable "family" { + description = "A unique name for your task definition" + type = string + default = null +} + +variable "inference_accelerator" { + description = "Configuration block(s) with Inference Accelerators settings" + type = any + default = {} +} + +variable "ipc_mode" { + description = "IPC resource namespace to be used for the containers in the task The valid values are `host`, `task`, and `none`" + type = string + default = null +} + +variable "memory" { + description = "Amount (in MiB) of memory used by the task. If the `requires_compatibilities` is `FARGATE` this field is required" + type = number + default = 2048 +} + +variable "network_mode" { + description = "Docker networking mode to use for the containers in the task. Valid values are `none`, `bridge`, `awsvpc`, and `host`" + type = string + default = "awsvpc" +} + +variable "pid_mode" { + description = "Process namespace to use for the containers in the task. The valid values are `host` and `task`" + type = string + default = null +} + +variable "task_definition_placement_constraints" { + description = "Configuration block for rules that are taken into consideration during task placement (up to max of 10). This is set at the task definition, see `placement_constraints` for setting at the service" + type = any + default = {} +} + +variable "proxy_configuration" { + description = "Configuration block for the App Mesh proxy" + type = any + default = {} +} + +variable "requires_compatibilities" { + description = "Set of launch types required by the task. The valid values are `EC2` and `FARGATE`" + type = list(string) + default = ["FARGATE"] +} + +variable "runtime_platform" { + description = "Configuration block for `runtime_platform` that containers in your task may use" + type = any + default = { + operating_system_family = "LINUX" + cpu_architecture = "X86_64" + } +} + +variable "skip_destroy" { + description = "If true, the task is not deleted when the service is deleted" + type = bool + default = null +} + +variable "volume" { + description = "Configuration block for volumes that containers in your task may use" + type = any + default = {} +} + +variable "task_tags" { + description = "A map of additional tags to add to the task definition/set created" + type = map(string) + default = {} +} + +################################################################################ +# Task Execution - IAM Role +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html +################################################################################ + +variable "create_task_exec_iam_role" { + description = "Determines whether the ECS task definition IAM role should be created" + type = bool + default = true +} + +variable "task_exec_iam_role_arn" { + description = "Existing IAM role ARN" + type = string + default = null +} + +variable "task_exec_iam_role_name" { + description = "Name to use on IAM role created" + type = string + default = null +} + +variable "task_exec_iam_role_use_name_prefix" { + description = "Determines whether the IAM role name (`task_exec_iam_role_name`) is used as a prefix" + type = bool + default = true +} + +variable "task_exec_iam_role_path" { + description = "IAM role path" + type = string + default = null +} + +variable "task_exec_iam_role_description" { + description = "Description of the role" + type = string + default = null +} + +variable "task_exec_iam_role_permissions_boundary" { + description = "ARN of the policy that is used to set the permissions boundary for the IAM role" + type = string + default = null +} + +variable "task_exec_iam_role_tags" { + description = "A map of additional tags to add to the IAM role created" + type = map(string) + default = {} +} + +variable "task_exec_iam_role_policies" { + description = "Map of IAM role policy ARNs to attach to the IAM role" + type = map(string) + default = {} +} + +variable "task_exec_iam_role_max_session_duration" { + description = "Maximum session duration (in seconds) for ECS task execution role. Default is 3600." + type = number + default = null +} + +variable "create_task_exec_policy" { + description = "Determines whether the ECS task definition IAM policy should be created. This includes permissions included in AmazonECSTaskExecutionRolePolicy as well as access to secrets and SSM parameters" + type = bool + default = true +} + +variable "task_exec_ssm_param_arns" { + description = "List of SSM parameter ARNs the task execution role will be permitted to get/read" + type = list(string) + default = ["arn:aws:ssm:*:*:parameter/*"] +} + +variable "task_exec_secret_arns" { + description = "List of SecretsManager secret ARNs the task execution role will be permitted to get/read" + type = list(string) + default = ["arn:aws:secretsmanager:*:*:secret:*"] +} + +variable "task_exec_iam_statements" { + description = "A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage" + type = any + default = {} +} + +################################################################################ +# Tasks - IAM role +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html +################################################################################ + +variable "create_tasks_iam_role" { + description = "Determines whether the ECS tasks IAM role should be created" + type = bool + default = true +} + +variable "tasks_iam_role_arn" { + description = "Existing IAM role ARN" + type = string + default = null +} + +variable "tasks_iam_role_name" { + description = "Name to use on IAM role created" + type = string + default = null +} + +variable "tasks_iam_role_use_name_prefix" { + description = "Determines whether the IAM role name (`tasks_iam_role_name`) is used as a prefix" + type = bool + default = true +} + +variable "tasks_iam_role_path" { + description = "IAM role path" + type = string + default = null +} + +variable "tasks_iam_role_description" { + description = "Description of the role" + type = string + default = null +} + +variable "tasks_iam_role_permissions_boundary" { + description = "ARN of the policy that is used to set the permissions boundary for the IAM role" + type = string + default = null +} + +variable "tasks_iam_role_tags" { + description = "A map of additional tags to add to the IAM role created" + type = map(string) + default = {} +} + +variable "tasks_iam_role_policies" { + description = "Map of IAM role policy ARNs to attach to the IAM role" + type = map(string) + default = {} +} + +variable "tasks_iam_role_statements" { + description = "A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage" + type = any + default = {} +} + +################################################################################ +# Task Set +################################################################################ + +variable "external_id" { + description = "The external ID associated with the task set" + type = string + default = null +} + +variable "scale" { + description = "A floating-point percentage of the desired number of tasks to place and keep running in the task set" + type = any + default = {} +} + +variable "force_delete" { + description = "Whether to allow deleting the task set without waiting for scaling down to 0" + type = bool + default = null +} + +variable "wait_until_stable" { + description = "Whether terraform should wait until the task set has reached `STEADY_STATE`" + type = bool + default = null +} + +variable "wait_until_stable_timeout" { + description = "Wait timeout for task set to reach `STEADY_STATE`. Valid time units include `ns`, `us` (or µs), `ms`, `s`, `m`, and `h`. Default `10m`" + type = string + default = null +} + +################################################################################ +# Autoscaling +################################################################################ + +variable "enable_autoscaling" { + description = "Determines whether to enable autoscaling for the service" + type = bool + default = true +} + +variable "autoscaling_min_capacity" { + description = "Minimum number of tasks to run in your service" + type = number + default = 1 +} + +variable "autoscaling_max_capacity" { + description = "Maximum number of tasks to run in your service" + type = number + default = 10 +} + +variable "autoscaling_policies" { + description = "Map of autoscaling policies to create for the service" + type = any + default = { + cpu = { + policy_type = "TargetTrackingScaling" + + target_tracking_scaling_policy_configuration = { + predefined_metric_specification = { + predefined_metric_type = "ECSServiceAverageCPUUtilization" + } + } + } + memory = { + policy_type = "TargetTrackingScaling" + + target_tracking_scaling_policy_configuration = { + predefined_metric_specification = { + predefined_metric_type = "ECSServiceAverageMemoryUtilization" + } + } + } + } +} + +variable "autoscaling_scheduled_actions" { + description = "Map of autoscaling scheduled actions to create for the service" + type = any + default = {} +} + +################################################################################ +# Security Group +################################################################################ + +variable "create_security_group" { + description = "Determines if a security group is created" + type = bool + default = true +} + +variable "security_group_name" { + description = "Name to use on security group created" + type = string + default = null +} + +variable "security_group_use_name_prefix" { + description = "Determines whether the security group name (`security_group_name`) is used as a prefix" + type = bool + default = true +} + +variable "security_group_description" { + description = "Description of the security group created" + type = string + default = null +} + +variable "security_group_rules" { + description = "Security group rules to add to the security group created" + type = any + default = {} +} + +variable "security_group_tags" { + description = "A map of additional tags to add to the security group created" + type = map(string) + default = {} +} diff --git a/2-simple-example/iac/modules/service/versions.tf b/2-simple-example/iac/modules/service/versions.tf new file mode 100644 index 0000000..682191e --- /dev/null +++ b/2-simple-example/iac/modules/service/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.66.1" + } + } +} diff --git a/2-simple-example/iac/outputs.tf b/2-simple-example/iac/outputs.tf new file mode 100644 index 0000000..6c77f7b --- /dev/null +++ b/2-simple-example/iac/outputs.tf @@ -0,0 +1,166 @@ +################################################################################ +# Cluster +################################################################################ + +output "cluster_arn" { + description = "ARN that identifies the cluster" + value = module.ecs_cluster.arn +} + +output "cluster_id" { + description = "ID that identifies the cluster" + value = module.ecs_cluster.id +} + +output "cluster_name" { + description = "Name that identifies the cluster" + value = module.ecs_cluster.name +} + +output "cluster_capacity_providers" { + description = "Map of cluster capacity providers attributes" + value = module.ecs_cluster.cluster_capacity_providers +} + +output "cluster_autoscaling_capacity_providers" { + description = "Map of capacity providers created and their attributes" + value = module.ecs_cluster.autoscaling_capacity_providers +} + +################################################################################ +# Service +################################################################################ + +output "service_id" { + description = "ARN that identifies the service" + value = module.ecs_service.id +} + +output "service_name" { + description = "Name of the service" + value = module.ecs_service.name +} + +output "service_iam_role_name" { + description = "Service IAM role name" + value = module.ecs_service.iam_role_name +} + +output "service_iam_role_arn" { + description = "Service IAM role ARN" + value = module.ecs_service.iam_role_arn +} + +output "service_iam_role_unique_id" { + description = "Stable and unique string identifying the service IAM role" + value = module.ecs_service.iam_role_unique_id +} + +output "service_container_definitions" { + description = "Container definitions" + value = module.ecs_service.container_definitions +} + +output "service_task_definition_arn" { + description = "Full ARN of the Task Definition (including both `family` and `revision`)" + value = module.ecs_service.task_definition_arn +} + +output "service_task_definition_revision" { + description = "Revision of the task in a particular family" + value = module.ecs_service.task_definition_revision +} + +output "service_task_definition_family" { + description = "The unique name of the task definition" + value = module.ecs_service.task_definition_family +} + +output "service_task_definition_family_revision" { + description = "The family and revision (family:revision) of the task definition" + value = module.ecs_service.task_definition_family_revision +} + +output "service_task_exec_iam_role_name" { + description = "Task execution IAM role name" + value = module.ecs_service.task_exec_iam_role_name +} + +output "service_task_exec_iam_role_arn" { + description = "Task execution IAM role ARN" + value = module.ecs_service.task_exec_iam_role_arn +} + +output "service_task_exec_iam_role_unique_id" { + description = "Stable and unique string identifying the task execution IAM role" + value = module.ecs_service.task_exec_iam_role_unique_id +} + +output "service_tasks_iam_role_name" { + description = "Tasks IAM role name" + value = module.ecs_service.tasks_iam_role_name +} + +output "service_tasks_iam_role_arn" { + description = "Tasks IAM role ARN" + value = module.ecs_service.tasks_iam_role_arn +} + +output "service_tasks_iam_role_unique_id" { + description = "Stable and unique string identifying the tasks IAM role" + value = module.ecs_service.tasks_iam_role_unique_id +} + +output "service_task_set_id" { + description = "The ID of the task set" + value = module.ecs_service.task_set_id +} + +output "service_task_set_arn" { + description = "The Amazon Resource Name (ARN) that identifies the task set" + value = module.ecs_service.task_set_arn +} + +output "service_task_set_stability_status" { + description = "The stability status. This indicates whether the task set has reached a steady state" + value = module.ecs_service.task_set_stability_status +} + +output "service_task_set_status" { + description = "The status of the task set" + value = module.ecs_service.task_set_status +} + +output "service_autoscaling_policies" { + description = "Map of autoscaling policies and their attributes" + value = module.ecs_service.autoscaling_policies +} + +output "service_autoscaling_scheduled_actions" { + description = "Map of autoscaling scheduled actions and their attributes" + value = module.ecs_service.autoscaling_scheduled_actions +} + +output "service_security_group_arn" { + description = "Amazon Resource Name (ARN) of the security group" + value = module.ecs_service.security_group_arn +} + +output "service_security_group_id" { + description = "ID of the security group" + value = module.ecs_service.security_group_id +} + +################################################################################ +# Standalone Task Definition (w/o Service) +################################################################################ + +output "task_definition_run_task_command" { + description = "awscli command to run the standalone task" + value = </fargate-chat-demo:latest . +docker push +``` + +## Deploy on AWS + +See the main README and change the image path to your own repo. diff --git a/chat-app/index.js b/chat-app/index.js new file mode 100644 index 0000000..99d5744 --- /dev/null +++ b/chat-app/index.js @@ -0,0 +1,103 @@ +// Setup basic express server +var express = require('express'); +var app = express(); +var path = require('path'); +var server = require('http').createServer(app); +var io = require('socket.io')(server); +var redis = require('socket.io-redis'); + +io.adapter(redis({ host: process.env.REDIS_ENDPOINT , port: 6379 })); + +var Presence = require('./lib/presence'); + +// Lower the heartbeat timeout +io.set('heartbeat timeout', 8000); +io.set('heartbeat interval', 4000); + +var port = process.env.PORT || 3000; + +server.listen(port, function() { + console.log('Server listening at port %d', port); +}); + +// Routing +app.use(express.static(path.join(__dirname, 'public'))); + +io.on('connection', function(socket) { + var addedUser = false; + + // when the client emits 'new message', this listens and executes + socket.on('new message', function(data) { + // we tell the client to execute 'new message' + socket.broadcast.emit('new message', { + username: socket.username, + message: data + }); + }); + + socket.conn.on('heartbeat', function() { + if (!addedUser) { + // Don't start upserting until the user has added themselves. + return; + } + + Presence.upsert(socket.id, { + username: socket.username + }); + }); + + // when the client emits 'add user', this listens and executes + socket.on('add user', function(username) { + if (addedUser) { + return; + } + + // we store the username in the socket session for this client + socket.username = username; + Presence.upsert(socket.id, { + username: socket.username + }); + addedUser = true; + + Presence.list(function(users) { + socket.emit('login', { + numUsers: users.length + }); + + // echo globally (all clients) that a person has connected + socket.broadcast.emit('user joined', { + username: socket.username, + numUsers: users.length + }); + }); + }); + + // when the client emits 'typing', we broadcast it to others + socket.on('typing', function() { + socket.broadcast.emit('typing', { + username: socket.username + }); + }); + + // when the client emits 'stop typing', we broadcast it to others + socket.on('stop typing', function() { + socket.broadcast.emit('stop typing', { + username: socket.username + }); + }); + + // when the user disconnects.. perform this + socket.on('disconnect', function() { + if (addedUser) { + Presence.remove(socket.id); + + Presence.list(function(users) { + // echo globally (all clients) that a person has connected + socket.broadcast.emit('user left', { + username: socket.username, + numUsers: users.length + }); + }); + } + }); +}); diff --git a/chat-app/lib/presence.js b/chat-app/lib/presence.js new file mode 100644 index 0000000..b83b76e --- /dev/null +++ b/chat-app/lib/presence.js @@ -0,0 +1,96 @@ +var redis = require('redis'); + +function Presence() { + this.client = redis.createClient({ + host: process.env.REDIS_ENDPOINT + }); +} +module.exports = new Presence(); + +/** + * Remember a present user with their connection ID + * + * @param {string} connectionId - The ID of the connection + * @param {object} meta - Any metadata about the connection +**/ +Presence.prototype.upsert = function(connectionId, meta) { + this.client.hset( + 'presence', + connectionId, + JSON.stringify({ + meta: meta, + when: Date.now() + }), + function(err) { + if (err) { + console.error('Failed to store presence in redis: ' + err); + } + } + ); +}; + +/** + * Remove a presence. Used when someone disconnects + * + * @param {string} connectionId - The ID of the connection + * @param {object} meta - Any metadata about the connection +**/ +Presence.prototype.remove = function(connectionId) { + this.client.hdel( + 'presence', + connectionId, + function(err) { + if (err) { + console.error('Failed to remove presence in redis: ' + err); + } + } + ); +}; + +/** + * Returns a list of present users, minus any expired + * + * @param {function} returnPresent - callback to return the present users +**/ +Presence.prototype.list = function(returnPresent) { + var active = []; + var dead = []; + var now = Date.now(); + var self = this; + + this.client.hgetall('presence', function(err, presence) { + if (err) { + console.error('Failed to get presence from Redis: ' + err); + return returnPresent([]); + } + + for (var connection in presence) { + var details = JSON.parse(presence[connection]); + details.connection = connection; + + if (now - details.when > 8000) { + dead.push(details); + } else { + active.push(details); + } + } + + if (dead.length) { + self._clean(dead); + } + + return returnPresent(active); + }); +}; + +/** + * Cleans a list of connections by removing expired ones + * + * @param +**/ +Presence.prototype._clean = function(toDelete) { + console.log(`Cleaning ${toDelete.length} expired presences`); + for (var presence of toDelete) { + this.remove(presence.connection); + } +}; diff --git a/chat-app/package-lock.json b/chat-app/package-lock.json new file mode 100644 index 0000000..90726a2 --- /dev/null +++ b/chat-app/package-lock.json @@ -0,0 +1,749 @@ +{ + "name": "socket.io-chat", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=" + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, + "body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + } + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "double-ended-queue": { + "version": "2.1.0-0", + "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", + "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "engine.io": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.1.5.tgz", + "integrity": "sha512-D06ivJkYxyRrcEe0bTpNnBQNgP9d3xog+qZlLbui8EsMr/DouQpf5o9FzJnWYHEYE0YsFHllUv2R1dkgYZXHcA==", + "requires": { + "accepts": "~1.3.4", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.0", + "uws": "~9.14.0", + "ws": "~3.3.1" + }, + "dependencies": { + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + } + } + }, + "engine.io-client": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.1.6.tgz", + "integrity": "sha512-hnuHsFluXnsKOndS4Hv6SvUrgdYx1pk2NqfaDMW+GWdgfU3+/V25Cj7I8a0x92idSpa5PIhJRKxPvp9mnoLsfg==", + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.1", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~3.3.1", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "engine.io-parser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", + "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "requires": { + "isarray": "2.0.1" + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "mime-db": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + }, + "mime-types": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "requires": { + "mime-db": "~1.38.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "notepack.io": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-2.1.3.tgz", + "integrity": "sha512-AgSt+cP5XMooho1Ppn8NB3FFaVWefV+qZoZncYTUSch2GAEwlYLcIIbT5YVkMlFeNHnfwOvc4HDlbvrB5BRxXA==" + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", + "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.8.0" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + }, + "redis": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", + "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", + "requires": { + "double-ended-queue": "^2.1.0-0", + "redis-commands": "^1.2.0", + "redis-parser": "^2.6.0" + } + }, + "redis-commands": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.4.0.tgz", + "integrity": "sha512-cu8EF+MtkwI4DLIT0x9P8qNTLFhQD4jLfxLR0cCNkeGzs87FN6879JOJwNQR/1zD7aSYNbU0hgsV9zGY71Itvw==" + }, + "redis-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", + "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "socket.io": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.0.4.tgz", + "integrity": "sha1-waRZDO/4fs8TxyZS8Eb3FrKeYBQ=", + "requires": { + "debug": "~2.6.6", + "engine.io": "~3.1.0", + "socket.io-adapter": "~1.1.0", + "socket.io-client": "2.0.4", + "socket.io-parser": "~3.1.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "socket.io-adapter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", + "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" + }, + "socket.io-client": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.0.4.tgz", + "integrity": "sha1-CRilUkBtxeVAs4Dc2Xr8SmQzL44=", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~2.6.4", + "engine.io-client": "~3.1.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.1.1", + "to-array": "0.1.4" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "socket.io-parser": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.3.tgz", + "integrity": "sha512-g0a2HPqLguqAczs3dMECuA1RgoGFPyvDqcbaDEdCWY9g59kdUAz3YRmaJBNKXflrHNwB7Q12Gkf/0CZXfdHR7g==", + "requires": { + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "has-binary2": "~1.0.2", + "isarray": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "socket.io-redis": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/socket.io-redis/-/socket.io-redis-5.2.0.tgz", + "integrity": "sha1-j+KtlEX8UIhvtwq8dZ1nQD1Ymd8=", + "requires": { + "debug": "~2.6.8", + "notepack.io": "~2.1.2", + "redis": "~2.8.0", + "socket.io-adapter": "~1.1.0", + "uid2": "0.0.3" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "uid2": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", + "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" + }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uws": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/uws/-/uws-9.14.0.tgz", + "integrity": "sha512-HNMztPP5A1sKuVFmdZ6BPVpBQd5bUjNC8EFMFiICK+oho/OQsAJy5hnIx4btMHiOk8j04f/DbIlqnEZ9d72dqg==", + "optional": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } + }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + } + } +} diff --git a/chat-app/package.json b/chat-app/package.json new file mode 100644 index 0000000..f8781c8 --- /dev/null +++ b/chat-app/package.json @@ -0,0 +1,18 @@ +{ + "name": "socket.io-chat", + "version": "0.0.0", + "description": "A simple chat client using socket.io", + "main": "index.js", + "author": "Grant Timmerman", + "private": true, + "license": "BSD", + "dependencies": { + "express": "^4.16.4", + "redis": "2.8.0", + "socket.io": "2.0.4", + "socket.io-redis": "5.2.0" + }, + "scripts": { + "start": "node index.js" + } +} diff --git a/chat-app/public/index.html b/chat-app/public/index.html new file mode 100644 index 0000000..b443d79 --- /dev/null +++ b/chat-app/public/index.html @@ -0,0 +1,28 @@ + + + + + Fargate.chat + + + + + + + + + + diff --git a/chat-app/public/main.js b/chat-app/public/main.js new file mode 100644 index 0000000..83d1428 --- /dev/null +++ b/chat-app/public/main.js @@ -0,0 +1,282 @@ +$(function() { + var FADE_TIME = 150; // ms + var TYPING_TIMER_LENGTH = 400; // ms + var COLORS = [ + '#e21400', '#91580f', '#f8a700', '#f78b00', + '#58dc00', '#287b00', '#a8f07a', '#4ae8c4', + '#3b88eb', '#3824aa', '#a700ff', '#d300e7' + ]; + + // Initialize variables + var $window = $(window); + var $usernameInput = $('.usernameInput'); // Input for username + var $messages = $('.messages'); // Messages area + var $inputMessage = $('.inputMessage'); // Input message input box + + var $loginPage = $('.login.page'); // The login page + var $chatPage = $('.chat.page'); // The chatroom page + + // Prompt for setting a username + var username; + var connected = false; + var typing = false; + var lastTypingTime; + var $currentInput = $usernameInput.focus(); + + var socket = io(); + + function addParticipantsMessage (data) { + var message = ''; + if (data.numUsers === 1) { + message += "there's 1 participant"; + } else { + message += "there are " + data.numUsers + " participants"; + } + log(message); + } + + // Sets the client's username + function setUsername () { + username = cleanInput($usernameInput.val().trim()); + + // If the username is valid + if (username) { + $loginPage.fadeOut(); + $chatPage.show(); + $loginPage.off('click'); + $currentInput = $inputMessage.focus(); + + // Tell the server your username + socket.emit('add user', username); + } + } + + // Sends a chat message + function sendMessage () { + var message = $inputMessage.val(); + // Prevent markup from being injected into the message + message = cleanInput(message); + // if there is a non-empty message and a socket connection + if (message && connected) { + $inputMessage.val(''); + addChatMessage({ + username: username, + message: message + }); + // tell server to execute 'new message' and send along one parameter + socket.emit('new message', message); + } + } + + // Log a message + function log (message, options) { + var $el = $('
  • ').addClass('log').text(message); + addMessageElement($el, options); + } + + // Adds the visual chat message to the message list + function addChatMessage (data, options) { + // Don't fade the message in if there is an 'X was typing' + var $typingMessages = getTypingMessages(data); + options = options || {}; + if ($typingMessages.length !== 0) { + options.fade = false; + $typingMessages.remove(); + } + + var $usernameDiv = $('') + .text(data.username) + .css('color', getUsernameColor(data.username)); + var $messageBodyDiv = $('') + .text(data.message); + + var typingClass = data.typing ? 'typing' : ''; + var $messageDiv = $('
  • ') + .data('username', data.username) + .addClass(typingClass) + .append($usernameDiv, $messageBodyDiv); + + addMessageElement($messageDiv, options); + } + + // Adds the visual chat typing message + function addChatTyping (data) { + data.typing = true; + data.message = 'is typing'; + addChatMessage(data); + } + + // Removes the visual chat typing message + function removeChatTyping (data) { + getTypingMessages(data).fadeOut(function () { + $(this).remove(); + }); + } + + // Adds a message element to the messages and scrolls to the bottom + // el - The element to add as a message + // options.fade - If the element should fade-in (default = true) + // options.prepend - If the element should prepend + // all other messages (default = false) + function addMessageElement (el, options) { + var $el = $(el); + + // Setup default options + if (!options) { + options = {}; + } + if (typeof options.fade === 'undefined') { + options.fade = true; + } + if (typeof options.prepend === 'undefined') { + options.prepend = false; + } + + // Apply options + if (options.fade) { + $el.hide().fadeIn(FADE_TIME); + } + if (options.prepend) { + $messages.prepend($el); + } else { + $messages.append($el); + } + $messages[0].scrollTop = $messages[0].scrollHeight; + } + + // Prevents input from having injected markup + function cleanInput (input) { + return $('
    ').text(input).html(); + } + + // Updates the typing event + function updateTyping () { + if (connected) { + if (!typing) { + typing = true; + socket.emit('typing'); + } + lastTypingTime = (new Date()).getTime(); + + setTimeout(function () { + var typingTimer = (new Date()).getTime(); + var timeDiff = typingTimer - lastTypingTime; + if (timeDiff >= TYPING_TIMER_LENGTH && typing) { + socket.emit('stop typing'); + typing = false; + } + }, TYPING_TIMER_LENGTH); + } + } + + // Gets the 'X is typing' messages of a user + function getTypingMessages (data) { + return $('.typing.message').filter(function (i) { + return $(this).data('username') === data.username; + }); + } + + // Gets the color of a username through our hash function + function getUsernameColor (username) { + // Compute hash code + var hash = 7; + for (var i = 0; i < username.length; i++) { + hash = username.charCodeAt(i) + (hash << 5) - hash; + } + // Calculate color + var index = Math.abs(hash % COLORS.length); + return COLORS[index]; + } + + // Keyboard events + + $window.keydown(function (event) { + // Auto-focus the current input when a key is typed + if (!(event.ctrlKey || event.metaKey || event.altKey)) { + $currentInput.focus(); + } + // When the client hits ENTER on their keyboard + if (event.which === 13) { + if (username) { + sendMessage(); + socket.emit('stop typing'); + typing = false; + } else { + setUsername(); + } + } + }); + + $inputMessage.on('input', function() { + updateTyping(); + }); + + // Click events + + // Focus input when clicking anywhere on login page + $loginPage.click(function () { + $currentInput.focus(); + }); + + // Focus input when clicking on the message input's border + $inputMessage.click(function () { + $inputMessage.focus(); + }); + + // Socket events + + // Whenever the server emits 'login', log the login message + socket.on('login', function (data) { + connected = true; + // Display the welcome message + var message = "Welcome to Socket.IO Chat – "; + log(message, { + prepend: true + }); + addParticipantsMessage(data); + }); + + // Whenever the server emits 'new message', update the chat body + socket.on('new message', function (data) { + addChatMessage(data); + }); + + // Whenever the server emits 'user joined', log it in the chat body + socket.on('user joined', function (data) { + log(data.username + ' joined'); + addParticipantsMessage(data); + }); + + // Whenever the server emits 'user left', log it in the chat body + socket.on('user left', function (data) { + log(data.username + ' left'); + addParticipantsMessage(data); + removeChatTyping(data); + }); + + // Whenever the server emits 'typing', show the typing message + socket.on('typing', function (data) { + addChatTyping(data); + }); + + // Whenever the server emits 'stop typing', kill the typing message + socket.on('stop typing', function (data) { + removeChatTyping(data); + }); + + socket.on('disconnect', function () { + log('you have been disconnected'); + }); + + socket.on('reconnect', function () { + log('you have been reconnected'); + if (username) { + socket.emit('add user', username); + } + }); + + socket.on('reconnect_error', function () { + log('attempt to reconnect has failed'); + }); + +}); diff --git a/chat-app/public/style.css b/chat-app/public/style.css new file mode 100644 index 0000000..3de49cf --- /dev/null +++ b/chat-app/public/style.css @@ -0,0 +1,149 @@ +/* Fix user-agent */ + +* { + box-sizing: border-box; +} + +html { + font-weight: 300; + -webkit-font-smoothing: antialiased; +} + +html, input { + font-family: + "HelveticaNeue-Light", + "Helvetica Neue Light", + "Helvetica Neue", + Helvetica, + Arial, + "Lucida Grande", + sans-serif; +} + +html, body { + height: 100%; + margin: 0; + padding: 0; +} + +ul { + list-style: none; + word-wrap: break-word; +} + +/* Pages */ + +.pages { + height: 100%; + margin: 0; + padding: 0; + width: 100%; +} + +.page { + height: 100%; + position: absolute; + width: 100%; +} + +/* Login Page */ + +.login.page { + background-color: #401664; +} + +.login.page .form { + height: 100px; + margin-top: -100px; + position: absolute; + + text-align: center; + top: 50%; + width: 100%; +} + +.login.page .form .usernameInput { + background-color: transparent; + border: none; + border-bottom: 2px solid #fff; + outline: none; + padding-bottom: 15px; + text-align: center; + width: 400px; +} + +.login.page .title { + font-size: 200%; +} + +.login.page .usernameInput { + font-size: 200%; + letter-spacing: 3px; +} + +.login.page .title, .login.page .usernameInput { + color: #fff; + font-weight: 100; +} + +/* Chat page */ + +.chat.page { + display: none; +} + +/* Font */ + +.messages { + font-size: 150%; +} + +.inputMessage { + font-size: 100%; +} + +.log { + color: gray; + font-size: 70%; + margin: 5px; + text-align: center; +} + +/* Messages */ + +.chatArea { + height: 100%; + padding-bottom: 60px; +} + +.messages { + height: 100%; + margin: 0; + overflow-y: scroll; + padding: 10px 20px 10px 20px; +} + +.message.typing .messageBody { + color: gray; +} + +.username { + font-weight: 700; + overflow: hidden; + padding-right: 15px; + text-align: right; +} + +/* Input */ + +.inputMessage { + border: 10px solid #000; + bottom: 0; + height: 60px; + left: 0; + outline: none; + padding-left: 10px; + position: absolute; + right: 0; + width: 100%; +} diff --git a/chat-app/recipes/chat-service.yml b/chat-app/recipes/chat-service.yml new file mode 100644 index 0000000..770db66 --- /dev/null +++ b/chat-app/recipes/chat-service.yml @@ -0,0 +1,320 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: Socket.io chat service +Parameters: + EnvironmentName: + Type: String + Default: production + Description: A name for the environment that this cloudformation will be part of. + Used to locate other resources in the same environment. + ServiceName: + Type: String + Default: chat + Description: A name for the service + ImageUrl: + Type: String + Default: nginx + Description: The url of a docker image that contains the application process that + will handle the traffic for this service + ContainerPort: + Type: Number + Default: 3000 + Description: What port number the application inside the docker container is binding to + ContainerCpu: + Type: Number + Default: 1024 + Description: How much CPU to give the container. 1024 is 1 CPU + ContainerMemory: + Type: Number + Default: 2048 + Description: How much memory in megabytes to give the container + Path: + Type: String + Default: "*" + Description: A path on the public load balancer that this service + should be connected to. Use * to send all load balancer + traffic to this service. + Priority: + Type: Number + Default: 1 + Description: The priority for the routing rule added to the load balancer. + This only applies if your have multiple services which have been + assigned to different paths on the load balancer. + DesiredCount: + Type: Number + Default: 2 + Description: How many copies of the service task to run + Role: + Type: String + Default: "" + Description: (Optional) An IAM role to give the service's containers if the code within needs to + access other AWS resources like S3 buckets, DynamoDB tables, etc + +Conditions: + HasCustomRole: !Not [ !Equals [!Ref 'Role', ''] ] + +Resources: + # A log group for storing the container logs for this service + LogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Join ['-', [!Ref 'EnvironmentName', 'service', !Ref 'ServiceName']] + + # The task definition. This is a simple metadata description of what + # container to run, and what resource requirements it has. + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Ref 'ServiceName' + Cpu: !Ref 'ContainerCpu' + Memory: !Ref 'ContainerMemory' + NetworkMode: awsvpc + RequiresCompatibilities: + - FARGATE + ExecutionRoleArn: + Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'ECSTaskExecutionRole']] + TaskRoleArn: + Fn::If: + - 'HasCustomRole' + - !Ref 'Role' + - !Ref "AWS::NoValue" + ContainerDefinitions: + - Name: !Ref 'ServiceName' + Cpu: !Ref 'ContainerCpu' + Memory: !Ref 'ContainerMemory' + Image: !Ref 'ImageUrl' + Environment: + - Name: REDIS_ENDPOINT + Value: + Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'RedisEndpoint']] + PortMappings: + - ContainerPort: !Ref 'ContainerPort' + LogConfiguration: + LogDriver: 'awslogs' + Options: + awslogs-group: !Join ['-', [!Ref 'EnvironmentName', 'service', !Ref 'ServiceName']] + awslogs-region: !Ref 'AWS::Region' + awslogs-stream-prefix: !Ref 'ServiceName' + + # The service. The service is a resource which allows you to run multiple + # copies of a type of task, and gather up their logs and metrics, as well + # as monitor the number of running tasks and replace any that have crashed + Service: + Type: AWS::ECS::Service + DependsOn: LoadBalancerRule + Properties: + ServiceName: !Ref 'ServiceName' + Cluster: + Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'ClusterName']] + LaunchType: FARGATE + DeploymentConfiguration: + MaximumPercent: 200 + MinimumHealthyPercent: 75 + DesiredCount: !Ref 'DesiredCount' + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: ENABLED + SecurityGroups: + - Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'FargateContainerSecurityGroup']] + Subnets: + - Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'PublicSubnetOne']] + - Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'PublicSubnetTwo']] + TaskDefinition: !Ref 'TaskDefinition' + LoadBalancers: + - ContainerName: !Ref 'ServiceName' + ContainerPort: !Ref 'ContainerPort' + TargetGroupArn: !Ref 'TargetGroup' + + # A target group. This is used for keeping track of all the tasks, and + # what IP addresses / port numbers they have. You can query it yourself, + # to use the addresses yourself, but most often this target group is just + # connected to an application load balancer, or network load balancer, so + # it can automatically distribute traffic across all the targets. + TargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + HealthCheckIntervalSeconds: 6 + HealthCheckPath: / + HealthCheckProtocol: HTTP + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 2 + TargetType: ip + Name: !Ref 'ServiceName' + Port: 80 + Protocol: HTTP + UnhealthyThresholdCount: 2 + TargetGroupAttributes: + - Key: stickiness.enabled + Value: true + - Key: deregistration_delay.timeout_seconds + Value: 30 + VpcId: + Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'VPCId']] + + # Create a rule on the load balancer for routing traffic to the target group + LoadBalancerRule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - TargetGroupArn: !Ref 'TargetGroup' + Type: 'forward' + Conditions: + - Field: path-pattern + Values: [!Ref 'Path'] + ListenerArn: + Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'PublicListener']] + Priority: !Ref 'Priority' + + # Enable autoscaling for this service + ScalableTarget: + Type: AWS::ApplicationAutoScaling::ScalableTarget + DependsOn: Service + Properties: + ServiceNamespace: 'ecs' + ScalableDimension: 'ecs:service:DesiredCount' + ResourceId: + Fn::Join: + - '/' + - - service + - Fn::ImportValue: !Join [':', [!Ref 'EnvironmentName', 'ClusterName']] + - !Ref 'ServiceName' + MinCapacity: 2 + MaxCapacity: 10 + RoleARN: + Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'AutoscalingRole']] + + # Create scaling policies for the service + ScaleDownPolicy: + Type: AWS::ApplicationAutoScaling::ScalingPolicy + DependsOn: ScalableTarget + Properties: + PolicyName: + Fn::Join: + - '/' + - - scale + - !Ref 'EnvironmentName' + - !Ref 'ServiceName' + - down + PolicyType: StepScaling + ResourceId: + Fn::Join: + - '/' + - - service + - Fn::ImportValue: !Join [':', [!Ref 'EnvironmentName', 'ClusterName']] + - !Ref 'ServiceName' + ScalableDimension: 'ecs:service:DesiredCount' + ServiceNamespace: 'ecs' + StepScalingPolicyConfiguration: + AdjustmentType: 'ChangeInCapacity' + StepAdjustments: + - MetricIntervalUpperBound: 0 + ScalingAdjustment: -1 + MetricAggregationType: 'Average' + Cooldown: 60 + + ScaleUpPolicy: + Type: AWS::ApplicationAutoScaling::ScalingPolicy + DependsOn: ScalableTarget + Properties: + PolicyName: + Fn::Join: + - '/' + - - scale + - !Ref 'EnvironmentName' + - !Ref 'ServiceName' + - up + PolicyType: StepScaling + ResourceId: + Fn::Join: + - '/' + - - service + - Fn::ImportValue: !Join [':', [!Ref 'EnvironmentName', 'ClusterName']] + - !Ref 'ServiceName' + ScalableDimension: 'ecs:service:DesiredCount' + ServiceNamespace: 'ecs' + StepScalingPolicyConfiguration: + AdjustmentType: 'ChangeInCapacity' + StepAdjustments: + - MetricIntervalLowerBound: 0 + MetricIntervalUpperBound: 15 + ScalingAdjustment: 1 + - MetricIntervalLowerBound: 15 + MetricIntervalUpperBound: 25 + ScalingAdjustment: 2 + - MetricIntervalLowerBound: 25 + ScalingAdjustment: 3 + MetricAggregationType: 'Average' + Cooldown: 60 + + # Create alarms to trigger these policies + LowCpuUsageAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + AlarmName: + Fn::Join: + - '-' + - - low-cpu + - !Ref 'EnvironmentName' + - !Ref 'ServiceName' + AlarmDescription: + Fn::Join: + - ' ' + - - "Low CPU utilization for service" + - !Ref 'ServiceName' + - "in stack" + - !Ref 'EnvironmentName' + MetricName: CPUUtilization + Namespace: AWS/ECS + Dimensions: + - Name: ServiceName + Value: !Ref 'ServiceName' + - Name: ClusterName + Value: + Fn::ImportValue: !Join [':', [!Ref 'EnvironmentName', 'ClusterName']] + Statistic: Average + Period: 60 + EvaluationPeriods: 1 + Threshold: 20 + ComparisonOperator: LessThanOrEqualToThreshold + AlarmActions: + - !Ref ScaleDownPolicy + + HighCpuUsageAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + AlarmName: + Fn::Join: + - '-' + - - high-cpu + - !Ref 'EnvironmentName' + - !Ref 'ServiceName' + AlarmDescription: + Fn::Join: + - ' ' + - - "High CPU utilization for service" + - !Ref 'ServiceName' + - "in stack" + - !Ref 'EnvironmentName' + MetricName: CPUUtilization + Namespace: AWS/ECS + Dimensions: + - Name: ServiceName + Value: !Ref 'ServiceName' + - Name: ClusterName + Value: + Fn::ImportValue: !Join [':', [!Ref 'EnvironmentName', 'ClusterName']] + Statistic: Average + Period: 60 + EvaluationPeriods: 1 + Threshold: 70 + ComparisonOperator: GreaterThanOrEqualToThreshold + AlarmActions: + - !Ref ScaleUpPolicy diff --git a/chat-app/recipes/cluster.yml b/chat-app/recipes/cluster.yml new file mode 100644 index 0000000..217476c --- /dev/null +++ b/chat-app/recipes/cluster.yml @@ -0,0 +1,329 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: The baseline resources used to create a Fargate environment + to launch containerized applications in. +Parameters: + EnvironmentName: + Type: String + Default: production + Description: A name for the environment that this cloudformation will be part of. +Mappings: + # Hard values for the subnet masks. These masks define + # the range of internal IP addresses that can be assigned. + # The VPC can have all IP's from 10.0.0.0 to 10.0.255.255 + # There are two subnets which cover the ranges: + # + # 10.0.0.0 - 10.0.0.255 + # 10.0.1.0 - 10.0.1.255 + # + # If you need more IP addresses (perhaps you have so many + # instances that you run out) then you can customize these + # ranges to add more + SubnetConfig: + VPC: + CIDR: '10.0.0.0/16' + PublicOne: + CIDR: '10.0.0.0/24' + PublicTwo: + CIDR: '10.0.1.0/24' +Resources: + # VPC in which containers will be networked. + # It has two public subnets + # We distribute the subnets across the first two available subnets + # for the region, for high availability. + VPC: + Type: AWS::EC2::VPC + Properties: + EnableDnsSupport: true + EnableDnsHostnames: true + CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] + + # Two public subnets, where containers can have public IP addresses + PublicSubnetOne: + Type: AWS::EC2::Subnet + Properties: + AvailabilityZone: + Fn::Select: + - 0 + - Fn::GetAZs: {Ref: 'AWS::Region'} + VpcId: !Ref 'VPC' + CidrBlock: !FindInMap ['SubnetConfig', 'PublicOne', 'CIDR'] + MapPublicIpOnLaunch: true + PublicSubnetTwo: + Type: AWS::EC2::Subnet + Properties: + AvailabilityZone: + Fn::Select: + - 1 + - Fn::GetAZs: {Ref: 'AWS::Region'} + VpcId: !Ref 'VPC' + CidrBlock: !FindInMap ['SubnetConfig', 'PublicTwo', 'CIDR'] + MapPublicIpOnLaunch: true + + # Setup networking resources for the public subnets. Containers + # in the public subnets have public IP addresses and the routing table + # sends network traffic via the internet gateway. + InternetGateway: + Type: AWS::EC2::InternetGateway + GatewayAttachement: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + VpcId: !Ref 'VPC' + InternetGatewayId: !Ref 'InternetGateway' + PublicRouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref 'VPC' + PublicRoute: + Type: AWS::EC2::Route + DependsOn: GatewayAttachement + Properties: + RouteTableId: !Ref 'PublicRouteTable' + DestinationCidrBlock: '0.0.0.0/0' + GatewayId: !Ref 'InternetGateway' + PublicSubnetOneRouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PublicSubnetOne + RouteTableId: !Ref PublicRouteTable + PublicSubnetTwoRouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PublicSubnetTwo + RouteTableId: !Ref PublicRouteTable + + # ECS Resources + ECSCluster: + Type: AWS::ECS::Cluster + + # A security group for the containers we will run in Fargate. + # Two rules, allowing network traffic from a public facing load + # balancer and from other members of the security group. + # + # Remove any of the following ingress rules that are not needed. + # If you want to make direct requests to a container using its + # public IP address you'll need to add a security group rule + # to allow traffic from all IP addresses. + FargateContainerSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Access to the Fargate containers + VpcId: !Ref 'VPC' + EcsSecurityGroupIngressFromPublicALB: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: Ingress from the public ALB + GroupId: !Ref 'FargateContainerSecurityGroup' + IpProtocol: -1 + SourceSecurityGroupId: !Ref 'PublicLoadBalancerSG' + EcsSecurityGroupIngressFromSelf: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: Ingress from other containers in the same security group + GroupId: !Ref 'FargateContainerSecurityGroup' + IpProtocol: -1 + SourceSecurityGroupId: !Ref 'FargateContainerSecurityGroup' + + # Load balancers for getting traffic to containers. + # This sample template creates one load balancer: + # + # - One public load balancer, hosted in public subnets that is accessible + # to the public, and is intended to route traffic to one or more public + # facing services. + + # A public facing load balancer, this is used for accepting traffic from the public + # internet and directing it to public facing microservices + PublicLoadBalancerSG: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Access to the public facing load balancer + VpcId: !Ref 'VPC' + SecurityGroupIngress: + # Allow access to ALB from anywhere on the internet + - CidrIp: 0.0.0.0/0 + IpProtocol: -1 + PublicLoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Scheme: internet-facing + LoadBalancerAttributes: + - Key: idle_timeout.timeout_seconds + Value: '30' + Subnets: + # The load balancer is placed into the public subnets, so that traffic + # from the internet can reach the load balancer directly via the internet gateway + - !Ref PublicSubnetOne + - !Ref PublicSubnetTwo + SecurityGroups: [!Ref 'PublicLoadBalancerSG'] + # A dummy target group is used to setup the ALB to just drop traffic + # initially, before any real service target groups have been added. + DummyTargetGroupPublic: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + HealthCheckIntervalSeconds: 6 + HealthCheckPath: / + HealthCheckProtocol: HTTP + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 2 + Name: !Join ['-', [!Ref 'EnvironmentName', 'drop-1']] + Port: 80 + Protocol: HTTP + UnhealthyThresholdCount: 2 + VpcId: !Ref 'VPC' + PublicLoadBalancerListener: + Type: AWS::ElasticLoadBalancingV2::Listener + DependsOn: + - PublicLoadBalancer + Properties: + DefaultActions: + - TargetGroupArn: !Ref 'DummyTargetGroupPublic' + Type: 'forward' + LoadBalancerArn: !Ref 'PublicLoadBalancer' + Port: 80 + Protocol: HTTP + + # This is an IAM role which authorizes ECS to manage resources on your + # account on your behalf, such as updating your load balancer with the + # details of where your containers are, so that traffic can reach your + # containers. + ECSRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: [ecs.amazonaws.com] + Action: ['sts:AssumeRole'] + Path: / + Policies: + - PolicyName: ecs-service + PolicyDocument: + Statement: + - Effect: Allow + Action: + # Rules which allow ECS to attach network interfaces to instances + # on your behalf in order for awsvpc networking mode to work right + - 'ec2:AttachNetworkInterface' + - 'ec2:CreateNetworkInterface' + - 'ec2:CreateNetworkInterfacePermission' + - 'ec2:DeleteNetworkInterface' + - 'ec2:DeleteNetworkInterfacePermission' + - 'ec2:Describe*' + - 'ec2:DetachNetworkInterface' + + # Rules which allow ECS to update load balancers on your behalf + # with the information sabout how to send traffic to your containers + - 'elasticloadbalancing:DeregisterInstancesFromLoadBalancer' + - 'elasticloadbalancing:DeregisterTargets' + - 'elasticloadbalancing:Describe*' + - 'elasticloadbalancing:RegisterInstancesWithLoadBalancer' + - 'elasticloadbalancing:RegisterTargets' + Resource: '*' + + # This is a role which is used by the ECS tasks themselves. + ECSTaskExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: [ecs-tasks.amazonaws.com] + Action: ['sts:AssumeRole'] + Path: / + Policies: + - PolicyName: AmazonECSTaskExecutionRolePolicy + PolicyDocument: + Statement: + - Effect: Allow + Action: + # Allow the ECS Tasks to download images from ECR + - 'ecr:GetAuthorizationToken' + - 'ecr:BatchCheckLayerAvailability' + - 'ecr:GetDownloadUrlForLayer' + - 'ecr:BatchGetImage' + + # Allow the ECS tasks to upload logs to CloudWatch + - 'logs:CreateLogStream' + - 'logs:PutLogEvents' + Resource: '*' + + # A role used by AWS Autoscaling to get the stats for a Fargate + # service, and update it to increase or decrease the number of containers + AutoscalingRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: [application-autoscaling.amazonaws.com] + Action: ['sts:AssumeRole'] + Path: / + Policies: + - PolicyName: service-autoscaling + PolicyDocument: + Statement: + - Effect: Allow + Action: + - 'application-autoscaling:*' + - 'cloudwatch:DescribeAlarms' + - 'cloudwatch:PutMetricAlarm' + - 'ecs:DescribeServices' + - 'ecs:UpdateService' + Resource: '*' + +# These are the values output by the CloudFormation template. Be careful +# about changing any of them, because of them are exported with specific +# names so that the other task related CF templates can use them. +Outputs: + ClusterName: + Description: The name of the ECS cluster + Value: !Ref 'ECSCluster' + Export: + Name: !Join [ ':', [ !Ref 'EnvironmentName', 'ClusterName' ] ] + ExternalUrl: + Description: The url of the external load balancer + Value: !Join ['', ['http://', !GetAtt 'PublicLoadBalancer.DNSName']] + Export: + Name: !Join [ ':', [ !Ref 'EnvironmentName', 'ExternalUrl' ] ] + ECSRole: + Description: The ARN of the ECS role + Value: !GetAtt 'ECSRole.Arn' + Export: + Name: !Join [ ':', [ !Ref 'EnvironmentName', 'ECSRole' ] ] + ECSTaskExecutionRole: + Description: The ARN of the ECS role + Value: !GetAtt 'ECSTaskExecutionRole.Arn' + Export: + Name: !Join [ ':', [ !Ref 'EnvironmentName', 'ECSTaskExecutionRole' ] ] + AutoscalingRole: + Description: The ARN of the ECS role + Value: !GetAtt 'ECSTaskExecutionRole.Arn' + Export: + Name: !Join [ ':', [ !Ref 'EnvironmentName', 'AutoscalingRole' ] ] + PublicListener: + Description: The ARN of the public load balancer's Listener + Value: !Ref PublicLoadBalancerListener + Export: + Name: !Join [ ':', [ !Ref 'EnvironmentName', 'PublicListener' ] ] + VPCId: + Description: The ID of the VPC that this stack is deployed in + Value: !Ref 'VPC' + Export: + Name: !Join [ ':', [ !Ref 'EnvironmentName', 'VPCId' ] ] + PublicSubnetOne: + Description: Public subnet one + Value: !Ref 'PublicSubnetOne' + Export: + Name: !Join [ ':', [ !Ref 'EnvironmentName', 'PublicSubnetOne' ] ] + PublicSubnetTwo: + Description: Public subnet two + Value: !Ref 'PublicSubnetTwo' + Export: + Name: !Join [ ':', [ !Ref 'EnvironmentName', 'PublicSubnetTwo' ] ] + FargateContainerSecurityGroup: + Description: A security group used to allow Fargate containers to receive traffic + Value: !Ref 'FargateContainerSecurityGroup' + Export: + Name: !Join [ ':', [ !Ref 'EnvironmentName', 'FargateContainerSecurityGroup' ] ] diff --git a/chat-app/recipes/resources.yml b/chat-app/recipes/resources.yml new file mode 100644 index 0000000..d615279 --- /dev/null +++ b/chat-app/recipes/resources.yml @@ -0,0 +1,59 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: Redis, and any other resources that the chat app needs. +Parameters: + EnvironmentName: + Type: String + Default: production + Description: The environment name, used for locating outputs from the + prerequisite stacks +Resources: + # Subnet group to control where the Redis gets placed + RedisSubnetGroup: + Type: AWS::ElastiCache::SubnetGroup + Properties: + Description: Group of subnets to place Redis into + SubnetIds: + - Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'PublicSubnetOne']] + - Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'PublicSubnetTwo']] + + # Security group to add the Redis cluster to the VPC, + # and to allow the Fargate containers to talk to Redis on port 6379 + RedisSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: "Redis Security Group" + VpcId: + Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'VPCId']] + RedisIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: Ingress from Fargate containers + GroupId: !Ref 'RedisSecurityGroup' + IpProtocol: tcp + FromPort: 6379 + ToPort: 6379 + SourceSecurityGroupId: + Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'FargateContainerSecurityGroup']] + + # The cluster itself. + Redis: + Type: AWS::ElastiCache::CacheCluster + Properties: + Engine: redis + CacheNodeType: cache.m4.large + NumCacheNodes: 1 + CacheSubnetGroupName: !Ref 'RedisSubnetGroup' + VpcSecurityGroupIds: + - !GetAtt 'RedisSecurityGroup.GroupId' + +Outputs: + RedisEndpoint: + Description: The endpoint of the redis cluster + Value: !GetAtt 'Redis.RedisEndpoint.Address' + Export: + Name: !Join [ ':', [ !Ref 'EnvironmentName', 'RedisEndpoint' ] ] +