From 186d04041dde8fcfa8dc93f08a604f752eff9f1f Mon Sep 17 00:00:00 2001 From: "Collin J. Doering" Date: Sat, 14 Jul 2018 23:37:51 -0400 Subject: [PATCH] Site infrastructure and deployment now managed with terraform Create a classic static site deployment using cloudfront with a s3 origin. Provision, verify and utilize a ACM certificate to enable (and force) https for cloudfront. This assumes that the build resources are available at ./_site as a null_resource is used to sync it to the s3 origin backing cloudfront. A IAM user and policy is provisioned prior to the null_resource execution with least privilege access to the s3 bucket. Note: The required terraform backend resources were manually provisioned. Signed-off-by: Collin J. Doering --- infra/main.tf | 241 ++++++++++++++++++++++++++ infra/outputs.tf | 0 infra/production.tfvars | 2 + infra/templates/s3_origin_policy.json | 33 ++++ infra/variables.tf | 18 ++ 5 files changed, 294 insertions(+) create mode 100644 infra/main.tf create mode 100644 infra/outputs.tf create mode 100644 infra/production.tfvars create mode 100644 infra/templates/s3_origin_policy.json create mode 100644 infra/variables.tf diff --git a/infra/main.tf b/infra/main.tf new file mode 100644 index 0000000..971ee83 --- /dev/null +++ b/infra/main.tf @@ -0,0 +1,241 @@ +terraform { + backend "s3" { + region = "ca-central-1" + bucket = "rekahsoft-terraform" + key = "blog-rekahsoft-ca/blog-rekahsoft-ca.tfstate" + dynamodb_table = "rekahsoft-terraform" + } +} + +provider "aws" { + region = "${var.region}" + version = "~> 1.9" + + assume_role = [{ + role_arn = "${var.workspace_iam_roles[terraform.workspace]}" + }] +} + +provider "aws" { + alias = "us_east_1" + region = "us-east-1" + version = "~> 1.9" + + assume_role = [{ + role_arn = "${var.workspace_iam_roles[terraform.workspace]}" + }] +} + +# +# Local values to be re-used throughout this template + +locals { + common_tags = "${map( + "Project", "${var.project}", + "Environment", "${terraform.workspace}" + )}" + cdn_origin_id = "${terraform.workspace}-origin-cdn" + project_env = "${var.project}-${terraform.workspace}" +} + + +# +# Data Sources + +data "template_file" "s3_origin_policy" { + template = "${file("templates/s3_origin_policy.json")}" + + vars { + bucket_arn = "${aws_s3_bucket.static.arn}" + user_arn = "${aws_iam_user.app_deploy.arn}" + cloudfront_arn = "${aws_cloudfront_origin_access_identity.origin_access_identity.iam_arn}" + } +} + +data "aws_route53_zone" "external" { + name = "${var.dns_apex}." +} + + +# +# Resources + +resource "aws_acm_certificate" "cert" { + domain_name = "${var.dns_name}" + validation_method = "DNS" + tags = "${local.common_tags}" + + provider = "aws.us_east_1" +} + +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 = "${data.aws_route53_zone.external.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}"] + + provider = "aws.us_east_1" +} + +resource "aws_s3_bucket" "static" { + bucket_prefix = "${local.project_env}" + acl = "private" + + website { + index_document = "index.html" + error_document = "error.html" + } + + tags = "${local.common_tags}" +} + +resource "aws_s3_bucket" "static_logs" { + bucket_prefix = "${local.project_env}" + acl = "private" +} + +resource "random_string" "app_deploy_username" { + length = 16 + special = true + override_special = "_+=,.@-" +} + +resource "aws_iam_user" "app_deploy" { + name = "${random_string.app_deploy_username.result}" +} + +resource "aws_iam_access_key" "app_deploy" { + user = "${aws_iam_user.app_deploy.name}" +# pgp_key = "keybase:some_person_that_exists" +} + +resource "aws_route53_record" "static" { + zone_id = "${data.aws_route53_zone.external.zone_id}" + name = "${var.dns_name}." + type = "A" + + alias { + name = "${aws_cloudfront_distribution.cdn.domain_name}" + zone_id = "${aws_cloudfront_distribution.cdn.hosted_zone_id}" + evaluate_target_health = true + } +} + +resource "aws_s3_bucket_policy" "static_policy" { + bucket = "${aws_s3_bucket.static.id}" + policy = "${data.template_file.s3_origin_policy.rendered}" +} + +resource "aws_cloudfront_origin_access_identity" "origin_access_identity" { + comment = "Origin access policy for ${local.project_env}" +} + +resource "aws_cloudfront_distribution" "cdn" { + # Static file origin + origin { + domain_name = "${aws_s3_bucket.static.bucket_regional_domain_name}" + origin_id = "${local.cdn_origin_id}" + + s3_origin_config { + origin_access_identity = "${aws_cloudfront_origin_access_identity.origin_access_identity.cloudfront_access_identity_path}" + } + } + + enabled = true + is_ipv6_enabled = true + comment = "CDN for ${var.project} (environment ${terraform.workspace})" + default_root_object = "index.html" + + # Return index.html for any route that is not found + custom_error_response { + error_caching_min_ttl = 0 + error_code = 403 + response_code = 200 + response_page_path = "/index.html" + } + + logging_config { + include_cookies = false + bucket = "${aws_s3_bucket.static_logs.bucket_domain_name}" + } + + aliases = ["${var.dns_name}"] + + default_cache_behavior { + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD", "OPTIONS"] + target_origin_id = "${local.cdn_origin_id}" + + forwarded_values { + query_string = false + + cookies { + forward = "none" + } + } + + min_ttl = 0 + default_ttl = 3600 + max_ttl = 86400 + viewer_protocol_policy = "redirect-to-https" + } + + # Cache behavior with precedence 0 + ordered_cache_behavior { + path_pattern = "index.html" + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD", "OPTIONS"] + target_origin_id = "${local.cdn_origin_id}" + + forwarded_values { + query_string = false + headers = ["Origin"] + cookies { + forward = "none" + } + } + + min_ttl = 0 + default_ttl = 0 + max_ttl = 0 + compress = true + viewer_protocol_policy = "redirect-to-https" + } + + price_class = "PriceClass_100" + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + tags = "${local.common_tags}" + + viewer_certificate { + acm_certificate_arn = "${aws_acm_certificate_validation.cert.certificate_arn}" + ssl_support_method = "sni-only" + minimum_protocol_version = "TLSv1.1_2016" + } +} + +resource "null_resource" "deploy_app" { + provisioner "local-exec" { + interpreter = ["bash", "-c"] + command = <