Compare commits

...

14 Commits

Author SHA1 Message Date
9648a0b161 todo: add todos 2025-09-18 13:30:51 -05:00
a9f8183309 Merge pull request 'Purely documentation and typing, let's try' (#2) from dev into master
All checks were successful
Terraform validate and apply / terraform (ubuntu-latest) (push) Successful in 2m13s
Reviewed-on: #2
2025-09-18 16:23:41 +00:00
1381635467 Merge branch 'master' into dev
All checks were successful
Terraform validate and apply / terraform (ubuntu-latest) (pull_request) Successful in 3m16s
2025-09-18 11:19:50 -05:00
d831f89ad8 doc: add notes to readme
All checks were successful
Terraform validate and apply / terraform (ubuntu-latest) (pull_request) Successful in 2m36s
2025-09-18 11:17:46 -05:00
579c73b128 todo: mark some done 2025-09-18 10:50:46 -05:00
f2b9fd4c11 fmt: add type to domains 2025-09-18 10:50:04 -05:00
64c297a905 Merge pull request 'modularise?' (#1) from module into master
All checks were successful
Terraform validate and apply / terraform (ubuntu-latest) (push) Successful in 7m30s
Reviewed-on: #1
2025-09-18 15:48:39 +00:00
39c02779a9 fix: set env var for multiple domains
All checks were successful
Terraform validate and apply / terraform (ubuntu-latest) (pull_request) Successful in 1m45s
2025-09-18 10:34:45 -05:00
daf180c210 fmt: formatting update
Some checks failed
Terraform validate and apply / terraform (ubuntu-latest) (pull_request) Failing after 1m42s
2025-09-18 10:28:01 -05:00
ba2e1e4655 also run plan for PRs
Some checks failed
Terraform validate and apply / terraform (ubuntu-latest) (pull_request) Failing after 5m27s
2025-09-18 10:03:48 -05:00
615fc2a24f modules? 2025-09-18 09:59:37 -05:00
6e106657ea modularise? 2025-09-18 01:10:57 -05:00
4e913599cb doc: change example vars
All checks were successful
Terraform validate and apply / nix (ubuntu-latest) (push) Successful in 3m7s
2025-09-17 15:58:44 -05:00
043c379647 adding some refactor and example values 2025-09-17 15:58:11 -05:00
11 changed files with 347 additions and 131 deletions

View File

@@ -4,15 +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:
@@ -46,3 +54,7 @@ jobs:
- name: Plan
id: plan
run: terraform plan -no-color -input=false
- name: Apply
id: apply
run: terraform apply -auto-approve -no-color -input=false
if: github.ref == 'refs/heads/master' && github.event_name == 'push'

View File

@@ -5,11 +5,32 @@ hosting hruday.me via terraform
---
Add `dotenv` to .envrc after other nix stuff, and store keys in .env, which is fine for a testing project.
~~Add `dotenv` to .envrc after other nix stuff, and store keys in .env, which is fine for a testing project.~~
Don't add dotenv.
Workflow is to just use the `dev` branch or anything else, then only actually deploy via PR to `master`.
PR to master is a great deployment strategy, no notes.
Currently manages hruday.me and deepakmallubhotla.com, creating buckets which match the domain names.
The content of the sites are managed externally, in their own repos which deploy by uploading to the S3 bucket created here.
## adding a domain
Not an ideal process, so we should improve.
1. Acquire domain name, manually atm.
2. let Cloudflare manage DNS by setting nameservers (following the wizard in cf works with no DNS records required before we get here!) etc., also manual
3. Add domain name to relevant Gitea variable, should be easy.
4. Bucket will be created, empty. If you want an easy start you can manually upload to the bucket.
5. Deploy with whatever method you want, can include a build process or anything else. Follow hruday.me as a guide maybe
## Todos
- [ ] better secrets management
- [x] better secrets management
- [x] ci
- [ ] test ci permissions with a real terraform apply (not in ci)
- [x] test ci permissions with a real terraform apply (not in ci)
- [ ] can we make a lower-weight runner? ubuntu-latest is heavy and still requires ~1m for providers
- [ ] For new domain should provide a default set of content in the bucket? or does that cost more for the extra creates, for a local project we may not care
- [ ] in ci our terraform plan steps should output a file, which could get manually reviewed (add to PR as comment)
- [ ] create workflow for drift detection

123
main.tf
View File

@@ -1,124 +1,19 @@
provider "aws" {
region = var.aws_region
assume_role {
role_arn = "arn:aws:iam::677425296084:role/tuffas-applier"
role_arn = var.tuffas_applier_role_arn
}
}
provider "cloudflare" {
}
# 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
}
# 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
]
}
# Full permissions for the bucket, which allows anyone to access any object in the bucket
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,
"${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
}
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
}

View 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
}
}

View 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
}

View 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."
}
}

View 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
View 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"]
}

View File

@@ -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 }
}

17
terraform.tfvars.example Normal file
View File

@@ -0,0 +1,17 @@
# Example Terraform variables file
# Copy this file to terraform.tfvars and update with your actual values
# AWS region where resources will be created
aws_region = "us-east-2"
# Your domain name (must be managed by Cloudflare)
site_domain = "example.com"
# Project name for resource tagging
project_name = "tuffas"
# Environment (dev, staging, prod)
environment = "prod"
# IAM role ARNs (update with your actual role ARNs)
tuffas_applier_role_arn = "arn:aws:iam::YOUR-ACCOUNT-ID:role/ROLE_NAME"

View File

@@ -1,9 +1,31 @@
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(string)
description = "The domain name of these sites, which will be mapped over"
}
variable "tuffas_applier_role_arn" {
type = string
description = "IAM role ARN for Terraform to assume when applying changes"
}
variable "project_name" {
type = string
description = "Name of the project for resource tagging"
default = "tuffas"
}
# future proofing
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."
}
}