provider/aws: Add support for ECS svc - LB target group (#8190)

This commit is contained in:
Radek Simko 2016-08-23 17:19:43 +01:00 committed by Clint
parent c30398ed90
commit 070942df0f
11 changed files with 657 additions and 6 deletions

View File

@ -76,7 +76,13 @@ func resourceAwsEcsService() *schema.Resource {
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"elb_name": &schema.Schema{ "elb_name": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Optional: true,
ForceNew: true,
},
"target_group_arn": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true, ForceNew: true,
}, },

View File

@ -241,6 +241,22 @@ func TestAccAWSEcsService_withEcsClusterName(t *testing.T) {
}) })
} }
func TestAccAWSEcsService_withAlb(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSEcsServiceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSEcsServiceWithAlb,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEcsServiceExists("aws_ecs_service.with_alb"),
),
},
},
})
}
func testAccCheckAWSEcsServiceDestroy(s *terraform.State) error { func testAccCheckAWSEcsServiceDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ecsconn conn := testAccProvider.Meta().(*AWSClient).ecsconn
@ -702,3 +718,128 @@ resource "aws_ecs_service" "jenkins" {
desired_count = 1 desired_count = 1
} }
` `
var testAccAWSEcsServiceWithAlb = `
data "aws_availability_zones" "available" {}
resource "aws_vpc" "main" {
cidr_block = "10.10.0.0/16"
}
resource "aws_subnet" "main" {
count = 2
cidr_block = "${cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)}"
availability_zone = "${data.aws_availability_zones.available.names[count.index]}"
vpc_id = "${aws_vpc.main.id}"
}
resource "aws_ecs_cluster" "main" {
name = "terraform_acc_test_ecs_15"
}
resource "aws_ecs_task_definition" "with_lb_changes" {
family = "tf_acc_test_ghost_lbd"
container_definitions = <<DEFINITION
[
{
"cpu": 256,
"essential": true,
"image": "ghost:latest",
"memory": 512,
"name": "ghost",
"portMappings": [
{
"containerPort": 2368,
"hostPort": 8080
}
]
}
]
DEFINITION
}
resource "aws_iam_role" "ecs_service" {
name = "tf_acc_test_15_role"
assume_role_policy = <<EOF
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "ecs.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_iam_role_policy" "ecs_service" {
name = "tf_acc_test_15_policy"
role = "${aws_iam_role.ecs_service.name}"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:Describe*",
"elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
"elasticloadbalancing:DeregisterTargets",
"elasticloadbalancing:Describe*",
"elasticloadbalancing:RegisterInstancesWithLoadBalancer",
"elasticloadbalancing:RegisterTargets"
],
"Resource": "*"
}
]
}
EOF
}
resource "aws_alb_target_group" "test" {
name = "tf-acc-test-ecs-ghost"
port = 80
protocol = "HTTP"
vpc_id = "${aws_vpc.main.id}"
}
resource "aws_alb" "main" {
name = "tf-acc-test-test-alb-ecs"
subnets = ["${aws_subnet.main.*.id}"]
}
resource "aws_alb_listener" "front_end" {
load_balancer_arn = "${aws_alb.main.id}"
port = "80"
protocol = "HTTP"
default_action {
target_group_arn = "${aws_alb_target_group.test.id}"
type = "forward"
}
}
resource "aws_ecs_service" "with_alb" {
name = "tf-acc-test-ecs-ghost"
cluster = "${aws_ecs_cluster.main.id}"
task_definition = "${aws_ecs_task_definition.with_lb_changes.arn}"
desired_count = 1
iam_role = "${aws_iam_role.ecs_service.name}"
load_balancer {
target_group_arn = "${aws_alb_target_group.test.id}"
container_name = "ghost"
container_port = "2368"
}
depends_on = [
"aws_iam_role_policy.ecs_service",
"aws_alb_listener.front_end"
]
}
`

View File

@ -125,9 +125,15 @@ func expandEcsLoadBalancers(configured []interface{}) []*ecs.LoadBalancer {
data := lRaw.(map[string]interface{}) data := lRaw.(map[string]interface{})
l := &ecs.LoadBalancer{ l := &ecs.LoadBalancer{
ContainerName: aws.String(data["container_name"].(string)), ContainerName: aws.String(data["container_name"].(string)),
ContainerPort: aws.Int64(int64(data["container_port"].(int))), ContainerPort: aws.Int64(int64(data["container_port"].(int))),
LoadBalancerName: aws.String(data["elb_name"].(string)), }
if v, ok := data["elb_name"]; ok && v.(string) != "" {
l.LoadBalancerName = aws.String(v.(string))
}
if v, ok := data["target_group_arn"]; ok && v.(string) != "" {
l.TargetGroupArn = aws.String(v.(string))
} }
loadBalancers = append(loadBalancers, l) loadBalancers = append(loadBalancers, l)
@ -553,10 +559,18 @@ func flattenEcsLoadBalancers(list []*ecs.LoadBalancer) []map[string]interface{}
result := make([]map[string]interface{}, 0, len(list)) result := make([]map[string]interface{}, 0, len(list))
for _, loadBalancer := range list { for _, loadBalancer := range list {
l := map[string]interface{}{ l := map[string]interface{}{
"elb_name": *loadBalancer.LoadBalancerName,
"container_name": *loadBalancer.ContainerName, "container_name": *loadBalancer.ContainerName,
"container_port": *loadBalancer.ContainerPort, "container_port": *loadBalancer.ContainerPort,
} }
if loadBalancer.LoadBalancerName != nil {
l["elb_name"] = *loadBalancer.LoadBalancerName
}
if loadBalancer.TargetGroupArn != nil {
l["target_group_arn"] = *loadBalancer.TargetGroupArn
}
result = append(result, l) result = append(result, l)
} }
return result return result

View File

@ -0,0 +1,33 @@
# ECS with ALB example
This example shows how to launch an ECS service fronted with Application Load Balancer.
The example uses latest CoreOS Stable AMIs.
To run, configure your AWS provider as described in https://www.terraform.io/docs/providers/aws/index.html
## Get up and running
Planning phase
```
terraform plan \
-var admin_cidr_ingress='"{your_ip_address}/32"' \
-var key_name={your_key_name}
```
Apply phase
```
terraform apply \
-var admin_cidr_ingress='"{your_ip_address}/32"' \
-var key_name={your_key_name}
```
Once the stack is created, wait for a few minutes and test the stack by launching a browser with the ALB url.
## Destroy :boom:
```
terraform destroy
```

View File

@ -0,0 +1,43 @@
#cloud-config
coreos:
units:
- name: update-engine.service
command: stop
- name: amazon-ecs-agent.service
command: start
runtime: true
content: |
[Unit]
Description=AWS ECS Agent
Documentation=https://docs.aws.amazon.com/AmazonECS/latest/developerguide/
Requires=docker.socket
After=docker.socket
[Service]
Environment=ECS_CLUSTER=${ecs_cluster_name}
Environment=ECS_LOGLEVEL=${ecs_log_level}
Environment=ECS_VERSION=${ecs_agent_version}
Restart=on-failure
RestartSec=30
RestartPreventExitStatus=5
SyslogIdentifier=ecs-agent
ExecStartPre=-/bin/mkdir -p /var/log/ecs /var/ecs-data /etc/ecs
ExecStartPre=-/usr/bin/docker kill ecs-agent
ExecStartPre=-/usr/bin/docker rm ecs-agent
ExecStartPre=/usr/bin/docker pull amazon/amazon-ecs-agent:$${ECS_VERSION}
ExecStart=/usr/bin/docker run --name ecs-agent \
--volume=/var/run/docker.sock:/var/run/docker.sock \
--volume=/var/log/ecs:/log \
--volume=/var/ecs-data:/data \
--volume=/sys/fs/cgroup:/sys/fs/cgroup:ro \
--volume=/run/docker/execdriver/native:/var/lib/docker/execdriver/native:ro \
--publish=127.0.0.1:51678:51678 \
--env=ECS_LOGFILE=/log/ecs-agent.log \
--env=ECS_LOGLEVEL=$${ECS_LOGLEVEL} \
--env=ECS_DATADIR=/data \
--env=ECS_CLUSTER=$${ECS_CLUSTER} \
--env=ECS_AVAILABLE_LOGGING_DRIVERS='["awslogs"]' \
--log-driver=awslogs \
--log-opt awslogs-region=${aws_region} \
--log-opt awslogs-group=${ecs_log_group_name} \
amazon/amazon-ecs-agent:$${ECS_VERSION}

View File

@ -0,0 +1,31 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ecsInstanceRole",
"Effect": "Allow",
"Action": [
"ecs:DeregisterContainerInstance",
"ecs:DiscoverPollEndpoint",
"ecs:Poll",
"ecs:RegisterContainerInstance",
"ecs:Submit*"
],
"Resource": [
"*"
]
},
{
"Sid": "allowLoggingToCloudWatch",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"${app_log_group_arn}",
"${ecs_log_group_arn}"
]
}
]
}

View File

@ -0,0 +1,308 @@
# Specify the provider and access details
provider "aws" {
region = "${var.aws_region}"
}
## EC2
### Network
data "aws_availability_zones" "available" {}
resource "aws_vpc" "main" {
cidr_block = "10.10.0.0/16"
}
resource "aws_subnet" "main" {
count = "${var.az_count}"
cidr_block = "${cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)}"
availability_zone = "${data.aws_availability_zones.available.names[count.index]}"
vpc_id = "${aws_vpc.main.id}"
}
resource "aws_internet_gateway" "gw" {
vpc_id = "${aws_vpc.main.id}"
}
resource "aws_route_table" "r" {
vpc_id = "${aws_vpc.main.id}"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_internet_gateway.gw.id}"
}
}
resource "aws_route_table_association" "a" {
count = "${var.az_count}"
subnet_id = "${element(aws_subnet.main.*.id, count.index)}"
route_table_id = "${aws_route_table.r.id}"
}
### Compute
resource "aws_autoscaling_group" "app" {
name = "tf-test-asg"
vpc_zone_identifier = ["${aws_subnet.main.*.id}"]
min_size = "${var.asg_min}"
max_size = "${var.asg_max}"
desired_capacity = "${var.asg_desired}"
launch_configuration = "${aws_launch_configuration.app.name}"
}
data "template_file" "cloud_config" {
template = "${file("${path.module}/cloud-config.yml")}"
vars {
aws_region = "${var.aws_region}"
ecs_cluster_name = "${aws_ecs_cluster.main.name}"
ecs_log_level = "info"
ecs_agent_version = "latest"
ecs_log_group_name = "${aws_cloudwatch_log_group.ecs.name}"
}
}
data "aws_ami" "stable_coreos" {
most_recent = true
filter {
name = "description"
values = ["CoreOS stable *"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["595879546273"] # CoreOS
}
resource "aws_launch_configuration" "app" {
security_groups = [
"${aws_security_group.instance_sg.id}"
]
key_name = "${var.key_name}"
image_id = "${data.aws_ami.stable_coreos.id}"
instance_type = "${var.instance_type}"
iam_instance_profile = "${aws_iam_instance_profile.app.name}"
user_data = "${data.template_file.cloud_config.rendered}"
associate_public_ip_address = true
lifecycle {
create_before_destroy = true
}
}
### Security
resource "aws_security_group" "lb_sg" {
description = "controls access to the application ELB"
vpc_id = "${aws_vpc.main.id}"
name = "tf-ecs-lbsg"
ingress {
protocol = "tcp"
from_port = 80
to_port = 80
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" "instance_sg" {
description = "controls direct access to application instances"
vpc_id = "${aws_vpc.main.id}"
name = "tf-ecs-instsg"
ingress {
protocol = "tcp"
from_port = 22
to_port = 22
cidr_blocks = [
"${var.admin_cidr_ingress}"
]
}
ingress {
protocol = "tcp"
from_port = 8080
to_port = 8080
security_groups = [
"${aws_security_group.lb_sg.id}"
]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
## ECS
resource "aws_ecs_cluster" "main" {
name = "terraform_example_ecs_cluster"
}
data "template_file" "task_definition" {
template = "${file("${path.module}/task-definition.json")}"
vars {
image_url = "ghost:latest"
container_name = "ghost"
log_group_region = "${var.aws_region}"
log_group_name = "${aws_cloudwatch_log_group.app.name}"
}
}
resource "aws_ecs_task_definition" "ghost" {
family = "tf_example_ghost_td"
container_definitions = "${data.template_file.task_definition.rendered}"
}
resource "aws_ecs_service" "test" {
name = "tf-example-ecs-ghost"
cluster = "${aws_ecs_cluster.main.id}"
task_definition = "${aws_ecs_task_definition.ghost.arn}"
desired_count = 1
iam_role = "${aws_iam_role.ecs_service.name}"
load_balancer {
target_group_arn = "${aws_alb_target_group.test.id}"
container_name = "ghost"
container_port = "2368"
}
depends_on = [
"aws_iam_role_policy.ecs_service",
"aws_alb_listener.front_end"
]
}
## IAM
resource "aws_iam_role" "ecs_service" {
name = "tf_example_ecs_role"
assume_role_policy = <<EOF
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "ecs.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_iam_role_policy" "ecs_service" {
name = "tf_example_ecs_policy"
role = "${aws_iam_role.ecs_service.name}"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:Describe*",
"elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
"elasticloadbalancing:DeregisterTargets",
"elasticloadbalancing:Describe*",
"elasticloadbalancing:RegisterInstancesWithLoadBalancer",
"elasticloadbalancing:RegisterTargets"
],
"Resource": "*"
}
]
}
EOF
}
resource "aws_iam_instance_profile" "app" {
name = "tf-ecs-instprofile"
roles = ["${aws_iam_role.app_instance.name}"]
}
resource "aws_iam_role" "app_instance" {
name = "tf-ecs-example-instance-role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
data "template_file" "instance_profile" {
template = "${file("${path.module}/instance-profile-policy.json")}"
vars {
app_log_group_arn = "${aws_cloudwatch_log_group.app.arn}"
ecs_log_group_arn = "${aws_cloudwatch_log_group.ecs.arn}"
}
}
resource "aws_iam_role_policy" "instance" {
name = "TfEcsExampleInstanceRole"
role = "${aws_iam_role.app_instance.name}"
policy = "${data.template_file.instance_profile.rendered}"
}
## ALB
resource "aws_alb_target_group" "test" {
name = "tf-example-ecs-ghost"
port = 80
protocol = "HTTP"
vpc_id = "${aws_vpc.main.id}"
}
resource "aws_alb" "main" {
name = "tf-example-alb-ecs"
subnets = ["${aws_subnet.main.*.id}"]
security_groups = ["${aws_security_group.lb_sg.id}"]
}
resource "aws_alb_listener" "front_end" {
load_balancer_arn = "${aws_alb.main.id}"
port = "80"
protocol = "HTTP"
default_action {
target_group_arn = "${aws_alb_target_group.test.id}"
type = "forward"
}
}
## CloudWatch Logs
resource "aws_cloudwatch_log_group" "ecs" {
name = "tf-ecs-group/ecs-agent"
}
resource "aws_cloudwatch_log_group" "app" {
name = "tf-ecs-group/app-ghost"
}

View File

@ -0,0 +1,15 @@
output "instance_security_group" {
value = "${aws_security_group.instance_sg.id}"
}
output "launch_configuration" {
value = "${aws_launch_configuration.app.id}"
}
output "asg_name" {
value = "${aws_autoscaling_group.app.id}"
}
output "elb_hostname" {
value = "${aws_alb.main.dns_name}"
}

View File

@ -0,0 +1,22 @@
[
{
"cpu": 256,
"essential": true,
"image": "${image_url}",
"memory": 512,
"name": "${container_name}",
"portMappings": [
{
"containerPort": 2368,
"hostPort": 8080
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "${log_group_name}",
"awslogs-region": "${log_group_region}"
}
}
}
]

View File

@ -0,0 +1,37 @@
variable "aws_region" {
description = "The AWS region to create things in."
default = "us-west-2"
}
variable "az_count" {
description = "Number of AZs to cover in a given AWS region"
default = "2"
}
variable "key_name" {
description = "Name of AWS key pair"
}
variable "instance_type" {
default = "t2.small"
description = "AWS instance type"
}
variable "asg_min" {
description = "Min numbers of servers in ASG"
default = "1"
}
variable "asg_max" {
description = "Max numbers of servers in ASG"
default = "2"
}
variable "asg_desired" {
description = "Desired numbers of servers in ASG"
default = "1"
}
variable "admin_cidr_ingress" {
description = "CIDR to allow tcp/22 ingress to EC2 instance"
}

View File

@ -50,7 +50,8 @@ The following arguments are supported:
Load balancers support the following: Load balancers support the following:
* `elb_name` - (Required) The name of the load balancer. * `elb_name` - (Required for ELB Classic) The name of the ELB (Classic) to associate with the service.
* `target_group_arn` - (Required for ALB) The ARN of the ALB target group to associate with the service.
* `container_name` - (Required) The name of the container to associate with the load balancer (as it appears in a container definition). * `container_name` - (Required) The name of the container to associate with the load balancer (as it appears in a container definition).
* `container_port` - (Required) The port on the container to associate with the load balancer. * `container_port` - (Required) The port on the container to associate with the load balancer.