Skip to content

Commit

Permalink
Easily Deploy a CDN Backend by S3 (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
osterman authored and const-bon committed Sep 20, 2017
1 parent a72889d commit 77a593b
Show file tree
Hide file tree
Showing 6 changed files with 472 additions and 3 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Compiled files
*.tfstate
*.tfstate.backup
*.terraform.tfstate*
# Module directory
.terraform/

.idea
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright {yyyy} {name of copyright owner}
Copyright 2017 Cloud Posse, LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
93 changes: 91 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,91 @@
# tf_cdn
Terraform module to easily provision CloudFront CDN with an S3 or custom origin
# tf_cdn_s3

Terraform module to easily provision CloudFront CDN with an S3 or custom origin.

## Usage

module "cdn_s3" {
source = "git::https://github.com/cloudposse/tf_cdn_s3.git?ref=master"
namespace = "${var.namespace}"
stage = "${var.stage}"
name = "${var.name}"
aliases = "${var.hostname}"
parent_zone_name = "${var.parent_zone_name}"
}

### Generating ACM Certificate

Use the AWS cli to [request new ACM certifiates](http://docs.aws.amazon.com/acm/latest/userguide/gs-acm-request.html) (requires email validation)
```
aws acm request-certificate --domain-name example.com --subject-alternative-names a.example.com b.example.com *.c.example.com
```


## Variables

| Name | Default | Description | Required |
|:-------------------------------|:----------------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|
| `namespace` | `` | Namespace (e.g. `cp` or `cloudposse`) | Yes |
| `stage` | `` | Stage (e.g. `prod`, `dev`, `staging`) | Yes |
| `name` | `` | Name (e.g. `bastion` or `db`) | Yes |
| `attributes` | `[]` | Additional attributes (e.g. `policy` or `role`) | No |
| `tags` | `{}` | Additional tags (e.g. `map("BusinessUnit","XYZ")` | No |
| `acm_certificate_arn` | `` | Existing ACM Certificate ARN | No |
| `aliases` | `[]` | List of aliases | Yes |
| `allowed_methods` | `["*"]` | List of allowed methods (e.g. ` GET, PUT, POST, DELETE, HEAD`) for AWS CloudFront | No |
| `cached_methods` | `["GET", "HEAD"]` | List of cached methods (e.g. ` GET, PUT, POST, DELETE, HEAD`) | No |
| `comment` | `Managed by Terraform` | Comment for the origin access identity | No |
| `compress` | `false` | Compress content for web requests that include Accept-Encoding: gzip in the request header | No |
| `cors_allowed_headers` | `["*"]` | List of allowed headers for S3 bucket | No |
| `cors_allowed_methods` | `["GET"]` | List of allowed methods (e.g. ` GET, PUT, POST, DELETE, HEAD`) for S3 bucket | No |
| `cors_allowed_origins` | `["*"]` | List of allowed origins (e.g. ` example.com, test.com`) for S3 bucket | No |
| `cors_max_age_seconds` | `3600` | Time in seconds that browser can cache the response for S3 bucket | No |
| `cors_expose_headers` | `["ETag"]` | List of expose header in the response for S3 bucket | No |
| `bucket_domain_format` | `%s.s3.amazonaws.com` | Format of bucket domain name | No |
| `default_root_object` | `index.html` | Object that CloudFront return when requests the root URL | No |
| `enabled` | `true` | State of CloudFront | No |
| `forward_cookies` | `none` | Forward cookies to the origin that is associated with this cache behavior | No |
| `forward_query_string` | `false` | Forward query strings to the origin that is associated with this cache behavior | No |
| `geo_restriction_locations` | `[]` | List of country codes for which CloudFront either to distribute content (whitelist) or not distribute your content (blacklist) | No |
| `geo_restriction_type` | `none` | Method that use to restrict distribution of your content by country: `none`, `whitelist`, or `blacklist` | No |
| `is_ipv6_enabled` | `true` | State of CloudFront IPv6 | No |
| `log_standard_transition_days` | `30` | Number of days to persist in the standard storage tier before moving to the glacier tier | No |
| `log_glacier_transition_days` | `60` | Number of days to persist in the standard storage tier before moving to the infrequent access | No |
| `log_expiration_days` | `90` | Number of days after which to expunge the objects | No |
| `log_include_cookies` | `false` | Include cookies in access logs | No |
| `log_prefix` | `` | Path of logs in S3 bucket | No |
| `min_ttl` | `0` | Minimum amount of time that you want objects to stay in CloudFront caches | No |
| `default_ttl` | `60` | Default amount of time (in seconds) that an object is in a CloudFront cache | No |
| `max_ttl` | `31536000` | Maximum amount of time (in seconds) that an object is in a CloudFront cache | No |
| `null` | `` | Empty string | No |
| `price_class` | `PriceClass_100` | Price class for this distribution: `PriceClass_All`, `PriceClass_200`, `PriceClass_100` | No |
| `viewer_protocol_policy` | `redirect-to-https` | Element to specify the protocol: `allow-all`, `https-only`, `redirect-to-https` | No |
| `null` | `` | Empty string | No |
| `origin_force_destroy` | `` | Delete all objects from the bucket so that the bucket can be destroyed without error (e.g. `true` or `false`) | No |
| `origin_bucket` | `` | Name of S3 bucket | No |
| `origin_path` | `` | Element that causes CloudFront to request your content from a directory in your Amazon S3 bucket. Begins with `/`. CAUTION! Do not use bare `/` as `origin_path`. | No |
| `parent_zone_id` | `` | ID of the hosted zone to contain this record (or specify `parent_zone_name`) | Yes |
| `parent_zone_name` | `` | Name of the hosted zone to contain this record (or specify `parent_zone_id`) | Yes |


## Outputs

| Name | Description |
|:------------------------|:--------------------------------------------------|
| `cf_arn` | ID of AWS CloudFront distribution |
| `cf_domain_name` | Domain name corresponding to the distribution |
| `cf_etag` | Current version of the distribution's information |
| `cf_hosted_zone_id` | CloudFront Route 53 zone ID |
| `cf_id` | ID of AWS CloudFront distribution |
| `cf_status` | Current status of the distribution |
| `s3_bucket` | Name of S3 bucket |
| `s3_bucket_domain_name` | Domain of S3 bucket |


## Known Issues

If the bucket is created in a region other than `us-east-1`, it will take a while for the distribution to become fully operational.

> All buckets have at least two REST endpoint hostnames. In eu-west-1, they are example-bucket.s3-eu-west-1.amazonaws.com and example-bucket.s3.amazonaws.com. The first one will be immediately valid when the bucket is created. The second one -- sometimes referred to as the "global endpoint" -- which is the one CloudFront uses -- will not, unless the bucket is in us-east-1. Over a period of seconds to minutes, variable by location and other factors, it becomes globally accessible as well. Before that, the 307 redirect is returned. Hence, the bucket was not ready.
Via: https://stackoverflow.com/questions/38706424/aws-cloudfront-returns-http-307-when-origin-is-s3-bucket
171 changes: 171 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
module "origin_label" {
source = "git::https://github.com/cloudposse/tf_label.git?ref=tags/0.2.0"
namespace = "${var.namespace}"
stage = "${var.stage}"
name = "${var.name}"
delimiter = "${var.delimiter}"
attributes = ["origin"]
tags = "${var.tags}"
}

resource "aws_cloudfront_origin_access_identity" "default" {
comment = "${module.distribution_label.id}"
}

data "aws_iam_policy_document" "origin" {
statement {
actions = ["s3:GetObject"]
resources = ["arn:aws:s3:::$${bucket_name}$${origin_path}*"]

principals {
type = "AWS"
identifiers = ["${aws_cloudfront_origin_access_identity.default.iam_arn}"]
}
}

statement {
actions = ["s3:ListBucket"]
resources = ["arn:aws:s3:::$${bucket_name}"]

principals {
type = "AWS"
identifiers = ["${aws_cloudfront_origin_access_identity.default.iam_arn}"]
}
}
}

data "template_file" "default" {
template = "${data.aws_iam_policy_document.origin.json}"

vars {
origin_path = "${coalesce(var.origin_path, "/")}"
bucket_name = "${null_resource.default.triggers.bucket}"
}
}

resource "aws_s3_bucket_policy" "default" {
bucket = "${null_resource.default.triggers.bucket}"
policy = "${data.template_file.default.rendered}"
}

resource "aws_s3_bucket" "origin" {
count = "${signum(length(var.origin_bucket)) == 1 ? 0 : 1}"
bucket = "${module.origin_label.id}"
acl = "private"
tags = "${module.origin_label.tags}"
force_destroy = "${var.origin_force_destroy}"

cors_rule {
allowed_headers = "${var.cors_allowed_headers}"
allowed_methods = "${var.cors_allowed_methods}"
allowed_origins = "${sort(distinct(compact(concat(var.cors_allowed_origins, var.aliases))))}"
expose_headers = "${var.cors_expose_headers}"
max_age_seconds = "${var.cors_max_age_seconds}"
}
}

module "logs" {
source = "git::https://github.com/cloudposse/tf_log_storage.git?ref=tags/0.1.0"
namespace = "${var.namespace}"
stage = "${var.stage}"
name = "${var.name}"
delimiter = "${var.delimiter}"
attributes = ["logs"]
tags = "${var.tags}"
prefix = "${var.log_prefix}"
standard_transition_days = "${var.log_standard_transition_days}"
glacier_transition_days = "${var.log_glacier_transition_days}"
expiration_days = "${var.log_expiration_days}"
}

module "distribution_label" {
source = "git::https://github.com/cloudposse/tf_label.git?ref=tags/0.2.0"
namespace = "${var.namespace}"
stage = "${var.stage}"
name = "${var.name}"
delimiter = "${var.delimiter}"
tags = "${var.tags}"
}

resource "null_resource" "default" {
triggers {
bucket = "${element(compact(concat(list(var.origin_bucket), aws_s3_bucket.origin.*.bucket)), 0)}"
bucket_domain_name = "${format(var.bucket_domain_format, element(compact(concat(list(var.origin_bucket), aws_s3_bucket.origin.*.bucket)), 0))}"
}

lifecycle {
create_before_destroy = true
}
}

resource "aws_cloudfront_distribution" "default" {
enabled = "${var.enabled}"
is_ipv6_enabled = "${var.is_ipv6_enabled}"
comment = "${var.comment}"
default_root_object = "${var.default_root_object}"
price_class = "${var.price_class}"
depends_on = ["aws_s3_bucket.origin"]

logging_config = {
include_cookies = "${var.log_include_cookies}"
bucket = "${module.logs.bucket_domain_name}"
prefix = "${var.log_prefix}"
}

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

origin {
domain_name = "${null_resource.default.triggers.bucket_domain_name}"
origin_id = "${module.distribution_label.id}"
origin_path = "${var.origin_path}"

s3_origin_config {
origin_access_identity = "${aws_cloudfront_origin_access_identity.default.cloudfront_access_identity_path}"
}
}

viewer_certificate {
acm_certificate_arn = "${var.acm_certificate_arn}"
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1"
cloudfront_default_certificate = true
}

default_cache_behavior {
allowed_methods = "${var.allowed_methods}"
cached_methods = "${var.cached_methods}"
target_origin_id = "${module.distribution_label.id}"
compress = "${var.compress}"

forwarded_values {
query_string = "${var.forward_query_string}"

cookies {
forward = "${var.forward_cookies}"
}
}

viewer_protocol_policy = "${var.viewer_protocol_policy}"
default_ttl = "${var.default_ttl}"
min_ttl = "${var.min_ttl}"
max_ttl = "${var.max_ttl}"
}

restrictions {
geo_restriction {
restriction_type = "${var.geo_restriction_type}"
locations = "${var.geo_restriction_locations}"
}
}

tags = "${module.distribution_label.tags}"
}

module "dns" {
source = "git::https://github.com/cloudposse/tf_vanity.git?ref=tags/0.2.1"
aliases = "${var.aliases}"
parent_zone_id = "${var.parent_zone_id}"
parent_zone_name = "${var.parent_zone_name}"
target_dns_name = "${aws_cloudfront_distribution.default.domain_name}"
target_zone_id = "${aws_cloudfront_distribution.default.hosted_zone_id}"
}
31 changes: 31 additions & 0 deletions outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
output "cf_id" {
value = "${aws_cloudfront_distribution.default.id}"
}

output "cf_arn" {
value = "${aws_cloudfront_distribution.default.arn}"
}

output "cf_status" {
value = "${aws_cloudfront_distribution.default.status}"
}

output "cf_domain_name" {
value = "${aws_cloudfront_distribution.default.domain_name}"
}

output "cf_etag" {
value = "${aws_cloudfront_distribution.default.etag}"
}

output "cf_hosted_zone_id" {
value = "${aws_cloudfront_distribution.default.hosted_zone_id}"
}

output "s3_bucket" {
value = "${null_resource.default.bucket}"
}

output "s3_bucket_domain_name" {
value = "${null_resource.default.bucket_domain_name}"
}
Loading

0 comments on commit 77a593b

Please sign in to comment.