diff --git a/.gitea/workflows/apply-master.yaml b/.gitea/workflows/apply-master.yaml index 97f21b8..e72b735 100644 --- a/.gitea/workflows/apply-master.yaml +++ b/.gitea/workflows/apply-master.yaml @@ -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: diff --git a/main.tf b/main.tf index 2456b18..c1dd3a2 100644 --- a/main.tf +++ b/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 } diff --git a/modules/static-website/main.tf b/modules/static-website/main.tf new file mode 100644 index 0000000..a2a0683 --- /dev/null +++ b/modules/static-website/main.tf @@ -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 + } +} \ No newline at end of file diff --git a/modules/static-website/outputs.tf b/modules/static-website/outputs.tf new file mode 100644 index 0000000..03f6b2b --- /dev/null +++ b/modules/static-website/outputs.tf @@ -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 +} \ No newline at end of file diff --git a/modules/static-website/variables.tf b/modules/static-website/variables.tf new file mode 100644 index 0000000..520a566 --- /dev/null +++ b/modules/static-website/variables.tf @@ -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." + } +} \ No newline at end of file diff --git a/modules/static-website/versions.tf b/modules/static-website/versions.tf new file mode 100644 index 0000000..b169744 --- /dev/null +++ b/modules/static-website/versions.tf @@ -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" +} \ No newline at end of file diff --git a/moved.tf b/moved.tf new file mode 100644 index 0000000..3c57188 --- /dev/null +++ b/moved.tf @@ -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"] +} diff --git a/outputs.tf b/outputs.tf index 36a32f8..d92b015 100644 --- a/outputs.tf +++ b/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 } } diff --git a/variables.tf b/variables.tf index f016494..104a327 100644 --- a/variables.tf +++ b/variables.tf @@ -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)"