nagelflorian/terraform-aws-s3-review-apps

View on GitHub
main.tf

Summary

Maintainability
Test Coverage
module "label" {
  source      = "git::https://github.com/cloudposse/terraform-null-label.git?ref=tags/0.25.0"
  enabled     = var.enabled
  namespace   = var.namespace
  environment = var.environment
  stage       = var.stage
  name        = var.name
  delimiter   = var.delimiter
  attributes  = var.attributes
  tags        = var.tags
}

# ----------------------------------------------------------------------------------------------------------------------
# CREATE THE CERTIFICATE
# ----------------------------------------------------------------------------------------------------------------------

resource "aws_acm_certificate" "cert" {
  count = var.enabled ? 1 : 0

  provider          = aws.virginia
  domain_name       = "*.${var.domain_name}"
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }

  tags = var.tags
}

resource "aws_route53_record" "cert_validation" {
  for_each = {
    for option in aws_acm_certificate.cert[0].domain_validation_options : option.domain_name => {
      name   = option.resource_record_name
      type   = option.resource_record_type
      record = option.resource_record_value
    }
  }

  name     = each.value.name
  provider = aws.virginia
  records  = [each.value.record]
  ttl      = 60
  type     = each.value.type
  zone_id  = var.route_53_zone_id
}

resource "aws_acm_certificate_validation" "cert" {
  count = var.enabled ? 1 : 0

  provider                = aws.virginia
  certificate_arn         = aws_acm_certificate.cert[0].arn
  validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}

# ----------------------------------------------------------------------------------------------------------------------
# CREATE THE BUCKET
# ----------------------------------------------------------------------------------------------------------------------

resource "aws_s3_bucket" "default" {
  count = var.enabled ? 1 : 0

  provider      = aws.virginia
  bucket        = var.domain_name
  force_destroy = var.force_destroy

  lifecycle {
    ignore_changes = [
      server_side_encryption_configuration,
      lifecycle_rule,
    ]
  }

  tags = module.label.tags
}

resource "aws_s3_bucket_ownership_controls" "default" {
  count = var.enabled ? 1 : 0

  bucket   = aws_s3_bucket.default[0].id
  provider = aws.virginia

  rule {
    object_ownership = "BucketOwnerPreferred"
  }
}

resource "aws_s3_bucket_acl" "default" {
  count      = var.enabled ? 1 : 0
  depends_on = [aws_s3_bucket_ownership_controls.default[0]]

  bucket   = aws_s3_bucket.default[0].id
  provider = aws.virginia
  acl      = "private"
}

resource "aws_s3_bucket_server_side_encryption_configuration" "default" {
  count = var.enabled ? 1 : 0

  bucket   = aws_s3_bucket.default[0].id
  provider = aws.virginia

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = var.sse_algorithm
      kms_master_key_id = var.kms_master_key_arn
    }
  }
}

resource "aws_s3_bucket_versioning" "default" {
  count = var.enabled ? 1 : 0

  bucket   = aws_s3_bucket.default[0].id
  provider = aws.virginia
  versioning_configuration {
    status = var.versioning_enabled
  }
}

resource "aws_s3_bucket_lifecycle_configuration" "default" {
  count = var.enabled ? 1 : 0

  bucket   = aws_s3_bucket.default[0].id
  provider = aws.virginia

  rule {
    id     = module.label.id
    status = var.lifecycle_rule_enabled

    noncurrent_version_transition {
      noncurrent_days = var.noncurrent_version_expiration_days
      storage_class   = "STANDARD_IA"
    }

    expiration {
      days = var.expiration_days
    }
  }
}

resource "aws_s3_bucket_policy" "default" {
  count = var.enabled ? 1 : 0

  provider = aws.virginia
  bucket   = aws_s3_bucket.default[0].id
  policy   = <<POLICY
{
  "Version": "2008-10-17",
  "Id": "${module.label.id}_cloudfront_access",
  "Statement": [
    {
      "Sid": "cloudfront_access",
      "Effect": "Allow",
      "Principal": {
        "AWS": "${aws_cloudfront_origin_access_identity.origin_access_identity[0].iam_arn}"
      },
      "Action": "s3:GetObject",
      "Resource": "${aws_s3_bucket.default[0].arn}/*"
    }
  ]
}
POLICY
}

resource "aws_s3_bucket_public_access_block" "block_public_access" {
  count = var.enabled ? 1 : 0

  provider                = aws.virginia
  bucket                  = aws_s3_bucket.default[0].id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# ----------------------------------------------------------------------------------------------------------------------
# CREATE EDGE LAMBDA FUNCTIONS
# Used for rewriting headers to use a specific subdirectory within the target bucket for a given subdomain.
# ----------------------------------------------------------------------------------------------------------------------

resource "aws_iam_role" "iam_for_lambda" {
  name = "${module.label.id}_iam_for_lambda"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": [
          "lambda.amazonaws.com",
          "edgelambda.amazonaws.com"
        ]
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

data "aws_iam_policy" "edge_execution" {
  arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy_attachment" "edge_execution" {
  role       = aws_iam_role.iam_for_lambda.name
  policy_arn = data.aws_iam_policy.edge_execution.arn
}

data "archive_file" "lambda_origin_request_zip_file" {
  type        = "zip"
  output_path = "/tmp/${module.label.id}/lambda_origin_request.zip"

  source {
    content  = file("${path.module}/code/origin_request/index.js")
    filename = "index.js"
  }
}

resource "aws_lambda_function" "lambda_origin_request" {
  count    = var.enabled ? 1 : 0
  provider = aws.virginia

  role             = aws_iam_role.iam_for_lambda.arn
  function_name    = "${module.label.id}_lambda_origin_request"
  filename         = data.archive_file.lambda_origin_request_zip_file.output_path
  source_code_hash = data.archive_file.lambda_origin_request_zip_file.output_base64sha256
  runtime          = "nodejs18.x"
  handler          = "index.handler"
  publish          = true
}

data "archive_file" "lambda_viewer_request_zip_file" {
  type        = "zip"
  output_path = "/tmp/${module.label.id}/lambda_viewer_request.zip"

  source {
    content  = file("${path.module}/code/viewer_request/index.js")
    filename = "index.js"
  }
}

resource "aws_lambda_function" "lambda_viewer_request" {
  count    = var.enabled ? 1 : 0
  provider = aws.virginia

  role             = aws_iam_role.iam_for_lambda.arn
  function_name    = "${module.label.id}_lambda_viewer_request"
  filename         = data.archive_file.lambda_viewer_request_zip_file.output_path
  source_code_hash = data.archive_file.lambda_viewer_request_zip_file.output_base64sha256
  runtime          = "nodejs18.x"
  handler          = "index.handler"
  publish          = true
}

# ----------------------------------------------------------------------------------------------------------------------
# CREATE THE CLOUDFRONT DISTRIBUTION
# ----------------------------------------------------------------------------------------------------------------------

locals {
  s3_origin_id = "S3-${var.domain_name}"
}

resource "aws_cloudfront_origin_access_identity" "origin_access_identity" {
  count = var.enabled ? 1 : 0
}

resource "aws_cloudfront_distribution" "s3_distribution" {
  count = var.enabled ? 1 : 0

  origin {
    domain_name = "${var.domain_name}.s3.amazonaws.com"
    origin_id   = local.s3_origin_id

    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.origin_access_identity[0].cloudfront_access_identity_path
    }
  }

  enabled             = true
  is_ipv6_enabled     = true
  comment             = "Managed by Terraform"
  default_root_object = "index.html"

  aliases = ["*.${var.domain_name}"]

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = local.s3_origin_id

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }

      headers = ["X-Original-Host"]
    }

    lambda_function_association {
      event_type   = "origin-request"
      include_body = "false"
      lambda_arn   = aws_lambda_function.lambda_origin_request[0].qualified_arn
    }

    lambda_function_association {
      event_type   = "viewer-request"
      include_body = "false"
      lambda_arn   = aws_lambda_function.lambda_viewer_request[0].qualified_arn
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 60
    max_ttl                = 60
  }

  price_class = var.cloudfront_price_class

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn            = aws_acm_certificate_validation.cert[0].certificate_arn
    cloudfront_default_certificate = false
    minimum_protocol_version       = "TLSv1.2_2021"
    ssl_support_method             = "sni-only"
  }
}

# ----------------------------------------------------------------------------------------------------------------------
# CREATE THE DNS ENTRIES
# ----------------------------------------------------------------------------------------------------------------------

resource "aws_route53_record" "a_record" {
  count = var.enabled ? 1 : 0

  zone_id = var.route_53_zone_id
  name    = "*.${var.domain_name}"
  type    = "A"

  alias {
    evaluate_target_health = false
    name                   = aws_cloudfront_distribution.s3_distribution[0].domain_name
    zone_id                = aws_cloudfront_distribution.s3_distribution[0].hosted_zone_id
  }
}

resource "aws_route53_record" "c_record" {
  count = var.enabled ? 1 : 0

  zone_id = var.route_53_zone_id
  name    = "www.*.${var.domain_name}"
  type    = "CNAME"
  ttl     = "60"
  records = ["*.${var.domain_name}"]
}