diff --git a/dns.tf b/dns.tf index 658628a..f4548a1 100644 --- a/dns.tf +++ b/dns.tf @@ -11,10 +11,12 @@ resource "aws_route53_record" "forum" { zone_id = "${aws_route53_zone.primary.zone_id}" name = "forum" type = "A" - ttl = "300" - # Get the IP address of our server's Elastic IP. - records = ["${module.language_learners_server.public_ip}"] + alias { + name = "${aws_lb.web_sites.dns_name}" + zone_id = "${aws_lb.web_sites.zone_id}" + evaluate_target_health = false + } } # Our "super-challenge" record. @@ -22,10 +24,12 @@ resource "aws_route53_record" "super_challenge" { zone_id = "${aws_route53_zone.primary.zone_id}" name = "super-challenge" type = "A" - ttl = "300" - # Get the IP address of our server's Elastic IP. - records = ["${module.language_learners_server.public_ip}"] + alias { + name = "${aws_lb.web_sites.dns_name}" + zone_id = "${aws_lb.web_sites.zone_id}" + evaluate_target_health = false + } } # An "old-forum" record while we're migrating. @@ -37,8 +41,7 @@ resource "aws_route53_record" "old-forum" { records = ["34.204.9.245"] } -# Our "www" record, still pointing to the old setup, but we'll update this -# soon to point to a Jekyll-based blog. +# Our "www" record, pointing to a blog hosted on GitHub Pages. resource "aws_route53_record" "www" { zone_id = "${aws_route53_zone.primary.zone_id}" name = "www" diff --git a/github_ecs_pipeline/build.tf b/github_ecs_pipeline/build.tf index 4ab9bd6..c16ce69 100644 --- a/github_ecs_pipeline/build.tf +++ b/github_ecs_pipeline/build.tf @@ -14,6 +14,7 @@ resource "aws_codebuild_project" "build" { compute_type = "BUILD_GENERAL1_SMALL" image = "aws/codebuild/docker:1.12.1" type = "LINUX_CONTAINER" + privileged_mode = true environment_variable { "name" = "IMAGE_REPO_NAME" diff --git a/github_ecs_pipeline/ecs.tf b/github_ecs_pipeline/ecs.tf index 432ecc4..790ef02 100644 --- a/github_ecs_pipeline/ecs.tf +++ b/github_ecs_pipeline/ecs.tf @@ -36,4 +36,18 @@ resource "aws_ecs_service" "service" { # version. deployment_maximum_percent = 100 deployment_minimum_healthy_percent = 0 + + # Allow our ECS to talk to our load balancer on behalf of our service. + iam_role = "arn:aws:iam::771600087445:role/ecsServiceRole" + + # Hook this up to our load balancer so we get web traffic. + load_balancer { + target_group_arn = "${aws_lb_target_group.target_group.arn}" + container_name = "${var.name}" + container_port = 80 + } + + # Don't try to create this until our ALB listener actually has a validated + # certificate assigned. + depends_on = ["aws_lb_listener_certificate.cert"] } diff --git a/github_ecs_pipeline/load_balancing.tf b/github_ecs_pipeline/load_balancing.tf new file mode 100644 index 0000000..ef0cda6 --- /dev/null +++ b/github_ecs_pipeline/load_balancing.tf @@ -0,0 +1,52 @@ +# Register this container with our AWS "application load balancer", which can +# serve multiple domains with certificates. + +resource "aws_lb_target_group" "target_group" { + name = "${var.name}" + port = 80 + protocol = "HTTP" + vpc_id = "vpc-5abfab3c" +} + +resource "aws_lb_listener_rule" "proxy" { + listener_arn = "${var.listener_arn}" + priority = "${var.listener_rule_priority}" + + action { + type = "forward" + target_group_arn = "${aws_lb_target_group.target_group.arn}" + } + + condition { + field = "host-header" + values = ["${var.host}"] + } +} + +resource "aws_acm_certificate" "cert" { + domain_name = "${var.host}" + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_route53_record" "cert_validation" { + name = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_name}" + type = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_type}" + zone_id = "${var.zone_id}" + records = ["${aws_acm_certificate.cert.domain_validation_options.0.resource_record_value}"] + ttl = 60 +} + +resource "aws_acm_certificate_validation" "cert" { + certificate_arn = "${aws_acm_certificate.cert.arn}" + validation_record_fqdns = ["${aws_route53_record.cert_validation.fqdn}"] +} + +resource "aws_lb_listener_certificate" "cert" { + listener_arn = "${var.listener_arn}" + certificate_arn = "${aws_acm_certificate_validation.cert.certificate_arn}" +} + diff --git a/github_ecs_pipeline/variables.tf b/github_ecs_pipeline/variables.tf index 2a6c01f..3b56836 100644 --- a/github_ecs_pipeline/variables.tf +++ b/github_ecs_pipeline/variables.tf @@ -4,6 +4,10 @@ variable "name" { description = "The name of the pipeline, and the image it builds." } +variable "host" { + description = "The hostname to use for this service." +} + variable "github_repo" { description = "The name of the GitHub repository we build." } @@ -45,6 +49,18 @@ variable "taskdef_revision" { description = "The revision of the taskdef that we defined using Terraform. Usually overriden by deployment pipelines." } +variable "zone_id" { + description = "The DNS zone ID to use for HTTPS certificate confirmation." +} + +variable "listener_arn" { + description = "The HTTPS listener associated with our load balancer." +} + +variable "listener_rule_priority" { + description = "The priority for the listener rule on our load balancer. Must be unique." +} + variable "notification_topic_arn" { description = "The ARN of an SNS notification topic that will receive messages when something interesting happens." } diff --git a/load_balancer.tf b/load_balancer.tf new file mode 100644 index 0000000..5f20250 --- /dev/null +++ b/load_balancer.tf @@ -0,0 +1,77 @@ +resource "aws_lb" "web_sites" { + name = "web-sites" + internal = false + load_balancer_type = "application" + security_groups = ["${aws_security_group.load_balancer.id}"] + subnets = ["subnet-011dc549", "subnet-0f045d6a"] +} + +resource "aws_lb_listener" "web_sites_https" { + load_balancer_arn = "${aws_lb.web_sites.arn}" + port = "443" + protocol = "HTTPS" + ssl_policy = "ELBSecurityPolicy-2015-05" + certificate_arn = "${aws_acm_certificate_validation.language_learners.certificate_arn}" + + default_action { + type = "redirect" + redirect { + host = "forum.language-learners.org" + port = "443" + protocol = "HTTPS" + status_code = "HTTP_301" + } + } +} + +# An AWS security group describing the firewall rules for a load balancer. +resource "aws_security_group" "load_balancer" { + name = "load-balancer" + description = "Allow HTTP and HTTPS traffic." + + # Allow inbound HTTP traffic. + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + # Allow inbound HTTPS traffic. + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + # Allow all outbound traffic. + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_acm_certificate" "language_learners" { + domain_name = "language-learners.org" + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_route53_record" "language_learners_validation" { + name = "${aws_acm_certificate.language_learners.domain_validation_options.0.resource_record_name}" + type = "${aws_acm_certificate.language_learners.domain_validation_options.0.resource_record_type}" + zone_id = "${aws_route53_zone.primary.zone_id}" + records = ["${aws_acm_certificate.language_learners.domain_validation_options.0.resource_record_value}"] + ttl = 60 +} + +resource "aws_acm_certificate_validation" "language_learners" { + certificate_arn = "${aws_acm_certificate.language_learners.arn}" + validation_record_fqdns = ["${aws_route53_record.language_learners_validation.fqdn}"] +} diff --git a/nginx-proxy-container-definitions.json b/nginx-proxy-container-definitions.json deleted file mode 100644 index 629bf4d..0000000 --- a/nginx-proxy-container-definitions.json +++ /dev/null @@ -1,91 +0,0 @@ -[ - { - "name": "nginx-proxy", - "image": "jwilder/nginx-proxy", - "memory": 128, - "essential": true, - "portMappings": [ - { - "hostPort": 80, - "containerPort": 80, - "protocol": "tcp" - }, - { - "hostPort": 443, - "containerPort": 443, - "protocol": "tcp" - } - ], - "environment": [ - { - "name": "DEFAULT_HOST", - "value": "forum.language-learners.org" - } - ], - "mountPoints": [ - { - "sourceVolume": "nginx-vhost-d", - "containerPath": "/etc/nginx/vhost.d" - }, - { - "sourceVolume": "nginx-html", - "containerPath": "/usr/share/nginx/html" - }, - { - "sourceVolume": "docker-sock", - "containerPath": "/tmp/docker.sock", - "readOnly": true - }, - { - "sourceVolume": "nginx-certs", - "containerPath": "/etc/nginx/certs", - "readOnly": true - } - ], - "volumesFrom": null, - "hostname": null, - "user": null, - "workingDirectory": null, - "extraHosts": null, - "logConfiguration": null, - "ulimits": null, - "dockerLabels": { - "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy": "true" - } - }, - { - "name": "letsencrypt-nginx-proxy-companion", - "image": "jrcs/letsencrypt-nginx-proxy-companion", - "memory": 64, - "essential": true, - "portMappings": [], - "environment": null, - "mountPoints": [ - { - "sourceVolume": "nginx-vhost-d", - "containerPath": "/etc/nginx/vhost.d" - }, - { - "sourceVolume": "nginx-html", - "containerPath": "/usr/share/nginx/html" - }, - { - "sourceVolume": "docker-sock", - "containerPath": "/var/run/docker.sock", - "readOnly": true - }, - { - "sourceVolume": "nginx-certs", - "containerPath": "/etc/nginx/certs", - "readOnly": false - } - ], - "hostname": null, - "user": null, - "workingDirectory": null, - "extraHosts": null, - "logConfiguration": null, - "ulimits": null, - "dockerLabels": null - } -] diff --git a/nginx_proxy.tf b/nginx_proxy.tf deleted file mode 100644 index 0f3ed8d..0000000 --- a/nginx_proxy.tf +++ /dev/null @@ -1,69 +0,0 @@ -# This defines the nginx reverse proxy service which handles: -# -# 1. Mapping multiple domains names each to the correct container. -# 2. Providing HTTPS support using Let's Encrypt. -# -# For more information, see: -# -# - https://github.com/jwilder/nginx-proxy -# - https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion -# -# We translate this configuration to Terraform + ECS task definition JSON. -# Also see the VIRTUAL_HOST environment variable which we add to the task -# definition JSON for each web container. -# -# We don't use a pipeline to build this, because everything we need is -# provided by two prebuilt upstream images, so we can just go ahead and -# reuse those. - -# An ECS service is in charge of making sure that our image gets run on the -# specified cluster. -resource "aws_ecs_service" "service" { - name = "nginx-proxy" - cluster = "${aws_ecs_cluster.language_learners.name}" - - # We only want to run one copy of this container. - desired_count = 1 - - # Use the task definition we declare below. - task_definition = "${aws_ecs_task_definition.nginx_proxy.arn}" - - # Don't try to run more than 100% of "desired_count" when updating the - # service. This means we never have two copies of the proxy running at - # once, but it also means there will be a brief outage when deploying a - # new version. - deployment_maximum_percent = 100 - deployment_minimum_healthy_percent = 0 -} - -# Load our container definitions from a template file. -data "template_file" "nginx_proxy_container_definitions" { - template = "${file("${path.module}/nginx-proxy-container-definitions.json")}" - vars {} -} - -# Declare our ECS task definition. -resource "aws_ecs_task_definition" "nginx_proxy" { - family = "nginx-proxy" - container_definitions = "${data.template_file.nginx_proxy_container_definitions.rendered}" - - # Define our volumes. These are used to map directories on our - # EBS-backed `/data` volume to volume names referred to in our - # `*-container-definitions.json` file. - volume { - name = "docker-sock" - host_path = "/var/run/docker.sock" - } - volume { - name = "nginx-certs" - host_path = "/data/nginx-proxy/etc/nginx/certs" - } - volume { - name = "nginx-vhost-d" - host_path = "/data/nginx-proxy/etc/nginx/vhost.d" - } - volume { - name = "nginx-html" - host_path = "/data/nginx-proxy/usr/share/nginx/html" - } -} diff --git a/phpbb-container-definitions.json b/phpbb-container-definitions.json index e7eff95..8c1c894 100644 --- a/phpbb-container-definitions.json +++ b/phpbb-container-definitions.json @@ -6,7 +6,11 @@ "dnsServers": null, "disableNetworking": null, "dnsSearchDomains": null, - "portMappings": [], + "portMappings": [ + { + "containerPort": 80 + } + ], "hostname": null, "essential": true, "entryPoint": null, @@ -41,18 +45,6 @@ "ulimits": null, "dockerSecurityOptions": null, "environment": [ - { - "name": "VIRTUAL_HOST", - "value": "forum.language-learners.org" - }, - { - "name": "LETSENCRYPT_HOST", - "value": "forum.language-learners.org" - }, - { - "name": "LETSENCRYPT_EMAIL", - "value": "letsencrypt@randomhacks.net" - } ], "links": null, "workingDirectory": null, diff --git a/phpbb_pipeline.tf b/phpbb_pipeline.tf index f4fbceb..b00caa5 100644 --- a/phpbb_pipeline.tf +++ b/phpbb_pipeline.tf @@ -4,13 +4,15 @@ module "phpbb_pipeline" { source = "github_ecs_pipeline" name = "phpbb" + host = "forum.language-learners.org" github_repo = "phpbb" github_branch = "custom" + listener_rule_priority = 99 # Pass our taskdef information to the module. taskdef_family = "${aws_ecs_task_definition.phpbb.family}" taskdef_revision = "${aws_ecs_task_definition.phpbb.revision}" - + # Standard parameters which are the same for all pipelines. aws_region = "${var.aws_region}" aws_account_id = "${var.aws_account_id}" @@ -19,6 +21,8 @@ module "phpbb_pipeline" { artifact_store_s3_bucket = "${aws_s3_bucket.codepipeline_artifacts.bucket}" ecs_cluster = "${aws_ecs_cluster.language_learners.name}" notification_topic_arn = "${aws_sns_topic.admin_updates.arn}" + zone_id = "${aws_route53_zone.primary.zone_id}" + listener_arn = "${aws_lb_listener.web_sites_https.arn}" } # Load our container definitions from a template file. diff --git a/servers.tf b/servers.tf index b6f8e6a..29a5299 100644 --- a/servers.tf +++ b/servers.tf @@ -7,7 +7,7 @@ module "language_learners_server" { name = "language-learners" ami = "${data.aws_ami.ecs_ami.id}" - instance_type = "t2.micro" + instance_type = "t2.small" ecs_cluster = "${aws_ecs_cluster.language_learners.name}" vpc_security_group_id = "${aws_security_group.web_server.id}" @@ -29,20 +29,13 @@ resource "aws_security_group" "web_server" { cidr_blocks = ["0.0.0.0/0"] } - # Allow inbound HTTP traffic. + # Allow inbound container traffic. ingress { - from_port = 80 - to_port = 80 + # This is supposedly the port range for automatically assigned ports. + from_port = 32768 + to_port = 61000 protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - - # Allow inbound HTTPS traffic. - ingress { - from_port = 443 - to_port = 443 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] + security_groups = ["${aws_security_group.load_balancer.id}"] } # Allow all outbound traffic. diff --git a/single_server_cluster/server.tf b/single_server_cluster/server.tf index 1e4a0bd..a0105b4 100644 --- a/single_server_cluster/server.tf +++ b/single_server_cluster/server.tf @@ -29,7 +29,7 @@ resource "aws_instance" "server" { associate_public_ip_address = true iam_instance_profile = "${aws_iam_instance_profile.instance_profile.name}" key_name = "aldebaran-emk" - + # The user_data script will be run when the instance boots up. This is # where we want to stick all customization and configuration, which # should be very minimal, because all the real work is done by ECS and @@ -38,13 +38,17 @@ resource "aws_instance" "server" { #!/bin/bash # Fail immediately if there are errors. -set -euo pipefail +set -euo pipefail -# Configure our ECS cluster membership. -echo ECS_CLUSTER="${var.ecs_cluster}" >> /etc/ecs/ecs.config +# Configure our ECS cluster membership. +cat <> /etc/ecs/ecs.config +ECS_CLUSTER=${var.ecs_cluster} +ECS_ENABLE_CONTAINER_METADATA=true +ECS_ENABLE_TASK_CPU_MEM_LIMIT=false +EOC # Install tools needed for this script. -yum -y install aws-cli +yum -y install aws-cli # Look up our instance ID using the AWS magic metadata address, and use it # to attach our EBS volume. Note that we tell this to mount as /dev/sdf, but @@ -67,7 +71,7 @@ mount /data # the Docker restart. sudo service docker restart sudo start ecs - + # Apply the latest security updates. We do this after setting up our # volume, just in case some security update wants to start Docker. We need # to guarantee that Docker is started _after_ the volume is mounted, or @@ -81,7 +85,7 @@ yum -y update # hacked. # # TODO: Configure automatic reboot after kernel updates? -yum -y install yum-cron +yum -y install yum-cron EOD tags { diff --git a/superchallengebot-container-definitions.json b/superchallengebot-container-definitions.json index a7127d9..723b3be 100644 --- a/superchallengebot-container-definitions.json +++ b/superchallengebot-container-definitions.json @@ -6,7 +6,11 @@ "dnsServers": null, "disableNetworking": null, "dnsSearchDomains": null, - "portMappings": [], + "portMappings": [ + { + "containerPort": 80 + } + ], "hostname": null, "essential": true, "entryPoint": null, @@ -21,18 +25,6 @@ "ulimits": null, "dockerSecurityOptions": null, "environment": [ - { - "name": "VIRTUAL_HOST", - "value": "super-challenge.language-learners.org" - }, - { - "name": "LETSENCRYPT_HOST", - "value": "super-challenge.language-learners.org" - }, - { - "name": "LETSENCRYPT_EMAIL", - "value": "letsencrypt@randomhacks.net" - } ], "links": null, "workingDirectory": null, diff --git a/superchallengebot_pipeline.tf b/superchallengebot_pipeline.tf index fa12d2b..779ba3a 100644 --- a/superchallengebot_pipeline.tf +++ b/superchallengebot_pipeline.tf @@ -5,8 +5,10 @@ module "superchallengebot_pipeline" { source = "github_ecs_pipeline" name = "superchallengebot" + host = "super-challenge.language-learners.org" github_repo = "superchallengebot" github_branch = "master" + listener_rule_priority = 100 # Pass our taskdef information to the module. taskdef_family = "${aws_ecs_task_definition.superchallengebot.family}" @@ -20,6 +22,8 @@ module "superchallengebot_pipeline" { artifact_store_s3_bucket = "${aws_s3_bucket.codepipeline_artifacts.bucket}" ecs_cluster = "${aws_ecs_cluster.language_learners.name}" notification_topic_arn = "${aws_sns_topic.admin_updates.arn}" + zone_id = "${aws_route53_zone.primary.zone_id}" + listener_arn = "${aws_lb_listener.web_sites_https.arn}" } # Load our container definitions from a template file. diff --git a/terraform.tfstate b/terraform.tfstate index b7974ab..91ec637 100644 Binary files a/terraform.tfstate and b/terraform.tfstate differ