Skip to content

Commit a247785

Browse files
Create static website module (#1)
1 parent c8d8c2a commit a247785

5 files changed

Lines changed: 375 additions & 1 deletion

File tree

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 DashDevs LLC
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,58 @@
1-
# terraform-aws-static-website
1+
# terraform-aws-static-website
2+
3+
4+
## Usage
5+
6+
7+
**IMPORTANT:** We do not pin modules to versions in our examples because of the
8+
difficulty of keeping the versions in the documentation in sync with the latest released versions.
9+
We highly recommend that in your code you pin the version to the exact version you are
10+
using so that your infrastructure remains stable, and update versions in a
11+
systematic way so that they do not catch you by surprise.
12+
13+
### example usage for website module:
14+
```
15+
module "website" {
16+
source = "dashdevs/static-website/aws"
17+
domain = var.domain
18+
domain_zone_name = var.domain_zone_name
19+
create_dns_records = true
20+
}
21+
22+
```
23+
24+
25+
<!-- markdownlint-restore -->
26+
<!-- markdownlint-disable -->
27+
## Requirements
28+
29+
| Name | Version |
30+
|------|---------|
31+
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.5.2 |
32+
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 3.34 |
33+
34+
## Providers
35+
36+
| Name | Version |
37+
|------|---------|
38+
| <a name="provider_aws"></a> [aws](#provider\_aws) | >= 3.34 |
39+
40+
41+
## Inputs
42+
43+
| Name | Description | Type | Default | Required |
44+
|------|-------------|------|---------|:--------:|
45+
| <a name="input_domain"></a> [domain](#input\_domain) | Domain name for the site | `string` | `n/a` | yes |
46+
| <a name="input_domain_zone_name"></a> [domain\_zone\_name](#input\_domain\_zone\_name) | The name of the domain zone in the route53 service for which DNS records will be created. Must be set if create_dns_records is `true` | `string` | `null` | no |
47+
| <a name="input_create_dns_records"></a> [create\_dns\_records](#input\_create\_dns\_records) | If true, then DNS records are created in route53 for this site and connected to the cloudfront distribution | `bool` |`true`| no |
48+
| <a name="input_cors_allowed_origins"></a> [cors\_allowed\_origins](#input\_cors\_allowed\_origins) | Used to declare domains from which the site will be accessed as a storage of static resources | `null` |`list(string)`| no |
49+
50+
51+
## Outputs
52+
53+
| Name | Description |
54+
|------|-------------|
55+
| <a name="output_bucket_id"></a> [bucket\_id](#output\_bucket\_id) | The S3 bucket identifier |
56+
| <a name="output_cloudfront_distribution_id"></a> [cloudfront\_distribution\_id](#output\_cloudfront\_distribution\_id) | The cloudfront distribution identifier assigned to the S3 bucket |
57+
| <a name="output_ssl_certificate_validation_dns_records"></a> [ssl\_certificate\_validation\_dns\_records](#output\_ssl\_certificate\_validation\_dns\_records) | List of text expressions of the certificate validation DNS records to create this records manually. Required if [create\_dns\_records](#input\_create\_dns\_records) is `false` |
58+
| <a name="output_resource_domain_record"></a> [resource\_domain\_record](#output\_resource\_domain\_record) | Text expressions of the website DNS record to create this records manually. Required if [create\_dns\_records](#input\_create\_dns\_records) is `false` |

main.tf

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/**
2+
* Config
3+
*
4+
**/
5+
6+
locals {
7+
s3_origin_id = "websiteorigin"
8+
s3_root_object = "index.html"
9+
сreate_cors_configuration = var.cors_allowed_origins != null ? true : false
10+
}
11+
12+
check "application_repository_validation" {
13+
assert {
14+
condition = !(var.create_dns_records && var.domain_zone_name == null)
15+
error_message = "If create_dns_records is true then domain_zone_name must be set!"
16+
}
17+
}
18+
19+
data "aws_route53_zone" "public_zone" {
20+
count = var.create_dns_records ? 1 : 0
21+
name = var.domain_zone_name
22+
private_zone = false
23+
}
24+
25+
26+
/**
27+
* Certificate
28+
*
29+
**/
30+
31+
# To use an ACM certificate with CloudFront, make sure you request (or import) the certificate in the US East (N. Virginia)
32+
provider "aws" {
33+
alias = "virginia"
34+
region = "us-east-1"
35+
}
36+
37+
resource "aws_acm_certificate" "website" {
38+
provider = aws.virginia
39+
40+
domain_name = var.domain
41+
validation_method = "DNS"
42+
}
43+
44+
resource "aws_route53_record" "website_certificate_validation_records" {
45+
provider = aws.virginia
46+
47+
for_each = {
48+
for dvo in var.create_dns_records ? aws_acm_certificate.cerificate.domain_validation_options : [] : dvo.domain_name => {
49+
name = dvo.resource_record_name
50+
record = dvo.resource_record_value
51+
type = dvo.resource_record_type
52+
}
53+
}
54+
55+
allow_overwrite = true
56+
name = each.value.name
57+
records = [each.value.record]
58+
ttl = 60
59+
type = each.value.type
60+
zone_id = data.aws_route53_zone.public_zone[0].zone_id
61+
}
62+
63+
resource "aws_acm_certificate_validation" "website_certificate_validation" {
64+
provider = aws.virginia
65+
66+
certificate_arn = aws_acm_certificate.website.arn
67+
validation_record_fqdns = [for record in aws_route53_record.website_certificate_validation_records : record.fqdn]
68+
}
69+
70+
71+
/**
72+
* DNS Record
73+
*
74+
**/
75+
76+
resource "aws_route53_record" "a" {
77+
count = var.create_dns_records ? 1 : 0
78+
zone_id = data.aws_route53_zone.public_zone[0].zone_id
79+
name = var.domain
80+
type = "A"
81+
82+
alias {
83+
name = aws_cloudfront_distribution.website.domain_name
84+
zone_id = aws_cloudfront_distribution.website.hosted_zone_id
85+
evaluate_target_health = false
86+
}
87+
}
88+
89+
90+
/**
91+
* S3 Bucket
92+
*
93+
**/
94+
95+
resource "aws_s3_bucket" "website" {
96+
bucket = var.domain
97+
}
98+
99+
resource "aws_s3_bucket_cors_configuration" "website" {
100+
count = local.сreate_cors_configuration ? 1 : 0
101+
bucket = aws_s3_bucket.website.id
102+
103+
cors_rule {
104+
allowed_headers = ["*"]
105+
allowed_methods = ["GET", "HEAD"]
106+
allowed_origins = var.cors_allowed_origins
107+
max_age_seconds = 3600
108+
}
109+
}
110+
111+
resource "aws_s3_bucket_website_configuration" "website" {
112+
bucket = aws_s3_bucket.website.id
113+
114+
index_document { suffix = local.s3_root_object }
115+
error_document { key = local.s3_root_object }
116+
}
117+
118+
119+
/**
120+
* CloudFront Distribution
121+
*
122+
**/
123+
124+
resource "aws_cloudfront_origin_access_identity" "website" {
125+
comment = var.domain
126+
}
127+
128+
resource "aws_cloudfront_distribution" "website" {
129+
enabled = true
130+
aliases = [var.domain]
131+
comment = var.domain
132+
default_root_object = local.s3_root_object
133+
price_class = "PriceClass_All"
134+
135+
origin {
136+
domain_name = aws_s3_bucket.website.bucket_domain_name
137+
origin_id = local.s3_origin_id
138+
139+
s3_origin_config {
140+
origin_access_identity = aws_cloudfront_origin_access_identity.website.cloudfront_access_identity_path
141+
}
142+
}
143+
144+
custom_error_response {
145+
error_caching_min_ttl = 86400
146+
error_code = 404
147+
response_code = 200
148+
response_page_path = "/${local.s3_root_object}"
149+
}
150+
151+
custom_error_response {
152+
error_caching_min_ttl = 86400
153+
error_code = 403
154+
response_code = 200
155+
response_page_path = "/${local.s3_root_object}"
156+
}
157+
158+
default_cache_behavior {
159+
allowed_methods = ["GET", "HEAD", "OPTIONS"]
160+
cached_methods = ["GET", "HEAD", "OPTIONS"]
161+
target_origin_id = local.s3_origin_id
162+
163+
forwarded_values {
164+
query_string = false
165+
cookies {
166+
forward = "none"
167+
}
168+
}
169+
170+
viewer_protocol_policy = "redirect-to-https"
171+
min_ttl = 60
172+
default_ttl = 3600
173+
max_ttl = 86400
174+
175+
response_headers_policy_id = aws_cloudfront_response_headers_policy.website_security.id
176+
}
177+
178+
restrictions {
179+
geo_restriction {
180+
restriction_type = "none"
181+
}
182+
}
183+
184+
viewer_certificate {
185+
acm_certificate_arn = aws_acm_certificate.website.arn
186+
cloudfront_default_certificate = false
187+
minimum_protocol_version = "TLSv1.2_2021"
188+
ssl_support_method = "sni-only"
189+
}
190+
}
191+
192+
resource "aws_cloudfront_response_headers_policy" "website_security" {
193+
name = replace(var.domain, ".", "-")
194+
195+
custom_headers_config {
196+
items {
197+
header = "X-Permitted-Cross-Domain-Policies"
198+
value = "none"
199+
override = true
200+
}
201+
items {
202+
header = "Feature-Policy"
203+
value = "camera 'none'; fullscreen 'self'; geolocation *; microphone 'self' https://${var.domain}/*"
204+
override = true
205+
}
206+
}
207+
208+
security_headers_config {
209+
content_security_policy {
210+
content_security_policy = "default-src * blob: data:; script-src https: 'unsafe-inline' 'unsafe-eval'; style-src https: 'unsafe-inline'"
211+
override = true
212+
}
213+
content_type_options {
214+
override = true
215+
}
216+
referrer_policy {
217+
referrer_policy = "no-referrer-when-downgrade"
218+
override = true
219+
}
220+
frame_options {
221+
frame_option = "SAMEORIGIN"
222+
override = true
223+
}
224+
strict_transport_security {
225+
access_control_max_age_sec = "31536000"
226+
include_subdomains = true
227+
preload = true
228+
override = true
229+
}
230+
xss_protection {
231+
mode_block = true
232+
protection = true
233+
override = true
234+
}
235+
}
236+
}
237+
238+
239+
/**
240+
* S3 Bucket IAM
241+
*
242+
**/
243+
244+
data "aws_iam_policy_document" "allow_website_cloudfront" {
245+
statement {
246+
sid = "Allow bucket access from CloudFront"
247+
248+
principals {
249+
type = "AWS"
250+
identifiers = [aws_cloudfront_origin_access_identity.website.iam_arn]
251+
}
252+
253+
actions = ["s3:GetObject"]
254+
resources = ["${aws_s3_bucket.website.arn}/*"]
255+
}
256+
}
257+
258+
resource "aws_s3_bucket_policy" "allow_cloudfront" {
259+
bucket = aws_s3_bucket.website.id
260+
policy = data.aws_iam_policy_document.allow_website_cloudfront.json
261+
}

outputs.tf

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
output "bucket_id" {
2+
value = aws_s3_bucket.website.id
3+
}
4+
5+
output "cloudfront_distribution_id" {
6+
value = aws_cloudfront_distribution.website.id
7+
}
8+
9+
output "resource_domain_record" {
10+
value = "${var.domain} CNAME ${aws_cloudfront_distribution.website.domain_name}"
11+
}
12+
13+
output "certificate_validation_records" {
14+
value = [
15+
for dvo in aws_acm_certificate.website.domain_validation_options : "${dvo.resource_record_name} ${dvo.resource_record_type} ${dvo.resource_record_value}"
16+
]
17+
}

variables.tf

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
variable "domain" {
2+
type = string
3+
}
4+
5+
variable "domain_zone_name" {
6+
type = string
7+
default = null
8+
}
9+
10+
variable "create_dns_records" {
11+
type = bool
12+
default = true
13+
}
14+
15+
variable "cors_allowed_origins" {
16+
type = list(string)
17+
default = null
18+
}

0 commit comments

Comments
 (0)