diff --git a/.circleci/config.yml b/.circleci/config.yml index 0bd80c3..7d01002 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,13 +3,13 @@ version: 2 jobs: build: docker: - - image: hashicorp/terraform:0.9.11 + - image: hashicorp/terraform:0.10.1 entrypoint: /bin/sh steps: - checkout - run: name: validate tf files (terraform validate) - command: find . -type f -name "*.tf" -exec dirname {} \;|sort -u | while read m; do (terraform validate "$m" && echo "√ $m") || exit 1 ; done + command: find . -type f -name "*.tf" -exec dirname {} \;|sort -u | while read m; do (terraform validate -check-variables=false "$m" && echo "√ $m") || exit 1 ; done - run: name: check if all tf files are formatted (terraform fmt) command: if [ `terraform fmt | wc -c` -ne 0 ]; then echo "Some terraform files need be formatted, run 'terraform fmt' to fix"; exit 1; fi diff --git a/.terraform-version b/.terraform-version new file mode 100644 index 0000000..5712157 --- /dev/null +++ b/.terraform-version @@ -0,0 +1 @@ +0.10.1 diff --git a/README.md b/README.md index a447985..a32f1b3 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,11 @@ ecs terraform module A terraform module to provide ECS clusters in AWS. +[![CircleCI](https://circleci.com/gh/terraform-community-modules/tf_aws_ecs.svg?style=svg)](https://circleci.com/gh/terraform-community-modules/tf_aws_ecs) + + +This Module currently supports Terraform 0.10.x, but does not require it. If you use tfenv, this module contains a `.terraform-version` file which matches the version of Terraform we currently use to test with. + Module Input Variables ---------------------- @@ -13,6 +18,9 @@ Module Input Variables - `vpc_id` - The VPC ID to place the cluster in #### Optional +note about `user_data` and - `additional_user_data_script`: The `user_data` parameter overwrites the user_data template used by this module, this will break some of the module features. (`docker_storage_size`, `dockerhub_token`, and `dockerhub_email`) `additional_user_data_script` however will concatenate additional data to the end of the current user_data script. It is recomended that you use `additional_user_data_script`. These two parameters are mutually exclusive - you can not pass both into this module and expect it to work. + +- `additional_user_data_script` - Additional user_data scripts content - `region` - AWS Region - defaults to us-east-1 - `servers` - Number of ECS Servers to start in the cluster - defaults to 2 - `instance_type` - AWS instance type - defaults to t2.micro @@ -37,6 +45,10 @@ extra_tags = [ - `security_group_ids` - a list of security group IDs to apply to the launch configuration - `user_data` - The instance user data (e.g. a `cloud-init` config) to use in the `aws_launch_configuration` +- `consul_image` - Image to use when deploying consul, defaults to the hashicorp consul image +- `registrator_image` - Image to use when deploying registrator agent, defaults to the gliderlabs registrator:latest +- `enable_agents` - Enable Consul Agent and Registrator tasks on each ECS Instance. Defaults to false + Usage ----- @@ -47,11 +59,57 @@ module "ecs-cluster" { servers = 1 subnet_id = ["subnet-6e101446"] vpc_id = "vpc-99e73dfc" - key_name = "key.pem" } ``` +#### Example cluster with consul and Registrator + +In order to start the Consul/Registrator task in ECS, you'll need to pass in a consul config into the `additional_user_data_script` script parameter. For example, you might pass something like this: + +Please note, this module will try to mount `/etc/consul/` into `/consul/config` in the container and assumes that the consul config lives under `/etc/consul` on the docker host. + +```Shell +/bin/mkdir -p /etc/consul +cat <<"CONSUL" > /etc/consul/config.json +{ + "raft_protocol": 3, + "log_level": "INFO", + "enable_script_checks": true, + "datacenter": "${datacenter}", + "retry_join_ec2": { + "tag_key": "consul_server", + "tag_value": "true" + } +} +CONSUL +``` + + +```hcl + +data "template_file" "ecs_consul_agent_json" { + template = "${file("ecs_consul_agent.json.sh")}" + + vars { + datacenter = "infra-services" + } +} + +module "ecs-cluster" { + source = "github.com/terraform-community-modules/tf_aws_ecs" + name = "infra-services" + servers = 1 + subnet_id = ["subnet-6e101446"] + vpc_id = "vpc-99e73dfc" + additional_user_data_script = "${data.template_file.ecs_consul_agent_json.rendered}" + enable_agents = true +} + + +``` + + Outputs ======= diff --git a/consul_agent.tf b/consul_agent.tf new file mode 100644 index 0000000..aeeec98 --- /dev/null +++ b/consul_agent.tf @@ -0,0 +1,54 @@ +data "template_file" "consul" { + template = "${file("${path.module}/templates/consul.json")}" + + vars { + env = "${aws_ecs_cluster.cluster.name}" + image = "${var.consul_image}" + registrator_image = "${var.registrator_image}" + awslogs_group = "consul-agent-${aws_ecs_cluster.cluster.name}" + awslogs_stream_prefix = "consul-agent-${aws_ecs_cluster.cluster.name}" + awslogs_region = "${var.region}" + } +} + +# End Data block + +resource "aws_ecs_task_definition" "consul" { + count = "${var.enable_agents ? 1 : 0}" + family = "consul-agent-${aws_ecs_cluster.cluster.name}" + container_definitions = "${data.template_file.consul.rendered}" + network_mode = "host" + task_role_arn = "${aws_iam_role.consul_task.arn}" + + volume { + name = "consul-config-dir" + host_path = "/etc/consul" + } + + volume { + name = "docker-sock" + host_path = "/var/run/docker.sock" + } +} + +resource "aws_cloudwatch_log_group" "consul" { + count = "${var.enable_agents ? 1 : 0}" + name = "${aws_ecs_task_definition.consul.family}" + + tags { + VPC = "${data.aws_vpc.vpc.tags["Name"]}" + Application = "${aws_ecs_task_definition.consul.family}" + } +} + +resource "aws_ecs_service" "consul" { + count = "${var.enable_agents ? 1 : 0}" + name = "consul-agent-${aws_ecs_cluster.cluster.name}" + cluster = "${aws_ecs_cluster.cluster.id}" + task_definition = "${aws_ecs_task_definition.consul.arn}" + desired_count = "${var.servers}" + + placement_constraints { + type = "distinctInstance" + } +} diff --git a/iam.tf b/iam.tf index 482e5ba..e29c790 100644 --- a/iam.tf +++ b/iam.tf @@ -66,3 +66,41 @@ resource "aws_iam_policy_attachment" "attach-ecs" { roles = ["${aws_iam_role.ecs-role.name}"] policy_arn = "${aws_iam_policy.ecs-policy.arn}" } + +# IAM Resources for Consul and Registrator Agents + +data "aws_iam_policy_document" "consul_task_policy" { + statement { + actions = [ + "ec2:Describe*", + "autoscaling:Describe*", + ] + + resources = ["*"] + } +} + +data "aws_iam_policy_document" "assume_role_consul_task" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "consul_task" { + count = "${var.enable_agents ? 1 : 0}" + name = "${replace(format("%.64s", replace("tf-consul-agentTaskRole-${var.name}-${data.aws_vpc.vpc.tags["Name"]}", "_", "-")), "/\\s/", "-")}" + path = "/" + assume_role_policy = "${data.aws_iam_policy_document.assume_role_consul_task.json}" +} + +resource "aws_iam_role_policy" "consul_ecs_task" { + count = "${var.enable_agents ? 1 : 0}" + name = "${replace(format("%.64s", replace("tf-consul-agentTaskPolicy-${var.name}-${data.aws_vpc.vpc.tags["Name"]}", "_", "-")), "/\\s/", "-")}" + role = "${aws_iam_role.consul_task.id}" + policy = "${data.aws_iam_policy_document.consul_task_policy.json}" +} diff --git a/main.tf b/main.tf index 4db0e27..419e5b1 100644 --- a/main.tf +++ b/main.tf @@ -16,13 +16,18 @@ data "template_file" "user_data" { template = "${file("${path.module}/templates/user_data.tpl")}" vars { - cluster_name = "${aws_ecs_cluster.cluster.name}" - docker_storage_size = "${var.docker_storage_size}" - dockerhub_token = "${var.dockerhub_token}" - dockerhub_email = "${var.dockerhub_email}" + additional_user_data_script = "${var.additional_user_data_script}" + cluster_name = "${aws_ecs_cluster.cluster.name}" + docker_storage_size = "${var.docker_storage_size}" + dockerhub_token = "${var.dockerhub_token}" + dockerhub_email = "${var.dockerhub_email}" } } +data "aws_vpc" "vpc" { + id = "${var.vpc_id}" +} + resource "aws_launch_configuration" "ecs" { name_prefix = "${coalesce(var.name_prefix, "ecs-${var.name}-")}" image_id = "${var.ami == "" ? format("%s", data.aws_ami.ecs_ami.id) : var.ami}" # Workaround until 0.9.6 @@ -71,7 +76,7 @@ resource "aws_autoscaling_group" "ecs" { resource "aws_security_group" "ecs" { name = "ecs-sg-${var.name}" description = "Container Instance Allowed Ports" - vpc_id = "${var.vpc_id}" + vpc_id = "${data.aws_vpc.vpc.id}" ingress { from_port = 0 diff --git a/templates/consul.json b/templates/consul.json new file mode 100644 index 0000000..b28cfaf --- /dev/null +++ b/templates/consul.json @@ -0,0 +1,52 @@ +[ + { + "name": "consul_agent-${env}", + "image": "${image}", + "memoryReservation": 512, + "environment": [ + { + "name": "CONSUL_BIND_INTERFACE", + "value": "eth0" + } + ], + "command": [ + "agent", "-client=0.0.0.0" + ], + "mountPoints": [ + { + "sourceVolume": "consul-config-dir", + "containerPath": "/consul/config" + } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "${awslogs_group}", + "awslogs-region": "${awslogs_region}", + "awslogs-stream-prefix": "${awslogs_stream_prefix}" + } + } + }, + { + "name": "registrator-${env}", + "image": "${registrator_image}", + "memoryReservation": 256, + "command": [ + "-retry-attempts=10", "-retry-interval=1000", "consul://localhost:8500" + ], + "mountPoints": [ + { + "sourceVolume": "docker-sock", + "containerPath": "/tmp/docker.sock" + } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "${awslogs_group}", + "awslogs-region": "${awslogs_region}", + "awslogs-stream-prefix": "${awslogs_stream_prefix}" + } + } + } +] diff --git a/templates/ecs_consul_agent.json.sh-example b/templates/ecs_consul_agent.json.sh-example new file mode 100644 index 0000000..80916bf --- /dev/null +++ b/templates/ecs_consul_agent.json.sh-example @@ -0,0 +1,13 @@ +/bin/mkdir -p /etc/consul +cat <<"CONSUL" > /etc/consul/config.json +{ + "raft_protocol": 3, + "log_level": "INFO", + "enable_script_checks": true, + "datacenter": "${datacenter}", + "retry_join_ec2": { + "tag_key": "consul_server", + "tag_value": "true" + } +} +CONSUL diff --git a/templates/user_data.tpl b/templates/user_data.tpl index e8e01b6..c20b72e 100644 --- a/templates/user_data.tpl +++ b/templates/user_data.tpl @@ -4,3 +4,6 @@ echo 'OPTIONS="$${OPTIONS} --storage-opt dm.basesize=${docker_storage_size}G"' > /etc/init.d/docker restart echo ECS_ENGINE_AUTH_TYPE=dockercfg >> /etc/ecs/ecs.config echo 'ECS_ENGINE_AUTH_DATA={"https://index.docker.io/v1/": { "auth": "${dockerhub_token}", "email": "${dockerhub_email}"}}' >> /etc/ecs/ecs.config + +# Append addition user-data script +${additional_user_data_script} diff --git a/variables.tf b/variables.tf index 13ba84d..334414f 100644 --- a/variables.tf +++ b/variables.tf @@ -1,13 +1,13 @@ +variable "additional_user_data_script" { + default = "" +} + variable "allowed_cidr_blocks" { default = ["0.0.0.0/0"] type = "list" description = "List of subnets to allow into the ECS Security Group. Defaults to ['0.0.0.0/0']" } -variable "name_prefix" { - default = "" -} - variable "ami" { default = "" } @@ -16,8 +16,13 @@ variable "ami_version" { default = "*" } -variable "user_data" { - default = "" +variable "associate_public_ip_address" { + default = false +} + +variable "consul_image" { + description = "Image to use when deploying consul, defaults to the hashicorp consul image" + default = "consul:latest" } variable "docker_storage_size" { @@ -35,6 +40,11 @@ variable "dockerhub_token" { description = "Auth Token used for dockerhub. http://docs.aws.amazon.com/AmazonECS/latest/developerguide/private-auth.html" } +variable "enable_agents" { + default = false + description = "Enable Consul Agent and Registrator tasks on each ECS Instance" +} + variable "extra_tags" { default = [] } @@ -57,11 +67,20 @@ variable "name" { description = "AWS ECS Cluster Name" } +variable "name_prefix" { + default = "" +} + variable "region" { default = "us-east-1" description = "The region of AWS, for AMI lookups." } +variable "registrator_image" { + default = "gliderlabs/registrator:latest" + description = "Image to use when deploying registrator agent, defaults to the gliderlabs registrator:latest image" +} + variable "security_group_ids" { type = "list" description = "A list of Security group IDs to apply to the launch configuration" @@ -83,10 +102,10 @@ variable "tagName" { description = "Name tag for the servers" } -variable "vpc_id" { - description = "The AWS VPC ID which you want to deploy your instances" +variable "user_data" { + default = "" } -variable "associate_public_ip_address" { - default = false +variable "vpc_id" { + description = "The AWS VPC ID which you want to deploy your instances" }