Compare commits
6 Commits
4e913599cb
...
64c297a905
| Author | SHA1 | Date | |
|---|---|---|---|
| 64c297a905 | |||
|
39c02779a9
|
|||
|
daf180c210
|
|||
|
ba2e1e4655
|
|||
|
615fc2a24f
|
|||
|
6e106657ea
|
@@ -4,19 +4,23 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
TF_VAR_aws_region: ${{ vars.TF_VAR_aws_region }}
|
||||
TF_VAR_site_domain: ${{ vars.TF_VAR_site_domain }}
|
||||
TF_VAR_site_domains: ${{ vars.TF_VAR_site_domains }}
|
||||
TF_VAR_project_name: ${{ vars.TF_VAR_project_name }}
|
||||
TF_VAR_environment: ${{ vars.TF_VAR_environment }}
|
||||
TF_VAR_tuffas_applier_role_arn: ${{ vars.TF_VAR_tuffas_applier_role_arn }}
|
||||
TF_VAR_tfstate_backend_role_arn: ${{ vars.TF_VAR_tfstate_backend_role_arn }}
|
||||
TF_PLUGIN_CACHE_DIR: ${{ github.workspace }}/.terraform.d/plugin-cache
|
||||
jobs:
|
||||
nix:
|
||||
terraform:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
|
||||
206
main.tf
206
main.tf
@@ -8,202 +8,12 @@ provider "aws" {
|
||||
provider "cloudflare" {
|
||||
}
|
||||
|
||||
locals {
|
||||
common_tags = {
|
||||
Project = var.project_name
|
||||
Environment = var.environment
|
||||
ManagedBy = "terraform"
|
||||
Domain = var.site_domain
|
||||
}
|
||||
}
|
||||
|
||||
# block with type resource, uses provider's aws_s3_bucket resource type and names it site
|
||||
resource "aws_s3_bucket" "site" {
|
||||
# presumably
|
||||
bucket = var.site_domain
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
# necessary to allow public users to eventually hit the s3 bucket
|
||||
resource "aws_s3_bucket_public_access_block" "site" {
|
||||
bucket = aws_s3_bucket.site.id
|
||||
|
||||
block_public_acls = false
|
||||
block_public_policy = false
|
||||
ignore_public_acls = false
|
||||
restrict_public_buckets = false
|
||||
}
|
||||
|
||||
# name says it all
|
||||
resource "aws_s3_bucket_website_configuration" "site" {
|
||||
bucket = aws_s3_bucket.site.id
|
||||
|
||||
# Note that this isn't an =, i don't know why
|
||||
index_document {
|
||||
suffix = "index.html"
|
||||
}
|
||||
|
||||
error_document {
|
||||
key = "error.html"
|
||||
}
|
||||
}
|
||||
|
||||
# This controls the ownership of the objects inside the bucket upon upload
|
||||
# If possible, this sets the ownership of objects to the bucket owner
|
||||
resource "aws_s3_bucket_ownership_controls" "site" {
|
||||
bucket = aws_s3_bucket.site.id
|
||||
|
||||
rule {
|
||||
object_ownership = "BucketOwnerPreferred"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_acl" "site" {
|
||||
|
||||
bucket = aws_s3_bucket.site.id
|
||||
|
||||
acl = "public-read"
|
||||
depends_on = [
|
||||
aws_s3_bucket_ownership_controls.site,
|
||||
aws_s3_bucket_public_access_block.site
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
# Public read access for website objects only (not bucket itself)
|
||||
resource "aws_s3_bucket_policy" "site" {
|
||||
bucket = aws_s3_bucket.site.id
|
||||
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Sid = "PublicReadGetObject"
|
||||
Effect = "Allow"
|
||||
Principal = "*"
|
||||
Action = "s3:GetObject"
|
||||
Resource = "${aws_s3_bucket.site.arn}/*"
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
depends_on = [
|
||||
aws_s3_bucket_public_access_block.site
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# Cloudflare time
|
||||
|
||||
# data block is about retrieving data from ext. source, not configuring a resource that lives in state
|
||||
data "cloudflare_zones" "domain" {
|
||||
# why a filter?
|
||||
filter {
|
||||
name = var.site_domain
|
||||
}
|
||||
}
|
||||
|
||||
# DNS setup
|
||||
resource "cloudflare_record" "site_cname" {
|
||||
zone_id = data.cloudflare_zones.domain.zones[0].id
|
||||
name = var.site_domain
|
||||
value = aws_s3_bucket_website_configuration.site.website_endpoint
|
||||
type = "CNAME"
|
||||
ttl = 1
|
||||
proxied = true
|
||||
}
|
||||
|
||||
resource "cloudflare_record" "www" {
|
||||
zone_id = data.cloudflare_zones.domain.zones[0].id
|
||||
name = "www"
|
||||
value = var.site_domain
|
||||
type = "CNAME"
|
||||
ttl = 1
|
||||
proxied = true
|
||||
}
|
||||
|
||||
# S3 bucket versioning for content protection
|
||||
resource "aws_s3_bucket_versioning" "site" {
|
||||
bucket = aws_s3_bucket.site.id
|
||||
versioning_configuration {
|
||||
status = "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
# Access logging bucket for monitoring
|
||||
# resource "aws_s3_bucket" "access_logs" {
|
||||
# bucket = "${var.site_domain}-access-logs"
|
||||
#
|
||||
# tags = merge(local.common_tags, {
|
||||
# Purpose = "access-logs"
|
||||
# })
|
||||
# }
|
||||
|
||||
# resource "aws_s3_bucket_public_access_block" "access_logs" {
|
||||
# bucket = aws_s3_bucket.access_logs.id
|
||||
#
|
||||
# block_public_acls = true
|
||||
# block_public_policy = true
|
||||
# ignore_public_acls = true
|
||||
# restrict_public_buckets = true
|
||||
# }
|
||||
|
||||
# Enable access logging for the main site bucket
|
||||
# resource "aws_s3_bucket_logging" "site" {
|
||||
# bucket = aws_s3_bucket.site.id
|
||||
#
|
||||
# target_bucket = aws_s3_bucket.access_logs.id
|
||||
# target_prefix = "access-logs/"
|
||||
# }
|
||||
|
||||
# Lifecycle rules for cost management
|
||||
resource "aws_s3_bucket_lifecycle_configuration" "site" {
|
||||
bucket = aws_s3_bucket.site.id
|
||||
|
||||
rule {
|
||||
id = "cleanup_old_versions"
|
||||
status = "Enabled"
|
||||
|
||||
noncurrent_version_expiration {
|
||||
noncurrent_days = 90
|
||||
}
|
||||
filter {
|
||||
prefix = ""
|
||||
}
|
||||
}
|
||||
|
||||
rule {
|
||||
id = "cleanup_incomplete_uploads"
|
||||
status = "Enabled"
|
||||
|
||||
abort_incomplete_multipart_upload {
|
||||
days_after_initiation = 7
|
||||
}
|
||||
filter {
|
||||
prefix = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Lifecycle rules for access logs
|
||||
# resource "aws_s3_bucket_lifecycle_configuration" "access_logs" {
|
||||
# bucket = aws_s3_bucket.access_logs.id
|
||||
#
|
||||
# rule {
|
||||
# id = "delete_old_logs"
|
||||
# status = "Enabled"
|
||||
#
|
||||
# expiration {
|
||||
# days = 90
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
resource "cloudflare_page_rule" "https" {
|
||||
zone_id = data.cloudflare_zones.domain.zones[0].id
|
||||
target = "*.${var.site_domain}/*"
|
||||
actions {
|
||||
always_use_https = true
|
||||
}
|
||||
module "static_website" {
|
||||
source = "./modules/static-website"
|
||||
|
||||
# toset to dedupe
|
||||
for_each = toset(var.site_domains)
|
||||
site_domain = each.key
|
||||
project_name = var.project_name
|
||||
environment = var.environment
|
||||
}
|
||||
|
||||
141
modules/static-website/main.tf
Normal file
141
modules/static-website/main.tf
Normal file
@@ -0,0 +1,141 @@
|
||||
locals {
|
||||
common_tags = {
|
||||
Project = var.project_name
|
||||
Environment = var.environment
|
||||
ManagedBy = "terraform"
|
||||
Domain = var.site_domain
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "site" {
|
||||
bucket = var.site_domain
|
||||
|
||||
tags = local.common_tags
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_public_access_block" "site" {
|
||||
bucket = aws_s3_bucket.site.id
|
||||
|
||||
block_public_acls = false
|
||||
block_public_policy = false
|
||||
ignore_public_acls = false
|
||||
restrict_public_buckets = false
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_website_configuration" "site" {
|
||||
bucket = aws_s3_bucket.site.id
|
||||
|
||||
index_document {
|
||||
suffix = "index.html"
|
||||
}
|
||||
|
||||
error_document {
|
||||
key = "error.html"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_ownership_controls" "site" {
|
||||
bucket = aws_s3_bucket.site.id
|
||||
|
||||
rule {
|
||||
object_ownership = "BucketOwnerPreferred"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_acl" "site" {
|
||||
bucket = aws_s3_bucket.site.id
|
||||
|
||||
acl = "public-read"
|
||||
depends_on = [
|
||||
aws_s3_bucket_ownership_controls.site,
|
||||
aws_s3_bucket_public_access_block.site
|
||||
]
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_policy" "site" {
|
||||
bucket = aws_s3_bucket.site.id
|
||||
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Sid = "PublicReadGetObject"
|
||||
Effect = "Allow"
|
||||
Principal = "*"
|
||||
Action = "s3:GetObject"
|
||||
Resource = "${aws_s3_bucket.site.arn}/*"
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
depends_on = [
|
||||
aws_s3_bucket_public_access_block.site
|
||||
]
|
||||
}
|
||||
|
||||
data "cloudflare_zones" "domain" {
|
||||
filter {
|
||||
name = var.site_domain
|
||||
}
|
||||
}
|
||||
|
||||
resource "cloudflare_record" "site_cname" {
|
||||
zone_id = data.cloudflare_zones.domain.zones[0].id
|
||||
name = var.site_domain
|
||||
value = aws_s3_bucket_website_configuration.site.website_endpoint
|
||||
type = "CNAME"
|
||||
ttl = 1
|
||||
proxied = true
|
||||
}
|
||||
|
||||
resource "cloudflare_record" "www" {
|
||||
zone_id = data.cloudflare_zones.domain.zones[0].id
|
||||
name = "www"
|
||||
value = var.site_domain
|
||||
type = "CNAME"
|
||||
ttl = 1
|
||||
proxied = true
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_versioning" "site" {
|
||||
bucket = aws_s3_bucket.site.id
|
||||
versioning_configuration {
|
||||
status = "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_lifecycle_configuration" "site" {
|
||||
bucket = aws_s3_bucket.site.id
|
||||
|
||||
rule {
|
||||
id = "cleanup_old_versions"
|
||||
status = "Enabled"
|
||||
|
||||
noncurrent_version_expiration {
|
||||
noncurrent_days = 90
|
||||
}
|
||||
filter {
|
||||
prefix = ""
|
||||
}
|
||||
}
|
||||
|
||||
rule {
|
||||
id = "cleanup_incomplete_uploads"
|
||||
status = "Enabled"
|
||||
|
||||
abort_incomplete_multipart_upload {
|
||||
days_after_initiation = 7
|
||||
}
|
||||
filter {
|
||||
prefix = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "cloudflare_page_rule" "https" {
|
||||
zone_id = data.cloudflare_zones.domain.zones[0].id
|
||||
target = "*.${var.site_domain}/*"
|
||||
actions {
|
||||
always_use_https = true
|
||||
}
|
||||
}
|
||||
14
modules/static-website/outputs.tf
Normal file
14
modules/static-website/outputs.tf
Normal file
@@ -0,0 +1,14 @@
|
||||
output "website_bucket_name" {
|
||||
description = "Name (id) of the bucket"
|
||||
value = aws_s3_bucket.site.id
|
||||
}
|
||||
|
||||
output "bucket_endpoint" {
|
||||
description = "Bucket endpoint"
|
||||
value = aws_s3_bucket_website_configuration.site.website_endpoint
|
||||
}
|
||||
|
||||
output "domain_name" {
|
||||
description = "Website endpoint"
|
||||
value = var.site_domain
|
||||
}
|
||||
20
modules/static-website/variables.tf
Normal file
20
modules/static-website/variables.tf
Normal file
@@ -0,0 +1,20 @@
|
||||
variable "site_domain" {
|
||||
type = string
|
||||
description = "The domain name of the site"
|
||||
}
|
||||
|
||||
variable "project_name" {
|
||||
type = string
|
||||
description = "Name of the project for resource tagging"
|
||||
default = "tuffas"
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
type = string
|
||||
description = "Environment name (e.g., dev, staging, prod)"
|
||||
|
||||
validation {
|
||||
condition = contains(["dev", "staging", "prod"], var.environment)
|
||||
error_message = "Environment must be one of: dev, staging, prod."
|
||||
}
|
||||
}
|
||||
14
modules/static-website/versions.tf
Normal file
14
modules/static-website/versions.tf
Normal file
@@ -0,0 +1,14 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.92"
|
||||
}
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "~> 2.19.2"
|
||||
}
|
||||
}
|
||||
|
||||
required_version = ">= 1.2"
|
||||
}
|
||||
60
moved.tf
Normal file
60
moved.tf
Normal file
@@ -0,0 +1,60 @@
|
||||
moved {
|
||||
from = aws_s3_bucket.site
|
||||
to = module.static_website.aws_s3_bucket.site
|
||||
}
|
||||
|
||||
moved {
|
||||
from = aws_s3_bucket_acl.site
|
||||
to = module.static_website.aws_s3_bucket_acl.site
|
||||
}
|
||||
|
||||
moved {
|
||||
from = aws_s3_bucket_lifecycle_configuration.site
|
||||
to = module.static_website.aws_s3_bucket_lifecycle_configuration.site
|
||||
}
|
||||
|
||||
moved {
|
||||
from = aws_s3_bucket_ownership_controls.site
|
||||
to = module.static_website.aws_s3_bucket_ownership_controls.site
|
||||
}
|
||||
|
||||
|
||||
moved {
|
||||
from = aws_s3_bucket_policy.site
|
||||
to = module.static_website.aws_s3_bucket_policy.site
|
||||
}
|
||||
|
||||
moved {
|
||||
from = aws_s3_bucket_public_access_block.site
|
||||
to = module.static_website.aws_s3_bucket_public_access_block.site
|
||||
}
|
||||
|
||||
moved {
|
||||
from = aws_s3_bucket_versioning.site
|
||||
to = module.static_website.aws_s3_bucket_versioning.site
|
||||
}
|
||||
|
||||
moved {
|
||||
from = aws_s3_bucket_website_configuration.site
|
||||
to = module.static_website.aws_s3_bucket_website_configuration.site
|
||||
}
|
||||
|
||||
moved {
|
||||
from = cloudflare_record.www
|
||||
to = module.static_website.cloudflare_record.www
|
||||
}
|
||||
|
||||
moved {
|
||||
from = cloudflare_record.site_cname
|
||||
to = module.static_website.cloudflare_record.site_cname
|
||||
}
|
||||
|
||||
moved {
|
||||
from = cloudflare_page_rule.https
|
||||
to = module.static_website.cloudflare_page_rule.https
|
||||
}
|
||||
|
||||
moved {
|
||||
from = module.static_website
|
||||
to = module.static_website["hruday.me"]
|
||||
}
|
||||
18
outputs.tf
18
outputs.tf
@@ -1,14 +1,14 @@
|
||||
output "website_bucket_name" {
|
||||
description = "Name (id) of the bucket"
|
||||
value = aws_s3_bucket.site.id
|
||||
output "website_bucket_names" {
|
||||
description = "Names (ids) of the buckets by domain"
|
||||
value = { for k, v in module.static_website : k => v.website_bucket_name }
|
||||
}
|
||||
|
||||
output "bucket_endpoint" {
|
||||
description = "Bucket endpoint"
|
||||
value = aws_s3_bucket_website_configuration.site.website_endpoint
|
||||
output "bucket_endpoints" {
|
||||
description = "Bucket endpoints by domain"
|
||||
value = { for k, v in module.static_website : k => v.bucket_endpoint }
|
||||
}
|
||||
|
||||
output "domain_name" {
|
||||
description = "Website endpoint"
|
||||
value = var.site_domain
|
||||
output "domain_names" {
|
||||
description = "Website endpoints by domain"
|
||||
value = { for k, v in module.static_website : k => v.domain_name }
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
variable "aws_region" {
|
||||
type = string
|
||||
description = "The AWS region of this site"
|
||||
description = "The AWS region of these sites"
|
||||
}
|
||||
|
||||
variable "site_domain" {
|
||||
type = string
|
||||
description = "The domain name of the site"
|
||||
variable "site_domains" {
|
||||
type = list(any)
|
||||
description = "The domain name of these sites, which will be mapped over"
|
||||
}
|
||||
|
||||
variable "tuffas_applier_role_arn" {
|
||||
@@ -19,6 +19,7 @@ variable "project_name" {
|
||||
default = "tuffas"
|
||||
}
|
||||
|
||||
# future proofing
|
||||
variable "environment" {
|
||||
type = string
|
||||
description = "Environment name (e.g., dev, staging, prod)"
|
||||
|
||||
Reference in New Issue
Block a user