From cff50323bb0d1a40a3a71d0ac3fb1cf1f571517f Mon Sep 17 00:00:00 2001 From: Lucas Szwarcberg Date: Mon, 25 Jun 2018 16:46:50 -0400 Subject: [PATCH 1/4] Initial commit --- .gitignore | 3 + main.tf | 21 ++++++ modules/networking/main.tf | 79 ++++++++++++++++++++ modules/networking/outputs.tf | 11 +++ modules/networking/variables.tf | 3 + modules/worker/main.tf | 83 +++++++++++++++++++++ modules/worker/tasks/worker_definition.json | 78 +++++++++++++++++++ modules/worker/variables.tf | 7 ++ terraform.tfvars | 9 +++ variables.tf | 5 ++ 10 files changed, 299 insertions(+) create mode 100644 .gitignore create mode 100644 main.tf create mode 100644 modules/networking/main.tf create mode 100644 modules/networking/outputs.tf create mode 100644 modules/networking/variables.tf create mode 100644 modules/worker/main.tf create mode 100644 modules/worker/tasks/worker_definition.json create mode 100644 modules/worker/variables.tf create mode 100644 terraform.tfvars create mode 100644 variables.tf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed73c82 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.swp +.terraform +*.tfstate* diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..4ab31a4 --- /dev/null +++ b/main.tf @@ -0,0 +1,21 @@ +provider "aws" { + region = "${var.region}" +} + +module "networking" { + source = "./modules/networking" + vpc_cidr = "${var.vpc_cidr}" + environment = "${var.environment}" + region = "${var.region}" +} + +module "worker" { + source = "./modules/worker" + environment = "${var.environment}" + region = "${var.region}" + fargate_cpu = "${var.fargate_cpu}" + fargate_memory = "${var.fargate_memory}" + vpc_id = "${module.networking.vpc_id}" + subnet_ids = ["${module.networking.public_subnet_ids}"] + default_security_group_id = "${module.networking.default_security_group_id}" +} diff --git a/modules/networking/main.tf b/modules/networking/main.tf new file mode 100644 index 0000000..31fa741 --- /dev/null +++ b/modules/networking/main.tf @@ -0,0 +1,79 @@ +/* Fetch the available AZs in the configured region */ +data "aws_availability_zones" "available" {} + +/* The VPC */ +resource "aws_vpc" "vpc" { + cidr_block = "${var.vpc_cidr}" + enable_dns_hostnames = true + enable_dns_support = true + + tags { + Name = "${var.environment}-vpc" + Environment = "${var.environment}" + } +} + +/* Subnets */ +/* Internet gateway for the public subnet */ +resource "aws_internet_gateway" "ig" { + vpc_id = "${aws_vpc.vpc.id}" + + tags { + Name = "${var.environment}-igw" + Environment = "${var.environment}" + } +} + +/* Public subnet */ +resource "aws_subnet" "public_subnet" { + vpc_id = "${aws_vpc.vpc.id}" + cidr_block = "${cidrsubnet(var.vpc_cidr, 8, 1)}" + availability_zone = "${data.aws_availability_zones.available.names[0]}" + map_public_ip_on_launch = true + + tags { + Name = "${var.environment}-${data.aws_availability_zones.available.names[0]}-public-subnet" + Environment = "${var.environment}" + } +} + +/* Routing table for public subnet */ +resource "aws_route_table" "public" { + vpc_id = "${aws_vpc.vpc.id}" + + tags { + Name = "${var.environment}-public-route-table" + Environment = "${var.environment}" + } +} + +resource "aws_route" "public_internet_gateway" { + route_table_id = "${aws_route_table.public.id}" + destination_cidr_block = "0.0.0.0/0" + gateway_id = "${aws_internet_gateway.ig.id}" +} + +/* Route table associations */ +resource "aws_route_table_association" "public" { + subnet_id = "${aws_subnet.public_subnet.id}" + route_table_id = "${aws_route_table.public.id}" +} + +/* VPC's Default Security Group */ +resource "aws_security_group" "default" { + name = "${var.environment}-default-sg" + description = "Default security group to allow all outbound" + vpc_id = "${aws_vpc.vpc.id}" + depends_on = ["aws_vpc.vpc"] + + egress { + from_port = "0" + to_port = "0" + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags { + Environment = "${var.environment}" + } +} diff --git a/modules/networking/outputs.tf b/modules/networking/outputs.tf new file mode 100644 index 0000000..19b2ef0 --- /dev/null +++ b/modules/networking/outputs.tf @@ -0,0 +1,11 @@ +output "vpc_id" { + value = "${aws_vpc.vpc.id}" +} + +output "public_subnet_ids" { + value = ["${aws_subnet.public_subnet.*.id}"] +} + +output "default_security_group_id" { + value = "${aws_security_group.default.id}" +} diff --git a/modules/networking/variables.tf b/modules/networking/variables.tf new file mode 100644 index 0000000..b110f8b --- /dev/null +++ b/modules/networking/variables.tf @@ -0,0 +1,3 @@ +variable "vpc_cidr" {} +variable "environment" {} +variable "region" {} diff --git a/modules/worker/main.tf b/modules/worker/main.tf new file mode 100644 index 0000000..4330123 --- /dev/null +++ b/modules/worker/main.tf @@ -0,0 +1,83 @@ +resource "aws_cloudwatch_log_group" "pb_worker" { + name = "pb_worker" + + tags { + Environment = "${var.environment}" + Application = "PolicyBrain Worker" + } +} + +/* ECS cluster */ +resource "aws_ecs_cluster" "cluster" { + name = "${var.environment}-ecs-cluster" +} + +data "aws_ecr_repository" "flask" { + name = "flask" +} + +data "aws_ecr_repository" "celery" { + name = "celery" +} + +data "aws_iam_role" "ecs_task" { + name = "ecsTaskExecutionRole" +} + +/* ECS task definition for backend workers */ +data "template_file" "worker_task" { + template = "${file("${path.module}/tasks/worker_definition.json")}" + + vars { + flask_image = "${data.aws_ecr_repository.flask.repository_url}" + celery_image = "${data.aws_ecr_repository.celery.repository_url}" + ecr_region = "${var.region}" + memory = "${var.fargate_memory}" + log_group = "${aws_cloudwatch_log_group.pb_worker.name}" + log_region = "${var.region}" + } +} + +resource "aws_ecs_task_definition" "worker" { + family = "${var.environment}_worker" + container_definitions = "${data.template_file.worker_task.rendered}" + requires_compatibilities = ["FARGATE"] + network_mode = "awsvpc" + cpu = "${var.fargate_cpu}" + memory = "${var.fargate_memory}" + execution_role_arn = "${data.aws_iam_role.ecs_task.arn}" + task_role_arn = "${data.aws_iam_role.ecs_task.arn}" +} + +/* Security Group for ECS */ +resource "aws_security_group" "ecs_service" { + vpc_id = "${var.vpc_id}" + name = "${var.environment}-ecs-service-sg" + + ingress { + from_port = 5050 + to_port = 5050 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + tags { + Name = "${var.environment}-ecs-service-sg" + Environment = "${var.environment}" + } +} + +resource "aws_ecs_service" "worker" { + name = "${var.environment}-worker" + task_definition = "${aws_ecs_task_definition.worker.arn}" + desired_count = 1 + launch_type = "FARGATE" + cluster = "${aws_ecs_cluster.cluster.id}" + + network_configuration { + security_groups = ["${aws_security_group.ecs_service.id}", + "${var.default_security_group_id}"] + subnets = ["${var.subnet_ids}"] + assign_public_ip = true + } +} diff --git a/modules/worker/tasks/worker_definition.json b/modules/worker/tasks/worker_definition.json new file mode 100644 index 0000000..4f14f12 --- /dev/null +++ b/modules/worker/tasks/worker_definition.json @@ -0,0 +1,78 @@ +[ + { + "name": "redis", + "image": "redis", + "portMappings": [ + { + "containerPort": 6379, + "hostPort": 6379, + "protocol": "tcp" + } + ], + "environment": [], + "memory": ${memory}, + "networkMode": "awsvpc", + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "${log_group}", + "awslogs-region": "${log_region}", + "awslogs-stream-prefix": "web" + } + } + }, + { + "name": "celery", + "image": "${celery_image}", + "portMappings": [ + { + "containerPort": 55555, + "hostPort": 55555, + "protocol": "tcp" + } + ], + "environment": [ + {"name": "CELERY_BROKER_URL", + "value": "redis://localhost:6379/0"}, + {"name": "CELERY_RESULT_BACKEND", + "value": "redis://localhost:6379/0"} + ], + "memory": ${memory}, + "networkMode": "awsvpc", + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "${log_group}", + "awslogs-region": "${log_region}", + "awslogs-stream-prefix": "web" + } + } + }, + { + "name": "flask", + "image": "${flask_image}", + "portMappings": [ + { + "containerPort": 5050, + "hostPort": 5050, + "protocol": "tcp" + } + ], + "environment": [ + {"name": "CELERY_BROKER_URL", + "value": "redis://localhost:6379/0"}, + {"name": "CELERY_RESULT_BACKEND", + "value": "redis://localhost:6379/0"} + ], + "memory": ${memory}, + "networkMode": "awsvpc", + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "${log_group}", + "awslogs-region": "${log_region}", + "awslogs-stream-prefix": "web" + } + } + } +] diff --git a/modules/worker/variables.tf b/modules/worker/variables.tf new file mode 100644 index 0000000..fb31b36 --- /dev/null +++ b/modules/worker/variables.tf @@ -0,0 +1,7 @@ +variable "vpc_id" {} +variable "environment" {} +variable "region" {} +variable "fargate_cpu" {} +variable "fargate_memory" {} +variable "subnet_ids" {type = "list"} +variable "default_security_group_id" {} diff --git a/terraform.tfvars b/terraform.tfvars new file mode 100644 index 0000000..8acb626 --- /dev/null +++ b/terraform.tfvars @@ -0,0 +1,9 @@ +environment = "production" +region = "us-east-2" + +# VPC +vpc_cidr = "10.0.0.0/16" + +# Fargate +fargate_cpu = 256 +fargate_memory = 512 diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..e9baaae --- /dev/null +++ b/variables.tf @@ -0,0 +1,5 @@ +variable "environment" {} +variable "region" {} +variable "vpc_cidr" {} +variable "fargate_cpu" {} +variable "fargate_memory" {} From 7bbb0198c17cd5892a6d9b78f87ef0486475e42b Mon Sep 17 00:00:00 2001 From: Lucas Szwarcberg Date: Thu, 12 Jul 2018 10:02:12 -0400 Subject: [PATCH 2/4] Next version: adding ElastiCache, ELB, workspaces, separation of Flask/Celery tasks --- main.tf | 31 ++- modules/networking/main.tf | 33 +-- modules/networking/outputs.tf | 6 +- modules/networking/variables.tf | 1 - modules/worker/main.tf | 263 +++++++++++++++++--- modules/worker/outputs.tf | 3 + modules/worker/tasks/celery_definition.json | 21 ++ modules/worker/tasks/flask_definition.json | 28 +++ modules/worker/tasks/worker_definition.json | 78 ------ modules/worker/variables.tf | 5 +- terraform.tfvars | 9 +- variables.tf | 5 +- 12 files changed, 308 insertions(+), 175 deletions(-) create mode 100644 modules/worker/outputs.tf create mode 100644 modules/worker/tasks/celery_definition.json create mode 100644 modules/worker/tasks/flask_definition.json delete mode 100644 modules/worker/tasks/worker_definition.json diff --git a/main.tf b/main.tf index 4ab31a4..92db417 100644 --- a/main.tf +++ b/main.tf @@ -2,20 +2,27 @@ provider "aws" { region = "${var.region}" } +terraform { + backend "s3" { + bucket = "ospc-terraform-state-storage-s3" + key = "terraform.tfstate" + dynamodb_table = "ospc-terraform-state-lock-table" + region = "us-east-2" + encrypt = true + } +} + module "networking" { - source = "./modules/networking" - vpc_cidr = "${var.vpc_cidr}" - environment = "${var.environment}" - region = "${var.region}" + source = "./modules/networking" + environment = "${terraform.workspace}" + region = "${var.region}" } module "worker" { - source = "./modules/worker" - environment = "${var.environment}" - region = "${var.region}" - fargate_cpu = "${var.fargate_cpu}" - fargate_memory = "${var.fargate_memory}" - vpc_id = "${module.networking.vpc_id}" - subnet_ids = ["${module.networking.public_subnet_ids}"] - default_security_group_id = "${module.networking.default_security_group_id}" + source = "./modules/worker" + environment = "${terraform.workspace}" + region = "${var.region}" + vpc_id = "${module.networking.vpc_id}" + subnet_ids = ["${module.networking.public_subnet_ids}"] + api_hostname = "${var.api_hostname}" } diff --git a/modules/networking/main.tf b/modules/networking/main.tf index 31fa741..1709f1b 100644 --- a/modules/networking/main.tf +++ b/modules/networking/main.tf @@ -3,7 +3,7 @@ data "aws_availability_zones" "available" {} /* The VPC */ resource "aws_vpc" "vpc" { - cidr_block = "${var.vpc_cidr}" + cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true @@ -25,14 +25,15 @@ resource "aws_internet_gateway" "ig" { } /* Public subnet */ -resource "aws_subnet" "public_subnet" { +resource "aws_subnet" "public" { vpc_id = "${aws_vpc.vpc.id}" - cidr_block = "${cidrsubnet(var.vpc_cidr, 8, 1)}" - availability_zone = "${data.aws_availability_zones.available.names[0]}" + cidr_block = "${cidrsubnet(aws_vpc.vpc.cidr_block, 8, count.index+1)}" + count = 2 + availability_zone = "${data.aws_availability_zones.available.names[count.index]}" map_public_ip_on_launch = true tags { - Name = "${var.environment}-${data.aws_availability_zones.available.names[0]}-public-subnet" + Name = "${var.environment}-${data.aws_availability_zones.available.names[count.index]}-public-subnet" Environment = "${var.environment}" } } @@ -55,25 +56,7 @@ resource "aws_route" "public_internet_gateway" { /* Route table associations */ resource "aws_route_table_association" "public" { - subnet_id = "${aws_subnet.public_subnet.id}" + count = "${aws_subnet.public.count}" + subnet_id = "${aws_subnet.public.*.id[count.index]}" route_table_id = "${aws_route_table.public.id}" } - -/* VPC's Default Security Group */ -resource "aws_security_group" "default" { - name = "${var.environment}-default-sg" - description = "Default security group to allow all outbound" - vpc_id = "${aws_vpc.vpc.id}" - depends_on = ["aws_vpc.vpc"] - - egress { - from_port = "0" - to_port = "0" - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags { - Environment = "${var.environment}" - } -} diff --git a/modules/networking/outputs.tf b/modules/networking/outputs.tf index 19b2ef0..8863cf9 100644 --- a/modules/networking/outputs.tf +++ b/modules/networking/outputs.tf @@ -3,9 +3,5 @@ output "vpc_id" { } output "public_subnet_ids" { - value = ["${aws_subnet.public_subnet.*.id}"] -} - -output "default_security_group_id" { - value = "${aws_security_group.default.id}" + value = ["${aws_subnet.public.*.id}"] } diff --git a/modules/networking/variables.tf b/modules/networking/variables.tf index b110f8b..3ff0915 100644 --- a/modules/networking/variables.tf +++ b/modules/networking/variables.tf @@ -1,3 +1,2 @@ -variable "vpc_cidr" {} variable "environment" {} variable "region" {} diff --git a/modules/worker/main.tf b/modules/worker/main.tf index 4330123..ac14b10 100644 --- a/modules/worker/main.tf +++ b/modules/worker/main.tf @@ -1,9 +1,9 @@ -resource "aws_cloudwatch_log_group" "pb_worker" { - name = "pb_worker" +resource "aws_cloudwatch_log_group" "pb_workers" { + name = "${var.environment}-pb_workers" tags { Environment = "${var.environment}" - Application = "PolicyBrain Worker" + Application = "PolicyBrain Workers" } } @@ -12,72 +12,257 @@ resource "aws_ecs_cluster" "cluster" { name = "${var.environment}-ecs-cluster" } -data "aws_ecr_repository" "flask" { - name = "flask" -} - -data "aws_ecr_repository" "celery" { - name = "celery" +resource "aws_ecr_repository" "celeryflask" { + name = "${var.environment}-celeryflask" } data "aws_iam_role" "ecs_task" { name = "ecsTaskExecutionRole" } -/* ECS task definition for backend workers */ -data "template_file" "worker_task" { - template = "${file("${path.module}/tasks/worker_definition.json")}" +/* Redis instance to store job queue and results */ +resource "aws_elasticache_subnet_group" "jobqr" { + name = "${var.environment}-jobqr" + subnet_ids = ["${var.subnet_ids}"] +} + +resource "aws_security_group" "jobqr" { + vpc_id = "${var.vpc_id}" + name = "${var.environment}-elasticache-jobqr-sg" + + ingress { + from_port = "${var.redis_port}" + to_port = "${var.redis_port}" + protocol = "tcp" + security_groups = ["${aws_security_group.ecs_flask.id}", + "${aws_security_group.ecs_celery.id}"] + } + + tags { + Environment = "${var.environment}" + } +} + +/* Fetch the available AZs in the configured region */ +data "aws_availability_zones" "available" {} + +resource "aws_elasticache_cluster" "jobqr" { + cluster_id = "${var.environment}-jobqr" + engine = "redis" + availability_zone = "${data.aws_availability_zones.available.names[0]}" + subnet_group_name = "${aws_elasticache_subnet_group.jobqr.name}" + security_group_ids = ["${aws_security_group.jobqr.id}"] + node_type = "cache.t2.small" + num_cache_nodes = 1 + parameter_group_name = "default.redis4.0" + port = "${var.redis_port}" + apply_immediately = true +} + +/* Security Group for Celery */ +resource "aws_security_group" "ecs_celery" { + vpc_id = "${var.vpc_id}" + name = "${var.environment}-ecs-celery-sg" + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags { + Environment = "${var.environment}" + } +} + +/* Celery worker */ +data "template_file" "celery_task" { + template = "${file("${path.module}/tasks/celery_definition.json")}" vars { - flask_image = "${data.aws_ecr_repository.flask.repository_url}" - celery_image = "${data.aws_ecr_repository.celery.repository_url}" - ecr_region = "${var.region}" - memory = "${var.fargate_memory}" - log_group = "${aws_cloudwatch_log_group.pb_worker.name}" - log_region = "${var.region}" + redis_hostname = "${aws_elasticache_cluster.jobqr.cache_nodes.0.address}" + redis_port = "${var.redis_port}" + repository_url = "${aws_ecr_repository.celeryflask.repository_url}" + ecr_region = "${var.region}" + log_group = "${aws_cloudwatch_log_group.pb_workers.name}" + log_region = "${var.region}" } } -resource "aws_ecs_task_definition" "worker" { - family = "${var.environment}_worker" - container_definitions = "${data.template_file.worker_task.rendered}" +resource "aws_ecs_task_definition" "celery" { + family = "${var.environment}-celery" + container_definitions = "${data.template_file.celery_task.rendered}" requires_compatibilities = ["FARGATE"] network_mode = "awsvpc" - cpu = "${var.fargate_cpu}" - memory = "${var.fargate_memory}" + cpu = 1024 + memory = 4096 execution_role_arn = "${data.aws_iam_role.ecs_task.arn}" task_role_arn = "${data.aws_iam_role.ecs_task.arn}" } -/* Security Group for ECS */ -resource "aws_security_group" "ecs_service" { - vpc_id = "${var.vpc_id}" - name = "${var.environment}-ecs-service-sg" +resource "aws_ecs_service" "celery" { + name = "${var.environment}-celery" + task_definition = "${aws_ecs_task_definition.celery.arn}" + desired_count = 4 + launch_type = "FARGATE" + cluster = "${aws_ecs_cluster.cluster.id}" - ingress { - from_port = 5050 - to_port = 5050 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] + network_configuration { + security_groups = ["${aws_security_group.ecs_celery.id}"] + subnets = ["${var.subnet_ids}"] + assign_public_ip = true } +} + +/* Security Group for Flask */ +resource "aws_security_group" "ecs_flask" { + vpc_id = "${var.vpc_id}" + name = "${var.environment}-ecs-flask-sg" tags { - Name = "${var.environment}-ecs-service-sg" Environment = "${var.environment}" } } -resource "aws_ecs_service" "worker" { - name = "${var.environment}-worker" - task_definition = "${aws_ecs_task_definition.worker.arn}" +resource "aws_security_group_rule" "allow_all_egress" { + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = "${aws_security_group.ecs_flask.id}" +} + +/* Flask worker */ +data "template_file" "flask_task" { + template = "${file("${path.module}/tasks/flask_definition.json")}" + + vars { + redis_hostname = "${aws_elasticache_cluster.jobqr.cache_nodes.0.address}" + redis_port = "${var.redis_port}" + repository_url = "${aws_ecr_repository.celeryflask.repository_url}" + ecr_region = "${var.region}" + log_group = "${aws_cloudwatch_log_group.pb_workers.name}" + log_region = "${var.region}" + } +} + +resource "aws_ecs_task_definition" "flask" { + family = "${var.environment}-flask" + container_definitions = "${data.template_file.flask_task.rendered}" + requires_compatibilities = ["FARGATE"] + network_mode = "awsvpc" + cpu = 1024 + memory = 4096 + execution_role_arn = "${data.aws_iam_role.ecs_task.arn}" + task_role_arn = "${data.aws_iam_role.ecs_task.arn}" +} + +resource "aws_ecs_service" "flask" { + name = "${var.environment}-flask" + task_definition = "${aws_ecs_task_definition.flask.arn}" desired_count = 1 launch_type = "FARGATE" cluster = "${aws_ecs_cluster.cluster.id}" network_configuration { - security_groups = ["${aws_security_group.ecs_service.id}", - "${var.default_security_group_id}"] - subnets = ["${var.subnet_ids}"] + security_groups = ["${aws_security_group.ecs_flask.id}"] + /* With X subnets, ECS will create X instances even if X > desired count */ + subnets = ["${var.subnet_ids[0]}"] assign_public_ip = true } + + /* See below for LB setup */ + load_balancer { + target_group_arn = "${aws_lb_target_group.flask.id}" + container_name = "flask" + container_port = "5050" + } +} + +/* Application Load Balancer */ +resource "aws_security_group" "lb_public" { + name = "${var.environment}-lb-public" + vpc_id = "${var.vpc_id}" + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 5050 + to_port = 5050 + protocol = "tcp" + security_groups = ["${aws_security_group.ecs_flask.id}"] + } + + tags { + Environment = "${var.environment}" + } +} + +resource "aws_security_group_rule" "allow_into_flask_from_lb" { + type = "ingress" + from_port = 5050 + to_port = 5050 + protocol = "tcp" + security_group_id = "${aws_security_group.ecs_flask.id}" + source_security_group_id = "${aws_security_group.lb_public.id}" +} + +resource "aws_lb" "public" { + load_balancer_type = "application" + security_groups = ["${aws_security_group.lb_public.id}"] + subnets = ["${var.subnet_ids}"] + + enable_deletion_protection = true +} + +resource "aws_lb_target_group" "flask" { + name = "${var.environment}-flask" + port = 5050 + protocol = "HTTP" + vpc_id = "${var.vpc_id}" + target_type = "ip" + + health_check { + path = "/hello" + matcher = 200 + } +} + +resource "aws_lb_listener" "public_http" { + load_balancer_arn = "${aws_lb.public.id}" + port = 80 + protocol = "HTTP" + + default_action { + target_group_arn = "${aws_lb_target_group.flask.id}" + type = "forward" + } +} + +/* Route53 hosted zone */ +resource "aws_route53_zone" "api" { + name = "${var.environment == "production" ? "" : "${var.environment}."}${var.api_hostname}" + + lifecycle { + prevent_destroy = true + } +} + +resource "aws_route53_record" "lb" { + zone_id = "${aws_route53_zone.api.zone_id}" + name = "${aws_route53_zone.api.name}" + type = "A" + + alias { + name = "${aws_lb.public.dns_name}" + zone_id = "${aws_lb.public.zone_id}" + evaluate_target_health = true + } } diff --git a/modules/worker/outputs.tf b/modules/worker/outputs.tf new file mode 100644 index 0000000..979553a --- /dev/null +++ b/modules/worker/outputs.tf @@ -0,0 +1,3 @@ +output "ecs_cluster_arn" { + value = "${aws_ecs_cluster.cluster.arn}" +} diff --git a/modules/worker/tasks/celery_definition.json b/modules/worker/tasks/celery_definition.json new file mode 100644 index 0000000..e42904b --- /dev/null +++ b/modules/worker/tasks/celery_definition.json @@ -0,0 +1,21 @@ +[ + { + "name": "celery", + "image": "${repository_url}:celery", + "environment": [ + {"name": "CELERY_BROKER_URL", + "value": "redis://${redis_hostname}:${redis_port}/0"}, + {"name": "CELERY_RESULT_BACKEND", + "value": "redis://${redis_hostname}:${redis_port}/0"} + ], + "networkMode": "awsvpc", + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "${log_group}", + "awslogs-region": "${log_region}", + "awslogs-stream-prefix": "web" + } + } + } +] diff --git a/modules/worker/tasks/flask_definition.json b/modules/worker/tasks/flask_definition.json new file mode 100644 index 0000000..1062c63 --- /dev/null +++ b/modules/worker/tasks/flask_definition.json @@ -0,0 +1,28 @@ +[ + { + "name": "flask", + "image": "${repository_url}:flask", + "portMappings": [ + { + "containerPort": 5050, + "hostPort": 5050, + "protocol": "tcp" + } + ], + "environment": [ + {"name": "CELERY_BROKER_URL", + "value": "redis://${redis_hostname}:${redis_port}/0"}, + {"name": "CELERY_RESULT_BACKEND", + "value": "redis://${redis_hostname}:${redis_port}/0"} + ], + "networkMode": "awsvpc", + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "${log_group}", + "awslogs-region": "${log_region}", + "awslogs-stream-prefix": "web" + } + } + } +] diff --git a/modules/worker/tasks/worker_definition.json b/modules/worker/tasks/worker_definition.json deleted file mode 100644 index 4f14f12..0000000 --- a/modules/worker/tasks/worker_definition.json +++ /dev/null @@ -1,78 +0,0 @@ -[ - { - "name": "redis", - "image": "redis", - "portMappings": [ - { - "containerPort": 6379, - "hostPort": 6379, - "protocol": "tcp" - } - ], - "environment": [], - "memory": ${memory}, - "networkMode": "awsvpc", - "logConfiguration": { - "logDriver": "awslogs", - "options": { - "awslogs-group": "${log_group}", - "awslogs-region": "${log_region}", - "awslogs-stream-prefix": "web" - } - } - }, - { - "name": "celery", - "image": "${celery_image}", - "portMappings": [ - { - "containerPort": 55555, - "hostPort": 55555, - "protocol": "tcp" - } - ], - "environment": [ - {"name": "CELERY_BROKER_URL", - "value": "redis://localhost:6379/0"}, - {"name": "CELERY_RESULT_BACKEND", - "value": "redis://localhost:6379/0"} - ], - "memory": ${memory}, - "networkMode": "awsvpc", - "logConfiguration": { - "logDriver": "awslogs", - "options": { - "awslogs-group": "${log_group}", - "awslogs-region": "${log_region}", - "awslogs-stream-prefix": "web" - } - } - }, - { - "name": "flask", - "image": "${flask_image}", - "portMappings": [ - { - "containerPort": 5050, - "hostPort": 5050, - "protocol": "tcp" - } - ], - "environment": [ - {"name": "CELERY_BROKER_URL", - "value": "redis://localhost:6379/0"}, - {"name": "CELERY_RESULT_BACKEND", - "value": "redis://localhost:6379/0"} - ], - "memory": ${memory}, - "networkMode": "awsvpc", - "logConfiguration": { - "logDriver": "awslogs", - "options": { - "awslogs-group": "${log_group}", - "awslogs-region": "${log_region}", - "awslogs-stream-prefix": "web" - } - } - } -] diff --git a/modules/worker/variables.tf b/modules/worker/variables.tf index fb31b36..1948d89 100644 --- a/modules/worker/variables.tf +++ b/modules/worker/variables.tf @@ -1,7 +1,6 @@ variable "vpc_id" {} variable "environment" {} variable "region" {} -variable "fargate_cpu" {} -variable "fargate_memory" {} variable "subnet_ids" {type = "list"} -variable "default_security_group_id" {} +variable "redis_port" {default = 6400} +variable "api_hostname" {} diff --git a/terraform.tfvars b/terraform.tfvars index 8acb626..ef753a5 100644 --- a/terraform.tfvars +++ b/terraform.tfvars @@ -1,9 +1,2 @@ -environment = "production" region = "us-east-2" - -# VPC -vpc_cidr = "10.0.0.0/16" - -# Fargate -fargate_cpu = 256 -fargate_memory = 512 +api_hostname = "ospcapi.org" diff --git a/variables.tf b/variables.tf index e9baaae..b0cc892 100644 --- a/variables.tf +++ b/variables.tf @@ -1,5 +1,2 @@ -variable "environment" {} variable "region" {} -variable "vpc_cidr" {} -variable "fargate_cpu" {} -variable "fargate_memory" {} +variable "api_hostname" {} From e7e3eabfda1790fe2ef1494a3149498cf8e042fd Mon Sep 17 00:00:00 2001 From: Lucas Szwarcberg Date: Thu, 12 Jul 2018 14:25:19 -0400 Subject: [PATCH 3/4] Route53 fix --- modules/worker/main.tf | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/modules/worker/main.tf b/modules/worker/main.tf index ac14b10..ff31f31 100644 --- a/modules/worker/main.tf +++ b/modules/worker/main.tf @@ -247,17 +247,13 @@ resource "aws_lb_listener" "public_http" { } /* Route53 hosted zone */ -resource "aws_route53_zone" "api" { - name = "${var.environment == "production" ? "" : "${var.environment}."}${var.api_hostname}" - - lifecycle { - prevent_destroy = true - } +data "aws_route53_zone" "api" { + name = "${var.api_hostname}" } resource "aws_route53_record" "lb" { - zone_id = "${aws_route53_zone.api.zone_id}" - name = "${aws_route53_zone.api.name}" + zone_id = "${data.aws_route53_zone.api.zone_id}" + name = "${var.environment == "production" ? "" : "${var.environment}."}${var.api_hostname}" type = "A" alias { From c1c86847a3854a5bf8c3aaa98d533f01a626328c Mon Sep 17 00:00:00 2001 From: Lucas Szwarcberg Date: Wed, 18 Jul 2018 11:51:35 -0400 Subject: [PATCH 4/4] Add README.md --- README.md | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/README.md b/README.md index 36ea68b..016f2ab 100644 --- a/README.md +++ b/README.md @@ -1 +1,83 @@ # pb_deploy +`pb_deploy` is a [Terraform](https://www.terraform.io) configuration for a +production deployment of [PolicyBrain], as run by the +[Open Source Policy Center], on Amazon Web Services infrastructure. + +## Why Terraform? +Terraform is a declarative "infrastructure as code" open-source application, +meaning that it can deploy a new set of infrastructure components with ease +given account information, and it can keep it up-to-date with a programmatically +specified configuration, performing changes only as needed. + +This is possible because Terraform keeps track of the [state][Terraform state]. +In the case of OSPC, we use [AWS S3 and DynamoDB][Terraform S3 backend] to share +and lock state information among different OSPC collaborators. + +## Infrastructure description +The main component of this infrastructure is [Amazon ECS Fargate], which is a +technology for deploying Docker containers without having to manage the +underlying servers. ECS is used to run relatively few Internet-facing Flask +servers, as well as relatively many Celery workers. Rather than being run on +ECS, Redis is managed through [Amazon ElastiCache], and stores the task queue. +[Amazon Route 53] and [Amazon ELB] are used to provide a permanent hostname, +such as `staging.ospcapi.org`, that will always point to the correct ECS task +IP(s). (Currently, the alternative, Route53 Service Discovery for ECS, only + supports private IPs.) + +The networking is handled using a [Virtual Public Cloud][Amazon VPC] and a +single public subnet, and all instances are given public IPs. This is to avoid +the expense of a NAT gateway required with private subnets, but AWS security +groups are used to define permissible Internet access as restrictively as +possible. + +## Usage +Because of Terraform's declarative nature, the steps for initial setup and for +modification of the infrastructure should be the same. You should specify +[AWS authentication credentials][Terraform AWS authentication] either through +environment variables or through the `~/.aws/credentials` file. The +configuration also assumes that the following resources have already been +created in the region given in the `terraform.tfvars` file: + + - An [Amazon ECS task execution role] + - An Amazon Route 53 zone with the name given in `terraform.tfvars`; this can + be any domain or subdomain for which you need to configure the nameservers + - An S3 bucket and a DynamoDB table with the names given in `main.tf` in order + to store Terraform state + +This configuration makes extensive use of [Terraform workspaces] in order to +separate state and resources for different deployment environments. The +`default` workspace can be used if this distinction is not needed, but should +**not** be used for OSPC deployments, which will make use of the `production` +and `staging` environments. + +An example deployment workflow would be: + +```shell +cd ~/pb_deploy +terraform init +terraform workspace select production +terraform plan -out=production.tfplan +terraform apply production.tfplan +``` + +A deployment environment could be torn down as follows: + +```shell +cd ~/pb_deploy +terraform init +terraform workspace select staging +terraform destroy +``` + +[PolicyBrain]: https://github.com/OpenSourcePolicyCenter/PolicyBrain +[Open Source Policy Center]: https://github.com/OpenSourcePolicyCenter/PolicyBrain +[Terraform state]: https://www.terraform.io/docs/state/index.html +[Terraform S3 backend]: https://www.terraform.io/docs/backends/types/s3.html +[Amazon ECS Fargate]: https://aws.amazon.com/fargate/ +[Amazon ElastiCache]: https://aws.amazon.com/elasticache/ +[Amazon Route 53]: https://aws.amazon.com/route53/ +[Amazon ELB]: https://aws.amazon.com/elasticloadbalancing/ +[Amazon VPC]: https://aws.amazon.com/vpc/ +[Terraform AWS authentication]: https://www.terraform.io/docs/providers/aws/index.html#authentication +[Amazon ECS task execution role]: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html +[Terraform workspaces]: https://www.terraform.io/docs/state/workspaces.html