From 0282ab40f3a287ccf8621e067a0e424f7883e6b8 Mon Sep 17 00:00:00 2001 From: Merrino Date: Tue, 3 Feb 2026 16:44:08 -0500 Subject: [PATCH 1/2] feat: add ACM certificate and CloudFront custom domain support - Add us-east-1 provider for ACM (required for CloudFront) - Create ACM certificate with DNS validation when domain_name is set - CloudFront uses ACM cert and domain alias when configured - Output ACM validation records and CloudFront domain for DNS setup --- terraform/acm.tf | 57 +++++++++++++++++++++++++++++++++++++++++ terraform/cloudfront.tf | 21 ++++++++++++--- terraform/main.tf | 10 ++++++++ terraform/outputs.tf | 10 ++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 terraform/acm.tf diff --git a/terraform/acm.tf b/terraform/acm.tf new file mode 100644 index 0000000..832b86b --- /dev/null +++ b/terraform/acm.tf @@ -0,0 +1,57 @@ +# SimpleNotes - ACM Certificate for Custom Domain +# Certificate must be in us-east-1 for CloudFront + +# ============================================ +# ACM Certificate (only if custom domain is set) +# ============================================ + +resource "aws_acm_certificate" "frontend" { + count = var.domain_name != "" ? 1 : 0 + provider = aws.us_east_1 + + domain_name = var.domain_name + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } + + tags = { + Name = "${local.prefix}-frontend-cert" + } +} + +# ============================================ +# DNS Validation Records +# NOTE: You need to add these CNAME records to your DNS provider +# The values are output after terraform apply +# ============================================ + +# Output the validation records for manual DNS setup +output "acm_validation_records" { + description = "DNS records to add for ACM certificate validation" + value = var.domain_name != "" ? { + for dvo in aws_acm_certificate.frontend[0].domain_validation_options : dvo.domain_name => { + name = dvo.resource_record_name + type = dvo.resource_record_type + value = dvo.resource_record_value + } + } : {} +} + +# ============================================ +# Wait for certificate validation +# ============================================ + +resource "aws_acm_certificate_validation" "frontend" { + count = var.domain_name != "" ? 1 : 0 + provider = aws.us_east_1 + + certificate_arn = aws_acm_certificate.frontend[0].arn + + # This will wait for the certificate to be validated + # You must add the DNS records first! + timeouts { + create = "30m" + } +} diff --git a/terraform/cloudfront.tf b/terraform/cloudfront.tf index 9d90eea..c66916f 100644 --- a/terraform/cloudfront.tf +++ b/terraform/cloudfront.tf @@ -24,6 +24,9 @@ resource "aws_cloudfront_distribution" "frontend" { comment = "${local.prefix} frontend distribution" price_class = "PriceClass_100" # North America & Europe only (cheapest) + # Custom domain alias (if set) + aliases = var.domain_name != "" ? [var.domain_name] : [] + # Origin configuration - S3 bucket origin { domain_name = aws_s3_bucket.frontend.bucket_regional_domain_name @@ -113,9 +116,21 @@ resource "aws_cloudfront_distribution" "frontend" { } } - # SSL Certificate - use CloudFront default certificate - viewer_certificate { - cloudfront_default_certificate = true + # SSL Certificate - use ACM certificate if custom domain, otherwise CloudFront default + dynamic "viewer_certificate" { + for_each = var.domain_name != "" ? [1] : [] + content { + acm_certificate_arn = aws_acm_certificate_validation.frontend[0].certificate_arn + ssl_support_method = "sni-only" + minimum_protocol_version = "TLSv1.2_2021" + } + } + + dynamic "viewer_certificate" { + for_each = var.domain_name == "" ? [1] : [] + content { + cloudfront_default_certificate = true + } } tags = { diff --git a/terraform/main.tf b/terraform/main.tf index e857394..fb81a9d 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -34,6 +34,16 @@ provider "aws" { } } +# ACM certificates for CloudFront MUST be in us-east-1 +provider "aws" { + alias = "us_east_1" + region = "us-east-1" + + default_tags { + tags = var.tags + } +} + # Data sources data "aws_caller_identity" "current" {} data "aws_region" "current" {} diff --git a/terraform/outputs.tf b/terraform/outputs.tf index 74812c4..584a5d9 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -50,3 +50,13 @@ output "aws_region" { description = "AWS region" value = var.aws_region } + +output "custom_domain" { + description = "Custom domain name (if configured)" + value = var.domain_name != "" ? var.domain_name : null +} + +output "cloudfront_domain" { + description = "CloudFront distribution domain (for DNS CNAME)" + value = aws_cloudfront_distribution.frontend.domain_name +} From 0795c132b3fc540de29cdb6d7c24af8134173f0d Mon Sep 17 00:00:00 2001 From: Merrino Date: Tue, 3 Feb 2026 16:47:55 -0500 Subject: [PATCH 2/2] refactor: use external ACM certificate ARN (matches babylog pattern) - Remove in-terraform ACM cert creation (was causing DNS validation issues) - Add acm_certificate_arn variable to pass existing cert ARN - Update CI/CD to pass ACM_CERTIFICATE_ARN secret - Simpler: create cert once in AWS, reuse across deploys --- .github/workflows/ci-cd.yml | 4 +++ terraform/acm.tf | 67 ++++++------------------------------- terraform/cloudfront.tf | 21 ++++-------- terraform/main.tf | 10 ------ terraform/outputs.tf | 14 ++++++++ terraform/variables.tf | 8 ++++- 6 files changed, 42 insertions(+), 82 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index e173da1..7777f14 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -161,6 +161,7 @@ jobs: TF_VAR_supabase_url: ${{ secrets.SUPABASE_URL }} TF_VAR_supabase_jwt_secret: ${{ secrets.SUPABASE_JWT_SECRET }} TF_VAR_domain_name: ${{ secrets.DOMAIN_NAME }} + TF_VAR_acm_certificate_arn: ${{ secrets.ACM_CERTIFICATE_ARN }} TF_VAR_environment: staging run: terraform plan -out=tfplan @@ -171,6 +172,7 @@ jobs: TF_VAR_supabase_url: ${{ secrets.SUPABASE_URL }} TF_VAR_supabase_jwt_secret: ${{ secrets.SUPABASE_JWT_SECRET }} TF_VAR_domain_name: ${{ secrets.DOMAIN_NAME }} + TF_VAR_acm_certificate_arn: ${{ secrets.ACM_CERTIFICATE_ARN }} TF_VAR_environment: staging run: terraform apply -auto-approve tfplan @@ -280,6 +282,7 @@ jobs: TF_VAR_supabase_url: ${{ secrets.SUPABASE_URL }} TF_VAR_supabase_jwt_secret: ${{ secrets.SUPABASE_JWT_SECRET }} TF_VAR_domain_name: ${{ secrets.DOMAIN_NAME }} + TF_VAR_acm_certificate_arn: ${{ secrets.ACM_CERTIFICATE_ARN }} TF_VAR_environment: prod run: terraform plan -out=tfplan @@ -290,6 +293,7 @@ jobs: TF_VAR_supabase_url: ${{ secrets.SUPABASE_URL }} TF_VAR_supabase_jwt_secret: ${{ secrets.SUPABASE_JWT_SECRET }} TF_VAR_domain_name: ${{ secrets.DOMAIN_NAME }} + TF_VAR_acm_certificate_arn: ${{ secrets.ACM_CERTIFICATE_ARN }} TF_VAR_environment: prod run: terraform apply -auto-approve tfplan diff --git a/terraform/acm.tf b/terraform/acm.tf index 832b86b..69a6d78 100644 --- a/terraform/acm.tf +++ b/terraform/acm.tf @@ -1,57 +1,12 @@ # SimpleNotes - ACM Certificate for Custom Domain -# Certificate must be in us-east-1 for CloudFront - -# ============================================ -# ACM Certificate (only if custom domain is set) -# ============================================ - -resource "aws_acm_certificate" "frontend" { - count = var.domain_name != "" ? 1 : 0 - provider = aws.us_east_1 - - domain_name = var.domain_name - validation_method = "DNS" - - lifecycle { - create_before_destroy = true - } - - tags = { - Name = "${local.prefix}-frontend-cert" - } -} - -# ============================================ -# DNS Validation Records -# NOTE: You need to add these CNAME records to your DNS provider -# The values are output after terraform apply -# ============================================ - -# Output the validation records for manual DNS setup -output "acm_validation_records" { - description = "DNS records to add for ACM certificate validation" - value = var.domain_name != "" ? { - for dvo in aws_acm_certificate.frontend[0].domain_validation_options : dvo.domain_name => { - name = dvo.resource_record_name - type = dvo.resource_record_type - value = dvo.resource_record_value - } - } : {} -} - -# ============================================ -# Wait for certificate validation -# ============================================ - -resource "aws_acm_certificate_validation" "frontend" { - count = var.domain_name != "" ? 1 : 0 - provider = aws.us_east_1 - - certificate_arn = aws_acm_certificate.frontend[0].arn - - # This will wait for the certificate to be validated - # You must add the DNS records first! - timeouts { - create = "30m" - } -} +# +# NOTE: The ACM certificate should be created separately (once) in us-east-1 +# and the ARN passed via the acm_certificate_arn variable. +# +# To create a certificate manually: +# 1. Go to AWS ACM Console in us-east-1 (N. Virginia) +# 2. Request a public certificate for your domain (e.g., notes.heybub.app) +# 3. Complete DNS validation by adding the CNAME record +# 4. Copy the certificate ARN and add it to GitHub secrets as ACM_CERTIFICATE_ARN +# +# For wildcard certs (*.heybub.app), you can reuse the same cert across apps. diff --git a/terraform/cloudfront.tf b/terraform/cloudfront.tf index c66916f..cd5c14d 100644 --- a/terraform/cloudfront.tf +++ b/terraform/cloudfront.tf @@ -116,21 +116,12 @@ resource "aws_cloudfront_distribution" "frontend" { } } - # SSL Certificate - use ACM certificate if custom domain, otherwise CloudFront default - dynamic "viewer_certificate" { - for_each = var.domain_name != "" ? [1] : [] - content { - acm_certificate_arn = aws_acm_certificate_validation.frontend[0].certificate_arn - ssl_support_method = "sni-only" - minimum_protocol_version = "TLSv1.2_2021" - } - } - - dynamic "viewer_certificate" { - for_each = var.domain_name == "" ? [1] : [] - content { - cloudfront_default_certificate = true - } + # SSL Certificate - use ACM certificate if provided, otherwise CloudFront default + viewer_certificate { + cloudfront_default_certificate = var.acm_certificate_arn == "" ? true : false + acm_certificate_arn = var.acm_certificate_arn != "" ? var.acm_certificate_arn : null + ssl_support_method = var.acm_certificate_arn != "" ? "sni-only" : null + minimum_protocol_version = var.acm_certificate_arn != "" ? "TLSv1.2_2021" : null } tags = { diff --git a/terraform/main.tf b/terraform/main.tf index fb81a9d..e857394 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -34,16 +34,6 @@ provider "aws" { } } -# ACM certificates for CloudFront MUST be in us-east-1 -provider "aws" { - alias = "us_east_1" - region = "us-east-1" - - default_tags { - tags = var.tags - } -} - # Data sources data "aws_caller_identity" "current" {} data "aws_region" "current" {} diff --git a/terraform/outputs.tf b/terraform/outputs.tf index 584a5d9..ffd244c 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -60,3 +60,17 @@ output "cloudfront_domain" { description = "CloudFront distribution domain (for DNS CNAME)" value = aws_cloudfront_distribution.frontend.domain_name } + +output "custom_domain_setup" { + description = "Instructions for custom domain setup" + value = var.domain_name != "" && var.acm_certificate_arn == "" ? <<-EOT + ⚠️ Custom domain set but no ACM certificate provided! + + To enable HTTPS on ${var.domain_name}: + 1. Create ACM certificate in us-east-1 for ${var.domain_name} + 2. Add certificate ARN to GitHub secrets as ACM_CERTIFICATE_ARN + 3. Re-deploy + + DNS: Point ${var.domain_name} CNAME to ${aws_cloudfront_distribution.frontend.domain_name} + EOT : null +} diff --git a/terraform/variables.tf b/terraform/variables.tf index 310d9f8..911a87c 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -32,7 +32,13 @@ variable "supabase_jwt_secret" { } variable "domain_name" { - description = "Custom domain name (optional)" + description = "Custom domain name (optional, e.g., notes.heybub.app)" + type = string + default = "" +} + +variable "acm_certificate_arn" { + description = "ACM certificate ARN in us-east-1 for the custom domain (required if domain_name is set)" type = string default = "" }