diff --git a/environments/cae-dev/terraform.tfvars b/environments/cae-dev/terraform.tfvars index e202ac6..60892b7 100644 --- a/environments/cae-dev/terraform.tfvars +++ b/environments/cae-dev/terraform.tfvars @@ -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" @@ -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 diff --git a/main.tf b/main.tf index 98e8326..139bc53 100644 --- a/main.tf +++ b/main.tf @@ -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) @@ -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 = [ @@ -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 = [ @@ -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 = [ @@ -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 } \ No newline at end of file diff --git a/modules/acm/outputs.tf b/modules/acm/outputs.tf index d6b75b7..d4cd8fe 100644 --- a/modules/acm/outputs.tf +++ b/modules/acm/outputs.tf @@ -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 diff --git a/outputs.tf b/outputs.tf index 5317f5b..a4dd16a 100644 --- a/outputs.tf +++ b/outputs.tf @@ -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" diff --git a/variables.tf b/variables.tf index cac077e..680c9a0 100644 --- a/variables.tf +++ b/variables.tf @@ -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 @@ -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 = "" } \ No newline at end of file