Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions environments/cae-dev/terraform.tfvars
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
environment = "cae-dev"
region = "us-west-2"
cluster_name = "jupyterhub"
domain_name = "cae-dev.rocktalus.com"
domain_name = "cae-dev.geopotential.org"
admin_email = "refuge@rocktalus.com"
owner_email = "refuge@rocktalus.com"
cost_center = "cae-dev"
Expand All @@ -32,15 +32,21 @@ cluster_admin_users = [
# Kubernetes
kubernetes_version = "1.34"

# ACM Certificate - Manual DNS validation (no Route53)
# ACM Certificate - Automated DNS validation via Route53 (same-account zone)
enable_acm = true
acm_enable_wildcard = false
acm_auto_validate = false # Manually add DNS validation records
acm_auto_validate = true # Route53 creates validation records automatically

# Route53 DNS Management - auto-creates/updates DNS + ACM validation on apply
# Zone must already exist in this account (992398409787); role_arn empty = same-account
manage_route53_dns = true
route53_zone_name = "geopotential.org"
route53_role_arn = ""

# Network Configuration
vpc_cidr = "10.6.0.0/16" # Different CIDR for dev
enable_nat_gateway = true
single_nat_gateway = true
vpc_cidr = "10.6.0.0/16" # Different CIDR for dev
enable_nat_gateway = true
single_nat_gateway = true
pin_main_nodes_single_az = true # All nodes in single AZ (matches cae)
pin_system_nodes_single_az = true # Prevents hub PVC/node zone affinity conflicts
pin_user_nodes_single_az = true
Expand Down
84 changes: 81 additions & 3 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,23 @@ provider "aws" {
}
}

# Cross-account provider for Route53 (when zone is in different account)
# With route53_role_arn empty, this behaves as the default same-account provider.
provider "aws" {
alias = "route53"
region = var.region

default_tags {
tags = local.common_tags
}

# Assume role if route53_role_arn is provided (cross-account)
assume_role {
role_arn = var.route53_role_arn != "" ? var.route53_role_arn : null
session_name = var.route53_role_arn != "" ? "tofu-route53-${var.environment}" : null
}
}

provider "kubernetes" {
host = module.eks.cluster_endpoint
cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
Expand Down Expand Up @@ -551,7 +568,7 @@ resource "aws_iam_role_policy" "kubecost" {
],
# CUR bucket access - only if CUR is enabled (bucket exists)
# Using for expression to ensure consistent types when condition is false
[for bucket in (local.cur_bucket_name != "" ? [local.cur_bucket_name] : []) : {
[for bucket in(local.cur_bucket_name != "" ? [local.cur_bucket_name] : []) : {
Sid = "CURBucketAccess"
Effect = "Allow"
Action = [
Expand All @@ -563,7 +580,7 @@ resource "aws_iam_role_policy" "kubecost" {
"arn:aws:s3:::${bucket}/*"
]
}],
[for _ in (local.cur_bucket_name != "" ? [1] : []) : {
[for _ in(local.cur_bucket_name != "" ? [1] : []) : {
Sid = "AthenaQueryAccess"
Effect = "Allow"
Action = [
Expand All @@ -573,7 +590,7 @@ resource "aws_iam_role_policy" "kubecost" {
]
Resource = "*"
}],
[for _ in (local.cur_bucket_name != "" ? [1] : []) : {
[for _ in(local.cur_bucket_name != "" ? [1] : []) : {
Sid = "GlueCatalogAccess"
Effect = "Allow"
Action = [
Expand Down Expand Up @@ -723,4 +740,65 @@ module "auto_shutdown" {
tags = local.common_tags

depends_on = [module.eks]
}

# =============================================================================
# Route53 DNS Management (Optional)
# =============================================================================
# Automatically creates/updates DNS record pointing to the load balancer and
# the ACM validation records. When enabled, destroy/recreate cycles will
# automatically update DNS. Supports cross-account Route53 via route53_role_arn
# (leave empty for a same-account zone).

# Look up the Route53 hosted zone (uses cross-account provider)
data "aws_route53_zone" "main" {
count = var.manage_route53_dns ? 1 : 0
provider = aws.route53
name = var.route53_zone_name
}

# Read the proxy-public service to get the load balancer hostname
data "kubernetes_service" "proxy_public" {
count = var.manage_route53_dns && var.enable_jupyterhub ? 1 : 0

metadata {
name = "proxy-public"
namespace = "daskhub"
}

depends_on = [module.helm]
}

# Create the DNS record pointing to the load balancer
# Using CNAME instead of Alias to avoid needing to look up NLB zone ID
resource "aws_route53_record" "jupyterhub" {
count = var.manage_route53_dns && var.enable_jupyterhub ? 1 : 0
provider = aws.route53

zone_id = data.aws_route53_zone.main[0].zone_id
name = var.domain_name
type = "CNAME"
ttl = 300
records = [data.kubernetes_service.proxy_public[0].status[0].load_balancer[0].ingress[0].hostname]
allow_overwrite = true
}

# Also create the ACM validation record(s) when validating via Route53
resource "aws_route53_record" "acm_validation" {
for_each = var.manage_route53_dns && var.enable_acm ? {
for dvo in module.acm[0].domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
} : {}

provider = aws.route53

zone_id = data.aws_route53_zone.main[0].zone_id
name = each.value.name
type = each.value.type
ttl = 60
records = [each.value.record]
allow_overwrite = true
}
5 changes: 5 additions & 0 deletions modules/acm/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ output "dns_validation_records" {
value = local.validation_options
}

output "domain_validation_options" {
description = "Domain validation options for Route53 automation"
value = aws_acm_certificate.cert.domain_validation_options
}

output "validation_instructions" {
description = "Instructions for manual DNS validation"
value = var.auto_validate ? "Certificate validation is automatic." : <<-EOT
Expand Down
20 changes: 20 additions & 0 deletions outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,26 @@ output "monitoring_urls" {
} : null
}

# Route53 DNS Management
output "route53_dns_record" {
description = "Route53 DNS record created for JupyterHub"
value = var.manage_route53_dns && var.enable_jupyterhub ? {
name = aws_route53_record.jupyterhub[0].name
type = aws_route53_record.jupyterhub[0].type
value = one(aws_route53_record.jupyterhub[0].records)
status = "Managed by OpenTofu - auto-updates on apply"
} : null
}

output "load_balancer_hostname" {
description = "Load balancer hostname for manual DNS configuration"
value = var.manage_route53_dns && var.enable_jupyterhub ? (
"Managed by Route53 - see route53_dns_record output"
) : (
var.enable_jupyterhub ? "Run: kubectl get svc -n daskhub proxy-public -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'" : "N/A"
)
}

# Maintenance Commands
output "maintenance_commands" {
description = "Useful maintenance commands"
Expand Down
21 changes: 20 additions & 1 deletion variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ variable "use_ecr_pull_through_cache" {
variable "github_username" {
description = "GitHub username for ECR pull-through cache authentication. Used with github.token from SOPS secrets."
type = string
default = "espg" # Default to repo owner
default = "espg" # Default to repo owner
}

# User Resource Limits
Expand Down Expand Up @@ -604,4 +604,23 @@ variable "enable_continuous_image_puller" {
description = "Enable continuous image puller to pre-pull images on nodes. Disable for small test clusters."
type = bool
default = true
}

# Route53 DNS Configuration
variable "manage_route53_dns" {
description = "Automatically create/update Route53 DNS record for the load balancer and ACM validation records"
type = bool
default = false
}

variable "route53_zone_name" {
description = "Route53 hosted zone name (e.g., 'geopotential.org'). Required if manage_route53_dns is true."
type = string
default = ""
}

variable "route53_role_arn" {
description = "IAM role ARN to assume for Route53 operations. Required only when the Route53 zone is in a different AWS account; leave empty for a same-account zone."
type = string
default = ""
}
Loading