ECS Module

In here I'm going to create a these modules and consume those to creaet the end solution with Terraform.

The project structure looks like this,

|-- modules
    |-- alb-rule
        |-- rule.tf
    |-- alb
        |-- alb.tf
        |-- output.tf
        |-- securitygroups.tf
        |-- vars.tf
    |-- ecs-cluster
        |-- templates
            |-- ecs_init.tpl
        |-- cloudwatch.tf
        |-- ecs.tf
        |-- iam.tf
        |-- output.tf
        |-- securitygroups.tf
        |-- vars.tf
    |-- ecs-service
        |-- alb.tf
        |-- ecs-service.json
        |-- ecs-service.tf
        |-- output.tf
        |-- vars.tf
|-- dev-env
    |-- ecr-login.sh
    |-- ecs.sh
    |-- key.tf
    |-- provider.tf
    |-- securitygroup.tf
    |-- vars.tf
    |-- vpc.tf

modules/alb-rule/rule.tf:

variable "LISTENER_ARN" {}
variable "PRIORITY" {}
variable "TARGET_GROUP_ARN" {}
variable "CONDITION_FIELD" {}

variable "CONDITION_VALUES" {
  type = "list"
}

resource "aws_lb_listener_rule" "alb_rule" {
  listener_arn = "${var.LISTENER_ARN}"
  priority     = "${var.PRIORITY}"

  action {
    type             = "forward"
    target_group_arn = "${var.TARGET_GROUP_ARN}"
  }

  condition {
    field  = "${var.CONDITION_FIELD}"
    values = ["${var.CONDITION_VALUES}"]
  }
}

modules/alb/alb.tf:

#
# ECS ALB
#
# alb main definition
resource "aws_alb" "alb" {
  name            = "${var.ALB_NAME}"
  internal        = "${var.INTERNAL}"
  security_groups = ["${aws_security_group.alb.id}"]
  subnets         = ["${split(",", var.VPC_SUBNETS)}"]

  enable_deletion_protection = false
}

# certificate
data "aws_acm_certificate" "certificate" {
  domain   = "${var.DOMAIN}"
  statuses = ["ISSUED", "PENDING_VALIDATION"]
}

# alb listener (https)
resource "aws_alb_listener" "alb-https" {
  load_balancer_arn = "${aws_alb.alb.arn}"
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = "${data.aws_acm_certificate.certificate.arn}"

  default_action {
    target_group_arn = "${var.DEFAULT_TARGET_ARN}"
    type             = "forward"
  }
}

# alb listener (http)
resource "aws_alb_listener" "alb-http" {
  load_balancer_arn = "${aws_alb.alb.arn}"
  port              = "80"
  protocol          = "HTTP"

  default_action {
    target_group_arn = "${var.DEFAULT_TARGET_ARN}"
    type             = "forward"
  }
}

modules/alb/output.tf:

output "dns_name" {
  value = "${aws_alb.alb.dns_name}"
}

output "alb_arn" {
  value = "${aws_alb.alb.arn}"
}

output "zone_id" {
  value = "${aws_alb.alb.zone_id}"
}

output "http_listener_arn" {
  value = "${aws_alb_listener.alb-http.arn}"
}
output "https_listener_arn" {
  value = "${aws_alb_listener.alb-https.arn}"
}

modules/alb/securitygroups.tf:

resource "aws_security_group" "alb" {
  name        = "${var.ALB_NAME}"
  vpc_id      = "${var.VPC_ID}"
  description = "${var.ALB_NAME}"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_security_group_rule" "cluster-allow-alb" {
  security_group_id        = "${var.ECS_SG}"
  type                     = "ingress"
  from_port                = 32768
  to_port                  = 61000
  protocol                 = "tcp"
  source_security_group_id = "${aws_security_group.alb.id}"
}

modules/alb/vars.tf:

variable "ALB_NAME" {}
variable "INTERNAL" {}
variable "VPC_ID" {}
variable "VPC_SUBNETS" {}
variable "DOMAIN" {}
variable "DEFAULT_TARGET_ARN" {}
variable "ECS_SG" {
  default = ""
}

modules/ecs-cluster/templates/ecs_init.tpl:

#!/bin/bash
echo 'ECS_CLUSTER=${CLUSTER_NAME}' > /etc/ecs/ecs.config
start ecs

modules/ecs-cluster/cloudwatch.tf:

#
# Cloudwatch logs
#
resource "aws_cloudwatch_log_group" "cluster" {
  name = "${var.LOG_GROUP}"
}

modules/ecs-cluster/ecs.tf

#
# ECS ami
#

data "aws_ami" "ecs" {
  most_recent = true

  filter {
    name   = "name"
    values = ["amzn-ami-*-amazon-ecs-optimized"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["591542846629"] # AWS
}

#
# ECS cluster
#

resource "aws_ecs_cluster" "cluster" {
  name = "${var.CLUSTER_NAME}"
}

data "template_file" "ecs_init" {
    template = "${file("${path.module}/templates/ecs_init.tpl")}"
    vars {
        CLUSTER_NAME   = "${var.CLUSTER_NAME}"
    }
}

#
# launchconfig
#
resource "aws_launch_configuration" "cluster" {
  name_prefix          = "ecs-${var.CLUSTER_NAME}-launchconfig"
  image_id             = "${data.aws_ami.ecs.id}"
  instance_type        = "${var.INSTANCE_TYPE}"
  key_name             = "${var.SSH_KEY_NAME}"
  iam_instance_profile = "${aws_iam_instance_profile.cluster-ec2-role.id}"
  security_groups      = ["${aws_security_group.cluster.id}"]
  user_data            = "${data.template_file.ecs_init.rendered}"
  lifecycle {
    create_before_destroy = true
  }
}

#
# autoscaling
#
resource "aws_autoscaling_group" "cluster" {
  name                 = "ecs-${var.CLUSTER_NAME}-autoscaling"
  vpc_zone_identifier  = ["${split(",", var.VPC_SUBNETS)}"]
  launch_configuration = "${aws_launch_configuration.cluster.name}"
  termination_policies = ["${split(",", var.ECS_TERMINATION_POLICIES)}"]
  min_size             = "${var.ECS_MINSIZE}"
  max_size             = "${var.ECS_MAXSIZE}"
  desired_capacity     = "${var.ECS_DESIRED_CAPACITY}"

  tag {
    key                 = "Name"
    value               = "${var.CLUSTER_NAME}-ecs"
    propagate_at_launch = true
  }
}

modules/ecs-cluster/iam.tf:

#
# ECS service role
#
resource "aws_iam_role" "cluster-service-role" {
  name = "ecs-service-role-${var.CLUSTER_NAME}"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "ecs.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

resource "aws_iam_role_policy" "cluster-service-role" {
  name = "${var.CLUSTER_NAME}-policy"
  role = "${aws_iam_role.cluster-service-role.name}"

  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:Describe*",
                "elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
                "elasticloadbalancing:DeregisterTargets",
                "elasticloadbalancing:Describe*",
                "elasticloadbalancing:RegisterInstancesWithLoadBalancer",
                "elasticloadbalancing:RegisterTargets"
            ],
            "Resource": "*"
        }
    ]
}
EOF
}

#
# IAM EC2 role
#
resource "aws_iam_role" "cluster-ec2-role" {
  name = "ecs-${var.CLUSTER_NAME}-ec2-role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

resource "aws_iam_instance_profile" "cluster-ec2-role" {
  name = "ecs-${var.CLUSTER_NAME}-ec2-role"
  role = "${aws_iam_role.cluster-ec2-role.name}"
}

resource "aws_iam_role_policy" "cluster-ec2-role" {
  name = "cluster-ec2-role-policy"
  role = "${aws_iam_role.cluster-ec2-role.id}"

  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
              "ecs:CreateCluster",
              "ecs:DeregisterContainerInstance",
              "ecs:DiscoverPollEndpoint",
              "ecs:Poll",
              "ecs:RegisterContainerInstance",
              "ecs:StartTelemetrySession",
              "ecs:Submit*",
              "ecs:StartTask",
              "ecr:GetAuthorizationToken",
              "ecr:BatchCheckLayerAvailability",
              "ecr:GetDownloadUrlForLayer",
              "ecr:BatchGetImage",
              "logs:CreateLogStream",
              "logs:PutLogEvents"
            ],
            "Resource": "*"
        },
        {
          "Effect": "Allow",
          "Action": [
              "logs:*"
          ],
          "Resource": [
              "arn:aws:logs:${var.AWS_REGION}:${var.AWS_ACCOUNT_ID}:log-group:${var.LOG_GROUP}:*"
          ]
        }
    ]
}
EOF
}

modules/ecs-cluster/output.tf:

output "cluster_arn" {
  value = "${aws_ecs_cluster.cluster.id}"
}

output "service_role_arn" {
  value = "${aws_iam_role.cluster-service-role.arn}"
}

output "cluster_sg" {
  value = "${aws_security_group.cluster.id}"
}

modules/ecs-cluster/securitygroups.tf:

resource "aws_security_group" "cluster" {
  name        = "${var.CLUSTER_NAME}"
  vpc_id      = "${var.VPC_ID}"
  description = "${var.CLUSTER_NAME}"
}

resource "aws_security_group_rule" "cluster-allow-ssh" {
  count                    = "${ var.ENABLE_SSH ? 1 : 0}"
  security_group_id        = "${aws_security_group.cluster.id}"
  type                     = "ingress"
  from_port                = 22
  to_port                  = 22
  protocol                 = "tcp"
  source_security_group_id = "${var.SSH_SG}"
}

resource "aws_security_group_rule" "cluster-egress" {
  security_group_id = "${aws_security_group.cluster.id}"
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
}

modules/ecs-cluster/vars.tf:

variable "AWS_ACCOUNT_ID" {}
variable "AWS_REGION" {}
variable "LOG_GROUP" {}
variable "VPC_ID" {}
variable "CLUSTER_NAME" {}
variable "INSTANCE_TYPE" {}
variable "SSH_KEY_NAME" {}
variable "VPC_SUBNETS" {}

variable "ECS_TERMINATION_POLICIES" {
  default = "OldestLaunchConfiguration,Default"
}

variable "ECS_MINSIZE" {
  default = 1
}

variable "ECS_MAXSIZE" {
  default = 1
}

variable "ECS_DESIRED_CAPACITY" {
  default = 1
}

variable "ENABLE_SSH" {
  default = false
}

variable "SSH_SG" {
  default = ""
}

modules/ecs-service/alb.tf:

#
# target
#
resource "aws_alb_target_group" "ecs-service" {
  name                 = "${var.APPLICATION_NAME}-${substr(md5(format("%s%s%s", var.APPLICATION_PORT, var.DEREGISTRATION_DELAY, var.HEALTHCHECK_MATCHER)), 0, 12)}"
  port                 = "${var.APPLICATION_PORT}"
  protocol             = "HTTP"
  vpc_id               = "${var.VPC_ID}"
  deregistration_delay = "${var.DEREGISTRATION_DELAY}"

  health_check {
    healthy_threshold   = 3
    unhealthy_threshold = 3
    protocol            = "HTTP"
    path                = "/"
    interval            = 60
    matcher             = "${var.HEALTHCHECK_MATCHER}"
  }
}

modules/ecs-service/ecs-service.json:

[
  {
    "name": "${APPLICATION_NAME}",
    "image": "${ECR_URL}:${APPLICATION_VERSION}",
    "cpu": ${CPU_RESERVATION},
    "memoryReservation": ${MEMORY_RESERVATION},
    "essential": true,
    "mountPoints": [],
    "portMappings" : [
      {
        "containerPort": ${APPLICATION_PORT},
        "hostPort": 0
      }
    ],
    "logConfiguration": {
          "logDriver": "awslogs",
          "options": {
              "awslogs-group": "${LOG_GROUP}",
              "awslogs-region": "${AWS_REGION}",
              "awslogs-stream-prefix": "${APPLICATION_NAME}"
          }
    }
  }
]

modules/ecs-service/ecs-service.tf:

#
# ECR
#

resource "aws_ecr_repository" "ecs-service" {
  name = "${var.APPLICATION_NAME}"
}

#
# get latest active revision
#
data "aws_ecs_task_definition" "ecs-service" {
  task_definition = "${aws_ecs_task_definition.ecs-service-taskdef.family}"
  depends_on      = ["aws_ecs_task_definition.ecs-service-taskdef"]
}

#
# task definition template
#

data "template_file" "ecs-service" {
  template = "${file("${path.module}/ecs-service.json")}"

  vars {
    APPLICATION_NAME    = "${var.APPLICATION_NAME}"
    APPLICATION_PORT    = "${var.APPLICATION_PORT}"
    APPLICATION_VERSION = "${var.APPLICATION_VERSION}"
    ECR_URL             = "${aws_ecr_repository.ecs-service.repository_url}"
    AWS_REGION          = "${var.AWS_REGION}"
    CPU_RESERVATION     = "${var.CPU_RESERVATION}"
    MEMORY_RESERVATION  = "${var.MEMORY_RESERVATION}"
    LOG_GROUP           = "${var.LOG_GROUP}"
  }
}

#
# task definition
#

resource "aws_ecs_task_definition" "ecs-service-taskdef" {
  family                = "${var.APPLICATION_NAME}"
  container_definitions = "${data.template_file.ecs-service.rendered}"
  task_role_arn         = "${var.TASK_ROLE_ARN}"
}

#
# ecs service
#

resource "aws_ecs_service" "ecs-service" {
  name                               = "${var.APPLICATION_NAME}"
  cluster                            = "${var.CLUSTER_ARN}"
  task_definition                    = "${aws_ecs_task_definition.ecs-service-taskdef.family}:${max("${aws_ecs_task_definition.ecs-service-taskdef.revision}", "${data.aws_ecs_task_definition.ecs-service.revision}")}"
  iam_role                           = "${var.SERVICE_ROLE_ARN}"
  desired_count                      = "${var.DESIRED_COUNT}"
  deployment_minimum_healthy_percent = "${var.DEPLOYMENT_MINIMUM_HEALTHY_PERCENT}"
  deployment_maximum_percent         = "${var.DEPLOYMENT_MAXIMUM_PERCENT}"

  load_balancer {
    target_group_arn = "${aws_alb_target_group.ecs-service.id}"
    container_name   = "${var.APPLICATION_NAME}"
    container_port   = "${var.APPLICATION_PORT}"
  }

  depends_on = ["null_resource.alb_exists"]
}

resource "null_resource" "alb_exists" {
  triggers {
    alb_name = "${var.ALB_ARN}"
  }
}

modules/ecs-service/output.tf:

output "target_group_arn" {
  value = "${aws_alb_target_group.ecs-service.arn}"
}

modules/ecs-service/vars.tf:

variable "VPC_ID" {}
variable "AWS_REGION" {}
variable "APPLICATION_NAME" {}
variable "APPLICATION_PORT" {}
variable "APPLICATION_VERSION" {}
variable "CLUSTER_ARN" {}
variable "SERVICE_ROLE_ARN" {}
variable "DESIRED_COUNT" {}

variable "DEPLOYMENT_MINIMUM_HEALTHY_PERCENT" {
  default = 100
}

variable "DEPLOYMENT_MAXIMUM_PERCENT" {
  default = 200
}

variable "DEREGISTRATION_DELAY" {
  default = 30
}

variable "HEALTHCHECK_MATCHER" {
  default = "200"
}

variable "CPU_RESERVATION" {}
variable "MEMORY_RESERVATION" {}
variable "LOG_GROUP" {}

variable "TASK_ROLE_ARN" {
  default = ""
}

variable "ALB_ARN" {}

Last updated