<

Setting up a container cluster in aws with services using terraform

I recently setup an ecs cluster in AWS, which allowed me to run containers on the cluster, rather than having to run individual ec2 instances per application.

Create a Dockerfile, such as

FROM docker.elastic.co/logstash/logstash:6.2.4

RUN logstash-plugin install logstash-input-beats
RUN logstash-plugin install logstash-output-http
RUN logstash-plugin install logstash-output-elasticsearch

RUN rm -f /usr/share/logstash/pipeline/logstash.conf
ADD pipeline/ /usr/share/logstash/pipeline/
ADD config/ /usr/share/logstash/config/

RUN cd ~ && curl -O http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz \
	&& tar -zxvf GeoLite2-City.tar.gz \
	&& cd GeoLite2-City_20180703 \	
	&& mv GeoLite2-City.mmdb /usr/share/logstash/bin/GeoLite2-City.mmdb	

Then you need to build your image, and push to your aws elastic container registry

#build.sh 
#!/bin/bash
set -e

BASE_REPO=XXXXXXXXXXXX.dkr.ecr.eu-west-2.amazonaws.com
IMAGE_NAME=my-image-name
VERSION=1.0.0

eval $(aws ecr get-login --region eu-west-2 --no-include-email)
sleep 1

docker build -t $IMAGE_NAME:$VERSION .
docker tag $IMAGE_NAME:$VERSION $BASE_REPO/$IMAGE_NAME:$VERSION
docker push $BASE_REPO/$IMAGE_NAME:$VERSION

Using terraform, you need to setup your elasticsearch cluster, and some ec2 to act as the container host

  • variables
variable "aws_region" {
  default = "eu-west-2"
}

data "aws_vpc" "vpc" {
  filter {
    name   = "tag:Name"
    values = ["vpc.eu-west-2"]
  }
}

data "aws_subnet" "private" {
  count = "${length(data.aws_subnet_ids.private.ids)}"
  id    = "${data.aws_subnet_ids.private.ids[count.index]}"
}

data "template_file" "user_data" {
  template = "${file("${path.module}/user_data.tpl")}"

  vars {
    cluster_name = "${aws_ecs_cluster.container-cluster.name}"
  }
}

And create a user data file, to make sure the container host can register with ECS

  • user_data.tpl
    #!/bin/bash
    echo ECS_CLUSTER=${cluster_name} >> /etc/ecs/ecs.config 
    echo ECS_BACKEND_HOST= >> /etc/ecs/ecs.config
    echo 'vm.max_map_count = 262144' >> /etc/sysctl.conf
    sysctl -p
    
#ECS
resource "aws_ecs_cluster" "my-container-cluster" {
  name = "my-container-cluster"
}

resource "aws_instance" "my-container-host" {
  ami           = "ami-3622cf51" #ecs optimized image
  instance_type = "t2.medium"

  vpc_security_group_ids = ["${aws_security_group.my-container-host.id}"]
  subnet_id              = "${data.aws_subnet_ids.private.ids[0]}"
  user_data              = "${data.template_file.user_data.rendered}"
  key_name               = "${terraform.workspace}-ec2-applications"
  iam_instance_profile   = "${aws_iam_instance_profile.my-ecs-instance-profile.name}"

  tags {
    Name = "my-container-host"
  }
}

resource "aws_security_group" "my-container-host" {
  name        = "allow_all"
  description = "Allow all inbound traffic"
  vpc_id      = "${data.aws_vpc.vpc.id}"

  ingress {
    from_port   = 0
    to_port     = 65535
    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"]
  }
}

Then start looking at creating your service to run on the container cluster

  • ecs cluster
data "aws_ecs_task_definition" "logstash" {
  task_definition = "${aws_ecs_task_definition.logstash.family}"
}

resource "aws_ecs_task_definition" "logstash" {
  family = "logstash"

  container_definitions = <<DEFINITION
[
  {
    "name": "logstash",
    "image": "XXXXXXXXXXXX.dkr.ecr.eu-west-2.amazonaws.com/craig/imagename",
    "essential": true,
    "requiresAttributes": [
        {
        "value": null,
        "name": "com.amazonaws.ecs.capability.ecr-auth",
        "targetId": null,
        "targetType": null
        },
        {
        "value": null,
        "name": "com.amazonaws.ecs.capability.task-iam-role",
        "targetId": null,
        "targetType": null
        },
        {
        "value": null,
        "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19",
        "targetId": null,
        "targetType": null
        }
    ],
    "portMappings": [
       {
         "hostPort": 5044,
         "protocol": "tcp",
         "containerPort": 5044
       },
       {
         "hostPort": 9600,
         "protocol": "tcp",
         "containerPort": 9600
       }
    ],
    "memory": 256,
    "cpu": 10
  }
]
DEFINITION
}

resource "aws_ecs_service" "logstash" {
  name                = "logstash"
  cluster             = "${aws_ecs_cluster.my-container-cluster.id}"
  task_definition     = "${aws_ecs_task_definition.logstash.family}:${max("${aws_ecs_task_definition.logstash.revision}", "${data.aws_ecs_task_definition.logstash.revision}")}"
  desired_count       = 1
  scheduling_strategy = "DAEMON"
}

Then setup you roles and policy

  • iam
resource "aws_iam_role" "acquisition-ecs-service-role" {
  name               = "acquisition-ecs-service-role"
  path               = "/"
  assume_role_policy = "${data.aws_iam_policy_document.ecs-service-policy.json}"
}

resource "aws_iam_role_policy_attachment" "acquisition-ecs-service-role-attachment" {
  role       = "${aws_iam_role.acquisition-ecs-service-role.name}"
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole"
}

data "aws_iam_policy_document" "ecs-service-policy" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ecs.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "ecs-instance-role" {
  name               = "ecs-instance-role"
  path               = "/"
  assume_role_policy = "${data.aws_iam_policy_document.ecs-instance-policy.json}"
}

data "aws_iam_policy_document" "ecs-instance-policy" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

resource "aws_iam_role_policy_attachment" "ecs-instance-role-attachment" {
  role       = "${aws_iam_role.ecs-instance-role.name}"
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
}

resource "aws_iam_instance_profile" "ecs-instance-profile" {
  name = "ecs-instance-profile"
  path = "/"
  role = "${aws_iam_role.ecs-instance-role.id}"
}

Once you apply the config, and the ec2 container host becomes ready, you will see that the container gets started

Written on July 5, 2018.