From faeac482c33a9ea1d01a0ea78fab4a73fe8a209c Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Fri, 13 Mar 2026 15:36:48 +0530 Subject: [PATCH 01/13] Fix nginx_gateway_fabric_legacy: annotations, wildcard listeners, multi-domain outputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Azure: always set load-balancer-internal annotation (true/false) - OVH: add loadbalancer.ovhcloud.com/flavor annotation - Migrate kubernetes_secret to kubernetes_secret_v1 (deprecated) - Wildcard hostname for certificate_reference domains (*.domain) - ParentRef: subdomain routes use wildcard domain listener when cert_ref present - Filter additional_hostname_configs to exclude cert_ref covered subdomains - cross_namespace_backends: use distinct() to avoid duplicate key error - ClusterIssuer count guard: use certmanager_managed_domains length - Output interfaces: multi-domain rule × domain cross-product - tls_secret: map of domain keys to cert secret names - Add missing outputs: lb_record_value, record_type, name, base_domain, etc. - Remove ACM-specific sections from README (ACM will be in cloud-specific module) Co-Authored-By: Claude Opus 4.6 --- .../nginx_gateway_fabric_legacy/1.0/README.md | 440 +++++++----------- .../nginx_gateway_fabric_legacy/1.0/main.tf | 82 ++-- .../1.0/outputs.tf | 83 +++- 3 files changed, 297 insertions(+), 308 deletions(-) diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md b/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md index 5523f5a4..fb12f900 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md @@ -1,4 +1,4 @@ -# NGINX Gateway Fabric +# NGINX Gateway Fabric (Legacy) Kubernetes Gateway API implementation for advanced ingress traffic management. @@ -6,18 +6,51 @@ Kubernetes Gateway API implementation for advanced ingress traffic management. This module deploys **NGINX Gateway Fabric**, NGINX's implementation of the Kubernetes Gateway API specification. It provides a modern, declarative approach to configuring ingress traffic with native support for advanced routing features. +This is the **legacy** flavor that uses `cc_metadata` and legacy input conventions. For the redesigned module, see `nginx_gateway_fabric`. + ### Features - **Gateway API Resources**: GatewayClass, Gateway, HTTPRoute, GRPCRoute -- **Advanced Routing**: Header matching, query parameter matching, HTTP method matching -- **URL Rewriting**: Path and hostname rewriting -- **Traffic Management**: Canary deployments, request mirroring +- **Advanced Routing**: Header matching, query parameter matching, HTTP method matching, RegularExpression path matching +- **URL Rewriting**: Path and hostname rewriting (ReplaceFullPath, ReplacePrefixMatch) +- **Traffic Management**: Canary deployments with weighted traffic splitting, request mirroring - **Multi-Domain Support**: Routes work across all configured domains -- **TLS Management**: Automatic SSL certificates via cert-manager (HTTP-01 and DNS-01) +- **TLS Management**: Automatic SSL certificates via cert-manager (HTTP-01 or DNS-01) or custom certificates - **Multi-Cloud**: AWS (NLB), Azure (LB), GCP (GCLB) -- **gRPC Support**: Native GRPCRoute resources -- **CORS**: Cross-origin resource sharing configuration -- **Observability**: Prometheus metrics via ServiceMonitor +- **gRPC Support**: Native GRPCRoute resources with method-level matching +- **CORS**: Per-route Cross-Origin Resource Sharing configuration +- **Basic Authentication**: Native NGF AuthenticationFilter with per-route opt-out +- **Observability**: Prometheus metrics via PodMonitor +- **Security Headers**: Automatic HSTS, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection +- **Cross-Namespace Backends**: ReferenceGrant support for services in other namespaces + +--- + +## Architecture + +``` + ┌─────────────────────────────────────────┐ + │ NGINX Gateway Fabric │ + │ │ + Internet ──────► │ Control Plane (Gateway Controller) │ + │ - Watches Gateway/Route resources │ + │ - Configures NGINX data plane │ + │ │ + │ Data Plane (NGINX Pods) │ + │ - Handles actual traffic │ + │ - Auto-scaled via HPA │ + │ - LoadBalancer Service │ + └─────────────────────────────────────────┘ +``` + +### cert-manager Validation Modes + +When in cert-manager mode, two validation strategies are available: + +| Validation | When Used | How It Works | +|------------|-----------|-------------| +| **HTTP-01** (default) | `disable_endpoint_validation: false` | Creates bootstrap self-signed certs, cert-manager validates via HTTP challenge | +| **DNS-01** | `disable_endpoint_validation: true` or `private: true` | Uses DNS challenges, required for private/internal LBs | --- @@ -53,13 +86,14 @@ This module deploys **NGINX Gateway Fabric**, NGINX's implementation of the Kube | `service_name` | Kubernetes service name | | `namespace` | Service namespace | | `port` | Service port number | -| `path` | URL path (required for HTTP routes) | +| `path` | URL path (required for HTTP routes, not needed for gRPC) | ### Path Type Options | Type | Default | Description | |------|---------|-------------| -| `PathPrefix` | Yes | Matches paths starting with the specified prefix | +| `RegularExpression` | Yes | Matches paths using regex | +| `PathPrefix` | No | Matches paths starting with the specified prefix | | `Exact` | No | Matches the exact path only | --- @@ -68,8 +102,6 @@ This module deploys **NGINX Gateway Fabric**, NGINX's implementation of the Kube ### Header-Based Routing -Route traffic based on HTTP headers: - ```json { "rules": { @@ -84,11 +116,6 @@ Route traffic based on HTTP headers: "name": "X-API-Version", "value": "v2", "type": "Exact" - }, - "client_header": { - "name": "X-Client-Type", - "value": "mobile.*", - "type": "RegularExpression" } } } @@ -98,8 +125,6 @@ Route traffic based on HTTP headers: ### Query Parameter Matching -Route traffic based on query parameters: - ```json { "rules": { @@ -108,7 +133,6 @@ Route traffic based on query parameters: "namespace": "default", "port": "8080", "path": "/api", - "path_type": "PathPrefix", "query_param_matches": { "version_param": { "name": "version", @@ -123,8 +147,6 @@ Route traffic based on query parameters: ### HTTP Method Matching -Route traffic based on HTTP method: - ```json { "rules": { @@ -133,7 +155,6 @@ Route traffic based on HTTP method: "namespace": "default", "port": "8080", "path": "/api", - "path_type": "PathPrefix", "method": "GET" } } @@ -146,30 +167,21 @@ Options: `ALL` (default), `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTI ## URL Rewriting -Rewrite request URLs before forwarding to backend: +### Prefix Replacement ```json { - "rules": { - "legacy_api": { - "service_name": "new-api-service", - "namespace": "default", - "port": "8080", - "path": "/old-api", - "path_type": "PathPrefix", - "url_rewrite": { - "rewrite_rule": { - "hostname": "internal-api.svc.cluster.local", - "path_type": "ReplacePrefixMatch", - "replace_path": "/new-api" - } - } + "url_rewrite": { + "rewrite_rule": { + "hostname": "internal-api.svc.cluster.local", + "path_type": "ReplacePrefixMatch", + "replace_path": "/new-api" } } } ``` -For full path replacement: +### Full Path Replacement ```json { @@ -188,36 +200,17 @@ For full path replacement: ### Request Headers -Modify headers sent to backend: - ```json { - "rules": { - "api": { - "service_name": "api", - "namespace": "default", - "port": "8080", - "path": "/", - "path_type": "PathPrefix", - "request_header_modifier": { - "add": { - "custom_header": { - "name": "X-Custom-Header", - "value": "custom-value" - } - }, - "set": { - "source_header": { - "name": "X-Request-Source", - "value": "gateway" - } - }, - "remove": { - "sensitive_header": { - "name": "X-Sensitive-Header" - } - } - } + "request_header_modifier": { + "add": { + "custom_header": { "name": "X-Custom-Header", "value": "custom-value" } + }, + "set": { + "source_header": { "name": "X-Request-Source", "value": "gateway" } + }, + "remove": { + "sensitive_header": { "name": "X-Sensitive-Header" } } } } @@ -225,103 +218,60 @@ Modify headers sent to backend: ### Response Headers -Modify headers sent to client: +Security headers (HSTS, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection) are automatically added to all responses. ```json { "response_header_modifier": { "add": { - "response_id": { - "name": "X-Response-ID", - "value": "unique-id" - } + "response_id": { "name": "X-Response-ID", "value": "unique-id" } }, "set": { - "cache_header": { - "name": "Cache-Control", - "value": "no-store" - } + "cache_header": { "name": "Cache-Control", "value": "no-store" } }, "remove": { - "server_header": { - "name": "Server" - } + "server_header": { "name": "Server" } } } } ``` -> **Note**: Security headers (HSTS, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection) are automatically added. - --- ## Request Timeouts -Configure request and backend timeouts: - ```json { - "rules": { - "api": { - "service_name": "slow-api", - "namespace": "default", - "port": "8080", - "path": "/api", - "path_type": "PathPrefix", - "timeouts": { - "request": "60s", - "backend_request": "30s" - } - } + "timeouts": { + "request": "60s", + "backend_request": "30s" } } ``` +Default: 300s for both request and backend_request. + --- ## CORS Configuration -Enable Cross-Origin Resource Sharing: - ```json { - "rules": { - "api": { - "service_name": "api", - "namespace": "default", - "port": "8080", - "path": "/", - "path_type": "PathPrefix", - "cors": { - "enabled": true, - "allow_origins": { - "origin1": { - "origin": "https://example.com" - }, - "origin2": { - "origin": "https://app.example.com" - } - }, - "allow_methods": { - "get": { - "method": "GET" - }, - "post": { - "method": "POST" - } - }, - "allow_headers": { - "content_type": { - "header": "Content-Type" - }, - "auth": { - "header": "Authorization" - } - }, - "allow_credentials": true, - "max_age": 86400 - } - } + "cors": { + "enabled": true, + "allow_origins": { + "origin1": { "origin": "https://example.com" } + }, + "allow_methods": { + "get": { "method": "GET" }, + "post": { "method": "POST" } + }, + "allow_headers": { + "content_type": { "header": "Content-Type" }, + "auth": { "header": "Authorization" } + }, + "allow_credentials": true, + "max_age": 86400 } } ``` @@ -352,26 +302,14 @@ Enable Cross-Origin Resource Sharing: ```json { - "rules": { - "grpc_service": { - "service_name": "grpc-backend", - "namespace": "default", - "port": "50051", - "grpc_config": { - "enabled": true, - "match_all_methods": false, - "method_match": { - "get_user": { - "service": "myapp.v1.UserService", - "method": "GetUser", - "type": "Exact" - }, - "list_users": { - "service": "myapp.v1.UserService", - "method": "ListUsers", - "type": "Exact" - } - } + "grpc_config": { + "enabled": true, + "match_all_methods": false, + "method_match": { + "get_user": { + "service": "myapp.v1.UserService", + "method": "GetUser", + "type": "Exact" } } } @@ -380,73 +318,73 @@ Enable Cross-Origin Resource Sharing: --- -## Canary Deployments +## Basic Authentication -Split traffic between service versions: +Enable basic auth globally with per-route opt-out: ```json { - "rules": { - "api": { - "service_name": "api-v1", - "namespace": "default", - "port": "8080", - "path": "/", - "path_type": "PathPrefix", - "canary_deployment": { - "enabled": true, - "canary_service": "api-v2", - "canary_weight": 20 + "spec": { + "basic_auth": true, + "rules": { + "protected_api": { + "service_name": "api", + "port": "8080", + "path": "/api", + "namespace": "default" + }, + "health_check": { + "service_name": "api", + "port": "8080", + "path": "/health", + "namespace": "default", + "disable_auth": true } } } } ``` -This sends 20% of traffic to `api-v2` and 80% to `api-v1`. +Uses NGF's native `AuthenticationFilter` CRD. Credentials are auto-generated (`{instance_name}user` / random 10-char password). --- -## Request Mirroring - -Mirror traffic to a secondary service for testing: +## Canary Deployments ```json { - "rules": { - "api": { - "service_name": "api-prod", - "namespace": "default", - "port": "8080", - "path": "/api", - "path_type": "PathPrefix", - "request_mirror": { - "service_name": "api-shadow", - "port": "8080", - "namespace": "testing" - } - } + "canary_deployment": { + "enabled": true, + "canary_service": "api-v2", + "canary_weight": 20 } } ``` +Sends 20% of traffic to `api-v2` and 80% to primary service. + --- -## Multi-Domain Configuration +## Request Mirroring -### Custom Domains +```json +{ + "request_mirror": { + "service_name": "api-shadow", + "port": "8080", + "namespace": "testing" + } +} +``` -Configure custom domains inside spec: +--- + +## Multi-Domain Configuration ```json { - "kind": "ingress", - "flavor": "nginx_gateway_fabric_legacy", - "version": "1.0", "spec": { - "private": false, "disable_base_domain": true, - "force_ssl_redirection": true, "domains": { "production": { "domain": "api.example.com", @@ -457,91 +395,66 @@ Configure custom domains inside spec: "alias": "staging", "certificate_reference": "staging-tls" } - }, - "rules": { - "api": { - "service_name": "api-service", - "namespace": "default", - "port": "8080", - "path": "/", - "path_type": "PathPrefix" - } } } } ``` -All routes are accessible on all domains: -- `https://api.example.com/` -- `https://staging-api.example.com/` - -### Domain Options - -| Field | Description | -|-------|-------------| -| `domain` | Full domain name | -| `alias` | Short identifier | -| `certificate_reference` | Existing TLS secret name (optional) | +All routes are accessible on all configured domains. --- ## TLS Certificate Management -### HTTP-01 Validation (Default) - -Used when `disable_endpoint_validation: false` (default): +### HTTP-01 Validation (Default -- cert-manager mode) - Creates bootstrap self-signed certificates for Gateway startup -- cert-manager replaces them with valid Let's Encrypt certificates +- cert-manager replaces them with valid Let's Encrypt certificates via HTTP-01 challenge - Requires port 80 accessible from internet -### DNS-01 Validation +### DNS-01 Validation (cert-manager mode) Used when `disable_endpoint_validation: true` or `private: true`: +```json +{ + "spec": { + "private": true, + "disable_endpoint_validation": true, + "dns_issuer": "gts-production" + } +} +``` + - Uses DNS challenges instead of HTTP - Required for private/internal load balancers -- Requires cert-manager DNS provider configuration +- All managed domains share a single wildcard certificate +- Requires cert-manager DNS provider (e.g., Route53) configuration -### Custom Certificates +### Custom Certificates (cert-manager mode) -Use existing TLS certificates: +Use an existing K8s TLS secret: ```json { - "spec": { - "domains": { - "custom": { - "domain": "api.example.com", - "alias": "api", - "certificate_reference": "my-existing-tls-secret" - } + "domains": { + "custom": { + "domain": "api.example.com", + "alias": "api", + "certificate_reference": "my-existing-tls-secret" } } } ``` ---- - ## Private Load Balancer -Deploy with internal/private load balancer: - ```json { "spec": { "private": true, "disable_endpoint_validation": true, - "force_ssl_redirection": true, - "rules": { - "api": { - "service_name": "api-service", - "namespace": "default", - "port": "8080", - "path": "/", - "path_type": "PathPrefix" - } - } + "force_ssl_redirection": true } } ``` @@ -550,15 +463,10 @@ Deploy with internal/private load balancer: ## Custom Helm Values -Override default Helm configuration: - ```json { "spec": { "helm_values": { - "nginxGateway": { - "replicaCount": 3 - }, "nginx": { "config": { "logging": { @@ -571,8 +479,6 @@ Override default Helm configuration: } ``` -See available values: https://github.com/nginxinc/nginx-gateway-fabric/blob/main/charts/nginx-gateway-fabric/values.yaml - --- ## Spec Options @@ -582,20 +488,24 @@ See available values: https://github.com/nginxinc/nginx-gateway-fabric/blob/main | `private` | boolean | `false` | Use internal load balancer | | `force_ssl_redirection` | boolean | `true` | Redirect HTTP to HTTPS | | `disable_base_domain` | boolean | `false` | Disable auto-generated base domain | -| `disable_endpoint_validation` | boolean | `false` | Use DNS-01 instead of HTTP-01 | +| `disable_endpoint_validation` | boolean | `false` | Use DNS-01 instead of HTTP-01 validation | | `domain_prefix_override` | string | - | Override auto-generated domain prefix | +| `dns_issuer` | string | `gts-production` | ClusterIssuer name for DNS-01 validation | +| `acme_email` | string | `systems@facets.cloud` | Email for Let's Encrypt ACME account | +| `cluster_issuer_override` | string | - | Override the auto-selected ClusterIssuer | | `renew_cert_before` | string | `720h` | Renew certificate before expiry | +| `basic_auth` | boolean | `false` | Enable basic authentication | +| `body_size` | string | `150m` | Maximum client request body size | | `helm_wait` | boolean | `true` | Wait for Helm release to be ready | -| `resources` | object | - | Controller resource limits/requests | | `helm_values` | object | - | Additional Helm values | --- ## Cloud Provider Support -| Provider | Load Balancer | DNS | Features | -|----------|--------------|-----|----------| -| AWS | Network Load Balancer (NLB) | Route53 | Proxy Protocol v2, private LB | +| Provider | Load Balancer | DNS | Annotations | +|----------|--------------|-----|-------------| +| AWS | Network Load Balancer (NLB) | Route53 | Proxy Protocol v2, NLB target type IP | | Azure | Azure Load Balancer | - | Internal LB support | | GCP | Google Cloud Load Balancer | - | Internal LB with global access | @@ -606,22 +516,14 @@ See available values: https://github.com/nginxinc/nginx-gateway-fabric/blob/main | Output | Description | |--------|-------------| | `domains` | List of all configured domains | -| `domain` | Base domain (if not disabled) | +| `domain` | Base domain (null if disabled) | | `secure_endpoint` | HTTPS endpoint for base domain | | `gateway_class` | GatewayClass name | | `gateway_name` | Gateway resource name | | `load_balancer_hostname` | LB hostname (for CNAME records) | | `load_balancer_ip` | LB IP address (for A records) | - ---- - -## Not Supported - -| Feature | Reason | -|---------|--------| -| Rate Limiting | Not natively supported in NGF | -| IP Whitelisting | Not natively supported in NGF | -| Basic Auth | Not natively supported in NGF | +| `tls_secret` | TLS certificate secret name for base domain | +| `subdomain` | Subdomain mappings | --- @@ -634,10 +536,11 @@ kubectl get gateway -n kubectl describe gateway -n ``` -### Check HTTPRoute Status +### Check Routes ```bash kubectl get httproute -n +kubectl get grpcroute -n kubectl describe httproute -n ``` @@ -647,11 +550,18 @@ kubectl describe httproute -n kubectl logs -n -l app.kubernetes.io/name=nginx-gateway-fabric -c nginx-gateway ``` +### Data Plane Logs + +```bash +kubectl logs -n -l app.kubernetes.io/name=nginx-gateway-fabric -c nginx +``` + ### Certificate Issues ```bash kubectl get certificate -n kubectl describe certificate -n +kubectl get clusterissuer ``` --- diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf index e7931088..2fb8e38b 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf @@ -72,13 +72,23 @@ locals { hostname if !contains(local.all_domain_hostnames, hostname) ] + # Domains that have a certificate_reference (wildcard cert covers subdomains) + domains_with_cert_ref = { + for domain_key, domain in local.domains : + domain.domain => lookup(domain, "certificate_reference", "") + if lookup(domain, "certificate_reference", "") != "" + } + # Map of additional hostnames to their config for listeners and certs + # Subdomains of domains with certificate_reference are excluded — the wildcard listener covers them additional_hostname_configs = { for hostname in local.additional_hostnames : replace(replace(hostname, ".", "-"), "*", "wildcard") => { hostname = hostname secret_name = "${local.name}-${replace(replace(hostname, ".", "-"), "*", "wildcard")}-tls-cert" } + # Exclude subdomains of domains with certificate_reference (covered by wildcard listener) + if !anytrue([for parent_domain, cert_ref in local.domains_with_cert_ref : endswith(hostname, ".${parent_domain}")]) } # Tolerations: merge environment defaults with facets dedicated tolerations @@ -138,9 +148,9 @@ locals { } ) - azure_annotations = lookup(var.instance.spec, "private", false) ? { - "service.beta.kubernetes.io/azure-load-balancer-internal" = "true" - } : {} + azure_annotations = { + "service.beta.kubernetes.io/azure-load-balancer-internal" = lookup(var.instance.spec, "private", false) ? "true" : "false" + } gcp_annotations = lookup(var.instance.spec, "private", false) ? { "cloud.google.com/load-balancer-type" = "Internal" @@ -148,12 +158,17 @@ locals { "networking.gke.io/internal-load-balancer-allow-global-access" = "true" } : {} + ovh_annotations = { + "loadbalancer.ovhcloud.com/flavor" = "small" + } + cloud_provider = upper(try(var.inputs.kubernetes_details.attributes.cloud_provider, "aws")) service_annotations = merge( local.cloud_provider == "AWS" ? local.aws_annotations : {}, local.cloud_provider == "AZURE" ? local.azure_annotations : {}, - local.cloud_provider == "GCP" ? local.gcp_annotations : {} + local.cloud_provider == "GCP" ? local.gcp_annotations : {}, + local.cloud_provider == "OVH" ? local.ovh_annotations : {} ) # Get ClusterIssuer names and config from cert-manager @@ -252,8 +267,7 @@ locals { } spec = { # Reference the correct listener(s) for this route's hostnames - # If route has domain_prefix, reference the additional hostname listeners - # If route has no domain_prefix, reference the base domain listeners + # Routes attach to HTTPS listeners per domain/hostname — TLS always terminates at the Gateway # When force_ssl_redirection is disabled, also attach to HTTP listener so traffic is served on port 80 parentRefs = concat( lookup(v, "domain_prefix", null) == null || lookup(v, "domain_prefix", null) == "" ? [ @@ -264,11 +278,13 @@ locals { sectionName = "https-${domain_key}" } ] : [ - # Has domain_prefix - use additional hostname listeners + # Has domain_prefix: + # - If parent domain has certificate_reference → use wildcard domain listener (*.domain covers subdomains) + # - Otherwise → use additional hostname listener for domain_key, domain in local.domains : { name = local.name namespace = var.environment.namespace - sectionName = "https-${replace(replace("${lookup(v, "domain_prefix", null)}.${domain.domain}", ".", "-"), "*", "wildcard")}" + sectionName = lookup(domain, "certificate_reference", "") != "" ? "https-${domain_key}" : "https-${replace(replace("${lookup(v, "domain_prefix", null)}.${domain.domain}", ".", "-"), "*", "wildcard")}" } ], # Also attach to HTTP listener when SSL redirection is disabled @@ -450,8 +466,7 @@ locals { } spec = { # Reference the correct listener(s) for this route's hostnames - # If route has domain_prefix, reference the additional hostname listeners - # If route has no domain_prefix, reference the base domain listeners + # Routes attach to HTTPS listeners per domain/hostname — TLS always terminates at the Gateway # When force_ssl_redirection is disabled, also attach to HTTP listener parentRefs = concat( lookup(v, "domain_prefix", null) == null || lookup(v, "domain_prefix", null) == "" ? [ @@ -462,11 +477,13 @@ locals { sectionName = "https-${domain_key}" } ] : [ - # Has domain_prefix - use additional hostname listeners + # Has domain_prefix: + # - If parent domain has certificate_reference → use wildcard domain listener (*.domain covers subdomains) + # - Otherwise → use additional hostname listener for domain_key, domain in local.domains : { name = local.name namespace = var.environment.namespace - sectionName = "https-${replace(replace("${lookup(v, "domain_prefix", null)}.${domain.domain}", ".", "-"), "*", "wildcard")}" + sectionName = lookup(domain, "certificate_reference", "") != "" ? "https-${domain_key}" : "https-${replace(replace("${lookup(v, "domain_prefix", null)}.${domain.domain}", ".", "-"), "*", "wildcard")}" } ], # Also attach to HTTP listener when SSL redirection is disabled @@ -552,8 +569,7 @@ locals { # Collect unique namespaces that need ReferenceGrants (for cross-namespace backends) cross_namespace_backends = { - for k, v in local.rulesFiltered : v.namespace => v.namespace - if v.namespace != var.environment.namespace + for ns in distinct([for k, v in local.rulesFiltered : v.namespace if v.namespace != var.environment.namespace]) : ns => ns } # ReferenceGrant resources for cross-namespace backends @@ -674,7 +690,7 @@ resource "tls_self_signed_cert" "bootstrap" { ] } -resource "kubernetes_secret" "bootstrap_tls" { +resource "kubernetes_secret_v1" "bootstrap_tls" { for_each = local.bootstrap_tls_domains metadata { @@ -695,15 +711,15 @@ resource "kubernetes_secret" "bootstrap_tls" { } # Bootstrap TLS for additional hostnames (from domain_prefix in rules) -# Only created for HTTP-01 validation (when disable_endpoint_validation is false) +# Only for additional hostnames that did NOT inherit a certificate_reference from parent domain resource "tls_private_key" "bootstrap_additional" { - for_each = local.disable_endpoint_validation ? {} : local.additional_hostname_configs + for_each = local.additional_hostname_configs algorithm = "RSA" rsa_bits = 2048 } resource "tls_self_signed_cert" "bootstrap_additional" { - for_each = local.disable_endpoint_validation ? {} : local.additional_hostname_configs + for_each = local.additional_hostname_configs private_key_pem = tls_private_key.bootstrap_additional[each.key].private_key_pem subject { @@ -721,8 +737,8 @@ resource "tls_self_signed_cert" "bootstrap_additional" { ] } -resource "kubernetes_secret" "bootstrap_tls_additional" { - for_each = local.disable_endpoint_validation ? {} : local.additional_hostname_configs +resource "kubernetes_secret_v1" "bootstrap_tls_additional" { + for_each = local.additional_hostname_configs metadata { name = each.value.secret_name @@ -817,7 +833,7 @@ module "http01_certificate" { # Name module for additional hostname certificates (keeps helm release names under 53 chars) # Only created when NOT using gateway-shim (same as http01_certificate for base domains) module "http01_certificate_additional_name" { - for_each = !local.use_gateway_shim && !local.disable_endpoint_validation ? local.additional_hostname_configs : {} + for_each = !local.use_gateway_shim ? local.additional_hostname_configs : {} source = "github.com/Facets-cloud/facets-utility-modules//name" environment = var.environment @@ -831,7 +847,7 @@ module "http01_certificate_additional_name" { # Explicit Certificate resources for additional hostnames (domain_prefix + domain) # Created when NOT using gateway-shim (gateway-shim handles certs automatically when enabled) module "http01_certificate_additional" { - for_each = !local.use_gateway_shim && !local.disable_endpoint_validation ? local.additional_hostname_configs : {} + for_each = !local.use_gateway_shim ? local.additional_hostname_configs : {} source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resource" name = module.http01_certificate_additional_name[each.key].name @@ -1049,17 +1065,19 @@ resource "helm_release" "nginx_gateway_fabric" { } } }], - # HTTPS Listeners per domain + # HTTPS Listeners per domain — TLS always terminates at the Gateway + # When certificate_reference is provided, use wildcard hostname (*.domain) to cover all subdomains + # Otherwise use exact hostname and rely on additional hostname listeners for subdomains [for domain_key, domain in local.domains : { name = "https-${domain_key}" protocol = "HTTPS" port = 443 - hostname = domain.domain + hostname = lookup(domain, "certificate_reference", "") != "" ? "*.${domain.domain}" : domain.domain tls = { mode = "Terminate" certificateRefs = [{ kind = "Secret" - # If certificate_reference is provided, use it (custom cert) + # If certificate_reference is provided, use it (custom cert or K8s secret name) # Otherwise: DNS-01 uses shared dns_validation_secret_name, HTTP-01 uses per-domain bootstrap cert name = lookup(domain, "certificate_reference", "") != "" ? domain.certificate_reference : ( local.disable_endpoint_validation ? local.dns_validation_secret_name : "${local.name}-${domain_key}-tls-cert" @@ -1072,7 +1090,7 @@ resource "helm_release" "nginx_gateway_fabric" { } } } if can(domain.domain)], - # HTTPS Listeners for additional hostnames from rules (domain_prefix + domain) + # HTTPS Listeners for additional hostnames [for hostname_key, config in local.additional_hostname_configs : { name = "https-${hostname_key}" protocol = "HTTPS" @@ -1082,7 +1100,7 @@ resource "helm_release" "nginx_gateway_fabric" { mode = "Terminate" certificateRefs = [{ kind = "Secret" - name = local.disable_endpoint_validation ? local.dns_validation_secret_name : config.secret_name + name = config.secret_name }] } allowedRoutes = { @@ -1099,15 +1117,15 @@ resource "helm_release" "nginx_gateway_fabric" { ] depends_on = [ - kubernetes_secret.bootstrap_tls, - kubernetes_secret.bootstrap_tls_additional + kubernetes_secret_v1.bootstrap_tls, + kubernetes_secret_v1.bootstrap_tls_additional ] } # Gateway API HTTP-01 ClusterIssuer - bundled here as it requires parentRefs to the Gateway # See: https://github.com/cert-manager/cert-manager/issues/7890 module "cluster-issuer-gateway-http01" { - count = local.disable_endpoint_validation ? 0 : 1 + count = length(local.certmanager_managed_domains) > 0 ? 1 : 0 depends_on = [helm_release.nginx_gateway_fabric] source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resource" name = local.cluster_issuer_gateway_http @@ -1158,7 +1176,7 @@ module "gateway_api_resources" { resources_data = local.gateway_api_resources advanced_config = {} - depends_on = [helm_release.nginx_gateway_fabric, kubernetes_secret.basic_auth] + depends_on = [helm_release.nginx_gateway_fabric, kubernetes_secret_v1.basic_auth] } # Basic Authentication using NGF AuthenticationFilter CRD @@ -1172,7 +1190,7 @@ resource "random_string" "basic_auth_password" { special = false } -resource "kubernetes_secret" "basic_auth" { +resource "kubernetes_secret_v1" "basic_auth" { count = lookup(var.instance.spec, "basic_auth", false) ? 1 : 0 metadata { diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/outputs.tf b/modules/ingress/nginx_gateway_fabric_legacy/1.0/outputs.tf index a613527e..072a801d 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/outputs.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/outputs.tf @@ -24,16 +24,33 @@ locals { } ) - output_interfaces = { - for route_key, route in local.rulesFiltered : route_key => { - connection_string = local.is_auth_enabled ? "https://${local.username}:${local.password}@${route.host}" : "https://${route.host}" - host = route.host - port = 443 - username = local.username - password = local.password - secrets = local.is_auth_enabled ? ["connection_string", "password"] : [] + # Generate output interfaces for every rule × domain combination + # Each rule is expanded across all configured domains (not just the base domain) + output_interfaces = length(local.rulesFiltered) == 0 || length(local.domains) == 0 ? {} : merge([ + for route_key, route in local.rulesFiltered : { + for domain_key, domain in local.domains : + "${route_key}--${domain_key}" => { + connection_string = local.is_auth_enabled ? "https://${local.username}:${local.password}@${ + lookup(route, "domain_prefix", null) == null || lookup(route, "domain_prefix", null) == "" ? + domain.domain : + "${lookup(route, "domain_prefix", null)}.${domain.domain}" + }" : "https://${ + lookup(route, "domain_prefix", null) == null || lookup(route, "domain_prefix", null) == "" ? + domain.domain : + "${lookup(route, "domain_prefix", null)}.${domain.domain}" + }" + host = ( + lookup(route, "domain_prefix", null) == null || lookup(route, "domain_prefix", null) == "" ? + domain.domain : + "${lookup(route, "domain_prefix", null)}.${domain.domain}" + ) + port = 443 + username = local.username + password = local.password + secrets = local.is_auth_enabled ? ["connection_string", "password"] : [] + } } - } + ]...) } output "domains" { @@ -85,8 +102,11 @@ output "subdomain" { } output "tls_secret" { - value = local.dns_validation_secret_name - description = "TLS certificate secret name" + value = { + for domain_key, domain in local.domains : + domain_key => lookup(domain, "certificate_reference", "") != "" ? domain.certificate_reference : "${local.name}-${domain_key}-tls-cert" + } + description = "Map of domain keys to their TLS certificate secret names" } output "load_balancer_hostname" { @@ -98,3 +118,44 @@ output "load_balancer_ip" { value = local.lb_ip description = "Load balancer IP address (for A records)" } + +output "lb_record_value" { + value = local.lb_record_value + description = "The value to use in DNS records (hostname or IP)" +} + +output "record_type" { + value = local.record_type + description = "DNS record type (CNAME or A)" +} + +output "name" { + value = local.name + description = "The computed resource name" +} + +output "base_domain" { + value = local.base_domain + description = "The computed base domain" +} + +output "base_subdomain" { + value = local.base_subdomain + description = "The wildcard base subdomain" +} + +output "username" { + value = local.username + description = "Basic auth username (empty if disabled)" +} + +output "password" { + value = local.password + sensitive = true + description = "Basic auth password (empty if disabled)" +} + +output "cloud_provider" { + value = local.cloud_provider + description = "Detected cloud provider (AWS, GCP, AZURE, OVH)" +} From 63e77e9391df41a5b266b5cdc6ff2946604f1ce8 Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Mon, 16 Mar 2026 12:13:32 +0530 Subject: [PATCH 02/13] feat: add ack_acm_controller legacy module for managing ACM certificates via ACK Deploys the AWS ACK ACM Controller on EKS via Helm with IRSA. Enables managing ACM certificates as Kubernetes CRDs and exporting them to K8s TLS secrets for use with Gateway API listeners. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ack_acm_controller/legacy/1.0/README.md | 147 ++++++++++++++++++ .../ack_acm_controller/legacy/1.0/facets.yaml | 58 +++++++ modules/ack_acm_controller/legacy/1.0/main.tf | 124 +++++++++++++++ .../ack_acm_controller/legacy/1.0/outputs.tf | 11 ++ .../legacy/1.0/variables.tf | 33 ++++ outputs/ack_acm_controller/output.facets.yaml | 26 ++++ 6 files changed, 399 insertions(+) create mode 100644 modules/ack_acm_controller/legacy/1.0/README.md create mode 100644 modules/ack_acm_controller/legacy/1.0/facets.yaml create mode 100644 modules/ack_acm_controller/legacy/1.0/main.tf create mode 100644 modules/ack_acm_controller/legacy/1.0/outputs.tf create mode 100644 modules/ack_acm_controller/legacy/1.0/variables.tf create mode 100644 outputs/ack_acm_controller/output.facets.yaml diff --git a/modules/ack_acm_controller/legacy/1.0/README.md b/modules/ack_acm_controller/legacy/1.0/README.md new file mode 100644 index 00000000..e3199723 --- /dev/null +++ b/modules/ack_acm_controller/legacy/1.0/README.md @@ -0,0 +1,147 @@ +# ACK ACM Controller (Legacy) + +Deploys the AWS Controllers for Kubernetes (ACK) ACM Controller on EKS clusters, enabling management of AWS Certificate Manager certificates as Kubernetes resources. + +## Overview + +This module installs the ACK ACM Controller via Helm and configures IAM Role for Service Accounts (IRSA) so the controller can manage ACM certificates. Once deployed, you can create `Certificate` custom resources in Kubernetes that provision real ACM certificates and export them to Kubernetes TLS secrets. + +This is primarily used by the `nginx_gateway_fabric_legacy_aws` module to handle domains with ACM ARN `certificate_reference`. + +## Architecture + +``` + ┌──────────────────────────────────────────┐ + │ ACK ACM Controller │ + │ │ + │ 1. Watches Certificate CRDs │ + │ 2. Calls ACM API via IRSA │ + │ 3. Requests/validates certificates │ + │ 4. Exports cert to K8s TLS secret │ + │ │ + │ IAM: acm:RequestCertificate, │ + │ acm:DescribeCertificate, │ + │ acm:GetCertificate, ... │ + └──────────────────────────────────────────┘ + │ │ + ▼ ▼ + AWS ACM (Certificate) K8s TLS Secret + (DNS validation) (exported cert) +``` + +## What It Creates + +| Resource | Description | +|----------|-------------| +| `aws_iam_policy` | IAM policy granting ACM permissions | +| IRSA module | IAM role bound to the controller's service account | +| `helm_release` | ACK ACM Controller deployment from `oci://public.ecr.aws/aws-controllers-k8s/acm-chart` | + +## Configuration + +### Basic Example + +```json +{ + "kind": "ack_acm_controller", + "flavor": "legacy", + "version": "1.0", + "spec": { + "chart_version": "1.3.4" + } +} +``` + +### With Custom Namespace + +```json +{ + "kind": "ack_acm_controller", + "flavor": "legacy", + "version": "1.0", + "spec": { + "chart_version": "1.3.4", + "namespace": "cert-manager" + } +} +``` + +## Spec Options + +| Field | Type | Default | Required | Description | +|-------|------|---------|----------|-------------| +| `chart_version` | string | `1.3.4` | Yes | Helm chart version of the ACK ACM Controller | +| `namespace` | string | environment namespace | No | Kubernetes namespace for the controller | +| `helm_values` | object | - | No | Additional Helm values to override defaults | + +## Inputs + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `kubernetes_details` | `@outputs/kubernetes` | Yes | Kubernetes cluster connection (provides OIDC provider ARN for IRSA) | + +## Outputs + +| Attribute | Description | +|-----------|-------------| +| `namespace` | Namespace where the controller is deployed | +| `release_name` | Helm release name | +| `chart_version` | Deployed chart version | +| `role_arn` | IAM role ARN used by the controller | +| `helm_release_id` | Helm release ID | + +## Usage with nginx_gateway_fabric_legacy_aws + +Once deployed, the `nginx_gateway_fabric_legacy_aws` module can use ACM ARNs as `certificate_reference` on domains: + +```json +{ + "kind": "ingress", + "flavor": "nginx_gateway_fabric_legacy_aws", + "version": "1.0", + "spec": { + "domains": { + "production": { + "domain": "api.example.com", + "alias": "prod", + "certificate_reference": "arn:aws:acm:us-east-1:123456789:certificate/abc-123" + } + } + } +} +``` + +The ingress module detects the ACM ARN, creates a `Certificate` CRD (kind: `acm.services.k8s.aws/v1alpha1`), and the ACK controller provisions the certificate and exports it to a K8s TLS secret. + +## Prerequisites + +- EKS cluster with OIDC provider configured +- Route53 hosted zone for DNS validation of ACM certificates + +## Troubleshooting + +### Check Controller Status + +```bash +kubectl get pods -n -l app.kubernetes.io/name=acm-chart +kubectl logs -n -l app.kubernetes.io/name=acm-chart +``` + +### Check ACM Certificate CRDs + +```bash +kubectl get certificate.acm.services.k8s.aws -n +kubectl describe certificate.acm.services.k8s.aws -n +``` + +### Check IRSA + +```bash +kubectl get sa ack-acm-controller -n -o yaml | grep eks.amazonaws.com/role-arn +``` + +### Common Issues + +- **Certificate stuck in pending**: Check Route53 DNS validation records are created. The ACM controller needs DNS validation to complete. +- **IAM errors**: Verify the IRSA role ARN in the service account annotation matches the IAM role with ACM permissions. +- **Export failure**: The target K8s secret must exist before the controller can export. The ingress module pre-creates empty TLS secrets for this purpose. diff --git a/modules/ack_acm_controller/legacy/1.0/facets.yaml b/modules/ack_acm_controller/legacy/1.0/facets.yaml new file mode 100644 index 00000000..40c79812 --- /dev/null +++ b/modules/ack_acm_controller/legacy/1.0/facets.yaml @@ -0,0 +1,58 @@ +intent: ack_acm_controller +flavor: legacy +version: "1.0" +description: | + Deploys the AWS ACK ACM Controller on EKS clusters via Helm. + This controller manages AWS Certificate Manager (ACM) certificates as Kubernetes resources + and can export issued certificates to Kubernetes TLS secrets for use with Gateway API or Ingress. + Uses IRSA for secure IAM authentication. Supports DNS validation via Route53. +clouds: + - aws + - kubernetes +inputs: + kubernetes_details: + type: "@outputs/kubernetes" + optional: false + default: + resource_type: kubernetes_cluster + resource_name: default + providers: + - kubernetes + - kubernetes-alpha + - helm +outputs: + default: + type: "@outputs/ack_acm_controller" +spec: + title: ACK ACM Controller Configuration + type: object + properties: + chart_version: + type: string + default: "1.3.4" + title: Chart Version + description: Helm chart version of the ACK ACM Controller + namespace: + type: string + title: Namespace + description: Kubernetes namespace for the controller (defaults to environment namespace) + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + maxLength: 63 + helm_values: + type: object + title: Helm Values + description: Additional Helm values to override defaults + x-ui-yaml-editor: true + required: + - chart_version + x-ui-order: + - chart_version + - namespace + - helm_values +sample: + kind: ack_acm_controller + flavor: legacy + version: "1.0" + disabled: true + spec: + chart_version: "1.3.4" diff --git a/modules/ack_acm_controller/legacy/1.0/main.tf b/modules/ack_acm_controller/legacy/1.0/main.tf new file mode 100644 index 00000000..27b08beb --- /dev/null +++ b/modules/ack_acm_controller/legacy/1.0/main.tf @@ -0,0 +1,124 @@ +data "aws_region" "current" {} + +module "name" { + source = "github.com/Facets-cloud/facets-utility-modules//name" + environment = var.environment + limit = 48 + resource_name = var.instance_name + resource_type = "ack_acm_controller" + globally_unique = true +} + +locals { + name = module.name.name + controller_namespace = coalesce(lookup(var.instance.spec, "namespace", null), var.environment.namespace) + controller_service_account = "ack-acm-controller" + eks_oidc_provider_arn = try(var.inputs.kubernetes_details.attributes.legacy_outputs.k8s_details.oidc_provider_arn, var.baseinfra.k8s_details.oidc_provider_arn) + aws_region = data.aws_region.current.name + + # Tolerations: merge environment defaults with facets dedicated tolerations + tolerations = concat( + lookup(var.environment, "default_tolerations", []), + try(var.inputs.kubernetes_details.attributes.legacy_outputs.facets_dedicated_tolerations, []) + ) + + # Node selector from kubernetes_details legacy outputs + node_selector = try(var.inputs.kubernetes_details.attributes.legacy_outputs.facets_dedicated_node_selectors, {}) + + controller_tolerations = [ + for taint in local.tolerations : { + key = taint.key + operator = "Equal" + value = lookup(taint, "value", "") + effect = taint.effect + } + ] +} + +# IAM Policy for ACK ACM Controller +resource "aws_iam_policy" "ack_acm" { + name = "${local.name}-ack-acm" + description = "IAM policy for the ACK ACM Controller" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "acm:RequestCertificate", + "acm:DescribeCertificate", + "acm:ListCertificates", + "acm:DeleteCertificate", + "acm:ExportCertificate", + "acm:GetCertificate", + "acm:ListTagsForCertificate", + "acm:AddTagsToCertificate", + "acm:RemoveTagsFromCertificate", + "acm:UpdateCertificateOptions", + "acm:ImportCertificate", + "acm:RenewCertificate" + ] + Resource = "*" + } + ] + }) +} + +# IRSA - IAM Role for Service Account +module "irsa" { + source = "github.com/Facets-cloud/facets-utility-modules//aws_irsa" + eks_oidc_provider_arn = local.eks_oidc_provider_arn + iam_role_name = local.name + namespace = local.controller_namespace + sa_name = local.controller_service_account + iam_arns = { + (local.controller_service_account) = { + arn = aws_iam_policy.ack_acm.arn + } + } +} + +# Deploy ACK ACM Controller via Helm +resource "helm_release" "ack_acm" { + namespace = local.controller_namespace + create_namespace = true + name = "ack-acm-controller" + repository = "oci://public.ecr.aws/aws-controllers-k8s" + chart = "acm-chart" + version = var.instance.spec.chart_version + wait = true + timeout = 600 + + values = [ + yamlencode(merge({ + aws = { + region = local.aws_region + } + serviceAccount = { + create = true + name = local.controller_service_account + annotations = { + "eks.amazonaws.com/role-arn" = module.irsa.iam_role_arn + } + } + resources = { + requests = { + cpu = "50m" + memory = "64Mi" + } + limits = { + cpu = "100m" + memory = "128Mi" + } + } + nodeSelector = local.node_selector + tolerations = local.controller_tolerations + installScope = "cluster" + }, lookup(var.instance.spec, "helm_values", {}))) + ] + + depends_on = [ + module.irsa + ] +} diff --git a/modules/ack_acm_controller/legacy/1.0/outputs.tf b/modules/ack_acm_controller/legacy/1.0/outputs.tf new file mode 100644 index 00000000..a11d2c70 --- /dev/null +++ b/modules/ack_acm_controller/legacy/1.0/outputs.tf @@ -0,0 +1,11 @@ +locals { + output_attributes = { + namespace = local.controller_namespace + release_name = helm_release.ack_acm.name + chart_version = var.instance.spec.chart_version + role_arn = module.irsa.iam_role_arn + helm_release_id = helm_release.ack_acm.id + } + + output_interfaces = {} +} diff --git a/modules/ack_acm_controller/legacy/1.0/variables.tf b/modules/ack_acm_controller/legacy/1.0/variables.tf new file mode 100644 index 00000000..8ea33853 --- /dev/null +++ b/modules/ack_acm_controller/legacy/1.0/variables.tf @@ -0,0 +1,33 @@ +variable "cluster" { + type = any + default = {} +} + +variable "baseinfra" { + type = any + default = {} +} + +variable "cc_metadata" { + type = any + default = {} +} + +variable "instance" { + type = any +} + +variable "instance_name" { + type = string + default = "test_instance" +} + +variable "environment" { + type = any + default = {} +} + +variable "inputs" { + type = any + default = {} +} diff --git a/outputs/ack_acm_controller/output.facets.yaml b/outputs/ack_acm_controller/output.facets.yaml new file mode 100644 index 00000000..e1e3aab4 --- /dev/null +++ b/outputs/ack_acm_controller/output.facets.yaml @@ -0,0 +1,26 @@ +name: '@outputs/ack_acm_controller' +properties: + type: object + properties: + attributes: + type: object + properties: + namespace: + description: Kubernetes namespace where the controller is deployed + type: string + release_name: + description: Helm release name + type: string + chart_version: + description: Helm chart version + type: string + role_arn: + description: IAM role ARN used by the controller + type: string + helm_release_id: + description: Helm release ID + type: string + interfaces: + type: object + properties: {} +providers: [] From ffd63292ab7c40b8f925a3a181f314cd45c42977 Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Mon, 16 Mar 2026 12:13:54 +0530 Subject: [PATCH 03/13] feat: add nginx_gateway_fabric_legacy_aws module with DNS-01 wildcard cert support AWS-specific wrapper for nginx_gateway_fabric with NLB, Proxy Protocol v2, ACM certificate integration, and DNS-01 wildcard certificate support. When use_dns01 is enabled, all domain listeners switch to wildcard hostnames (*.domain) and certificates are issued via a pre-existing ClusterIssuer (default: gts-production). Base domain is also covered by DNS-01 when active. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../1.0/README.md | 259 +++++ .../1.0/facets.yaml | 925 ++++++++++++++++++ .../1.0/main.tf | 274 ++++++ .../1.0/outputs.tf | 90 ++ .../1.0/variables.tf | 37 + 5 files changed, 1585 insertions(+) create mode 100644 modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/README.md create mode 100644 modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/facets.yaml create mode 100644 modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf create mode 100644 modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/outputs.tf create mode 100644 modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/variables.tf diff --git a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/README.md b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/README.md new file mode 100644 index 00000000..e5b529ef --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/README.md @@ -0,0 +1,259 @@ +# NGINX Gateway Fabric (AWS Legacy) + +Kubernetes Gateway API implementation for AWS with NLB, Proxy Protocol v2, ACM, and DNS-01 support. + +## Overview + +This module is an **AWS-specific wrapper** around the base `nginx_gateway_fabric` utility module. It adds: + +- **AWS NLB**: Network Load Balancer with Proxy Protocol v2 and IP target type +- **ACM Integration**: Use AWS Certificate Manager ARNs as `certificate_reference` — the module creates ACK Certificate CRDs and manages TLS secret lifecycle +- **DNS-01 Wildcard Certificates**: Optional DNS-01 validation via a pre-existing ClusterIssuer (e.g., `gts-production`) for wildcard listeners + +This is the **legacy** flavor that uses `cc_metadata` and legacy input conventions. + +--- + +## Architecture + +``` + ┌──────────────────────────────────────────────┐ + │ AWS Wrapper (this module) │ + │ │ + │ 1. ACM ARN → K8s secret rewrite │ + │ 2. DNS-01 → certificate_reference rewrite │ + │ 3. AWS NLB annotations │ + │ │ + │ ┌──────────────────────────────────┐ │ + │ │ Base Utility Module │ │ + Internet ────────► │ │ - Gateway + Listeners │ │ + (NLB) │ │ - HTTPRoute / GRPCRoute │ │ + │ │ - Helm chart deployment │ │ + │ │ - HTTP-01 certs (if needed) │ │ + │ └──────────────────────────────────┘ │ + └──────────────────────────────────────────────┘ +``` + +--- + +## TLS Certificate Flows + +Three mutually exclusive certificate strategies per domain: + +| Domain has | Flow | Listener | Managed by | +|---|---|---|---| +| ACM ARN in `certificate_reference` | ACK ACM Certificate CRD | Wildcard (`*.domain`) | AWS wrapper | +| No cert ref + `use_dns01: true` | cert-manager DNS-01 via ClusterIssuer | Wildcard (`*.domain`) | AWS wrapper | +| No cert ref + `use_dns01: false` | cert-manager HTTP-01 (default) | Exact hostname | Utility module | +| K8s secret in `certificate_reference` | User-managed | Wildcard (`*.domain`) | User | + +### HTTP-01 (Default) + +No extra configuration needed. The utility module creates a bundled HTTP-01 ClusterIssuer and issues certificates automatically via Let's Encrypt. + +### DNS-01 Wildcard Certificates + +When `use_dns01: true`, the module: + +1. Creates cert-manager `Certificate` resources requesting wildcard certs (`*.domain` + `domain`) from the specified ClusterIssuer +2. Creates bootstrap self-signed TLS secrets so Gateway listeners can start immediately +3. Sets `certificate_reference` on all domains, causing the utility module to create wildcard listeners (`*.domain`) +4. Disables the utility module's HTTP-01 ClusterIssuer and bootstrap cert creation (no domains left without `certificate_reference`) + +**Prerequisite**: The ClusterIssuer (default: `gts-production`) must already exist in the cluster with a DNS-01 solver configured. + +```json +{ + "spec": { + "use_dns01": true, + "dns01_cluster_issuer": "gts-production", + "domains": { + "production": { + "domain": "api.example.com", + "alias": "prod" + } + } + } +} +``` + +This creates: +- Wildcard listener: `*.api.example.com` +- Certificate: `*.api.example.com` + `api.example.com` via `gts-production` +- Base domain also gets a wildcard listener and DNS-01 cert + +### ACM Certificates + +Use an ACM ARN as `certificate_reference`. The module creates an ACK Certificate CRD that provisions the cert and exports it to a K8s TLS secret. + +```json +{ + "spec": { + "domains": { + "production": { + "domain": "api.example.com", + "alias": "prod", + "certificate_reference": "arn:aws:acm:us-east-1:123456789:certificate/abc-123" + } + } + } +} +``` + +**Prerequisite**: The `ack_acm_controller` must be deployed (optional input). + +### Mixed Mode + +ACM and DNS-01 can coexist. ACM domains keep their ACM flow; domains without `certificate_reference` use DNS-01 when `use_dns01: true`. + +```json +{ + "spec": { + "use_dns01": true, + "domains": { + "acm_domain": { + "domain": "acm.example.com", + "alias": "acm", + "certificate_reference": "arn:aws:acm:us-east-1:123456789:certificate/abc-123" + }, + "dns01_domain": { + "domain": "dns.example.com", + "alias": "dns" + } + } + } +} +``` + +--- + +## Configuration + +### Basic Example + +```json +{ + "kind": "ingress", + "flavor": "nginx_gateway_fabric_legacy_aws", + "version": "1.0", + "spec": { + "private": false, + "force_ssl_redirection": true, + "rules": { + "api": { + "service_name": "api-service", + "namespace": "default", + "port": "8080", + "path": "/api", + "path_type": "PathPrefix" + } + } + } +} +``` + +### DNS-01 + Private LB Example + +```json +{ + "kind": "ingress", + "flavor": "nginx_gateway_fabric_legacy_aws", + "version": "1.0", + "spec": { + "private": true, + "force_ssl_redirection": true, + "use_dns01": true, + "dns01_cluster_issuer": "gts-production", + "domains": { + "internal": { + "domain": "internal.example.com", + "alias": "internal" + } + }, + "rules": { + "api": { + "service_name": "api-service", + "namespace": "default", + "port": "8080", + "path": "/api" + } + } + } +} +``` + +--- + +## Spec Options + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `private` | boolean | `false` | Use internal NLB | +| `force_ssl_redirection` | boolean | `true` | Redirect HTTP to HTTPS | +| `disable_base_domain` | boolean | `false` | Disable auto-generated base domain | +| `domain_prefix_override` | string | - | Override auto-generated domain prefix | +| `use_dns01` | boolean | `false` | Enable DNS-01 wildcard certificates for all domains | +| `dns01_cluster_issuer` | string | `gts-production` | ClusterIssuer name for DNS-01 validation | +| `disable_endpoint_validation` | boolean | `false` | Disable HTTP endpoint validation | +| `basic_auth` | boolean | `false` | Enable basic authentication | +| `body_size` | string | `150m` | Maximum client request body size | +| `helm_wait` | boolean | `true` | Wait for Helm release to be ready | +| `helm_values` | object | - | Additional Helm values | + +--- + +## Inputs + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `kubernetes_details` | `@outputs/kubernetes` | Yes | Kubernetes cluster connection | +| `gateway_api_crd_details` | `@outputs/gateway_api_crd` | Yes | Gateway API CRD installation | +| `prometheus_details` | `@outputs/prometheus` | No | Prometheus for PodMonitor | +| `ack_acm_controller_details` | `@outputs/ack_acm_controller` | No | ACK ACM controller (required for ACM ARN domains) | + +--- + +## AWS-Specific Behavior + +### NLB Configuration + +- **Public**: `internet-facing` scheme with Proxy Protocol v2 and client IP preservation +- **Private**: `internal` scheme with Proxy Protocol v2, client IP preservation disabled +- Target type: IP (for direct pod routing) +- Load balancer class: `service.k8s.aws/nlb` + +### Proxy Protocol v2 + +Always enabled. The module configures `NginxProxy` CRD with `rewriteClientIP` in ProxyProtocol mode to correctly extract client IPs from NLB. + +--- + +## Troubleshooting + +### Check DNS-01 Certificate Status + +```bash +kubectl get certificate -n +kubectl describe certificate -dns01-cert- -n +kubectl get clusterissuer gts-production -o yaml +``` + +### Check ACM Certificate Status + +```bash +kubectl get certificate.acm.services.k8s.aws -n +kubectl describe certificate.acm.services.k8s.aws -acm-cert- -n +``` + +### Check Gateway Listeners + +```bash +kubectl get gateway -n -o yaml | grep -A 20 listeners +``` + +### NLB Issues + +```bash +kubectl get svc -n -l app.kubernetes.io/name=nginx-gateway-fabric +kubectl describe svc -n +``` diff --git a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/facets.yaml b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/facets.yaml new file mode 100644 index 00000000..c12d771b --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/facets.yaml @@ -0,0 +1,925 @@ +intent: ingress +flavor: nginx_gateway_fabric_legacy_aws +version: "1.0" +description: NGINX Gateway Fabric for AWS - Gateway API with NLB, Proxy Protocol v2, and ACM support (Legacy module format) +clouds: + - aws + - kubernetes +inputs: + kubernetes_details: + type: "@outputs/kubernetes" + optional: false + default: + resource_type: kubernetes_cluster + resource_name: default + providers: + - kubernetes + - kubernetes-alpha + - helm + gateway_api_crd_details: + type: "@outputs/gateway_api_crd" + optional: false + default: + resource_type: gateway_api_crd + resource_name: default + prometheus_details: + type: "@outputs/prometheus" + optional: true + default: + resource_type: configuration + resource_name: prometheus + ack_acm_controller_details: + type: "@outputs/ack_acm_controller" + optional: true +outputs: + default: + type: "@outputs/ingress" +spec: + title: NGINX Gateway Fabric (AWS) + type: object + properties: + private: + type: boolean + title: Private Load Balancer + description: Set load balancer as private/internal + disable_base_domain: + type: boolean + title: Disable Base Domain + description: Disable automatic creation of base domain for this gateway + default: false + domains: + title: Domains + description: Map of domain key to rules + type: object + x-ui-overrides-only: true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + title: Domain Object + description: Name of the domain object + type: object + properties: + domain: + type: string + title: Domain + description: Host name of the domain + pattern: ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*\.[A-Za-z]{2,6}$ + x-ui-unique: true + x-ui-placeholder: "Domain to map ingress. Eg: example.com, sub.example.com, my-domain.co.uk" + x-ui-error-message: "Value doesn't match the format. Eg: example.com, my-domain.co.uk" + alias: + type: string + title: Alias + description: Alias for the domain + certificate_reference: + type: string + title: Certificate Reference + description: Name of an existing TLS secret or ACM ARN (arn:aws:acm:...) to use instead of cert-manager managed certificates + x-ui-toggle: true + required: + - domain + - alias + data_plane: + type: object + title: Data Plane (NGINX) + description: Configuration for NGINX pods that handle actual traffic + properties: + scaling: + type: object + title: Autoscaling + description: Horizontal Pod Autoscaler configuration + properties: + min_replicas: + type: integer + title: Minimum Replicas + description: Minimum number of NGINX pods (HPA lower bound) + default: 2 + max_replicas: + type: integer + title: Maximum Replicas + description: Maximum number of NGINX pods (HPA upper bound) + default: 10 + target_cpu_utilization_percentage: + type: integer + title: Target CPU Utilization (%) + description: Target CPU utilization percentage for HPA scaling + default: 70 + target_memory_utilization_percentage: + type: integer + title: Target Memory Utilization (%) + description: Target memory utilization percentage for HPA scaling + default: 80 + resources: + type: object + title: Resources + description: CPU and memory resources per NGINX pod + properties: + requests: + type: object + title: Resource Requests + description: Minimum resource requirements + properties: + cpu: + type: string + title: CPU Request + description: Minimum CPU requirement (e.g., 250m, 500m, 1) + pattern: ^([0-9]*\.?[0-9]+m?|[0-9]+)$ + default: 250m + x-ui-placeholder: 250m + memory: + type: string + title: Memory Request + description: Minimum memory requirement (e.g., 256Mi, 512Mi, 1Gi) + pattern: ^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$ + default: 256Mi + x-ui-placeholder: 256Mi + limits: + type: object + title: Resource Limits + description: Maximum resource limits + properties: + cpu: + type: string + title: CPU Limit + description: Maximum CPU allowed (e.g., 1, 2, 500m) + pattern: ^([0-9]*\.?[0-9]+m?|[0-9]+)$ + default: "1" + x-ui-placeholder: "1" + memory: + type: string + title: Memory Limit + description: Maximum memory allowed (e.g., 512Mi, 1Gi, 2Gi) + pattern: ^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$ + default: 512Mi + x-ui-placeholder: 512Mi + control_plane: + type: object + title: Control Plane (Gateway Controller) + description: Configuration for the NGINX Gateway Fabric controller that manages configuration + x-ui-toggle: true + properties: + scaling: + type: object + title: Autoscaling + description: Horizontal Pod Autoscaler configuration + properties: + min_replicas: + type: integer + title: Minimum Replicas + description: Minimum number of controller pods (HPA lower bound) + default: 2 + max_replicas: + type: integer + title: Maximum Replicas + description: Maximum number of controller pods (HPA upper bound) + default: 3 + target_cpu_utilization_percentage: + type: integer + title: Target CPU Utilization (%) + description: Target CPU utilization percentage for HPA scaling + default: 70 + target_memory_utilization_percentage: + type: integer + title: Target Memory Utilization (%) + description: Target memory utilization percentage for HPA scaling + default: 80 + resources: + type: object + title: Resources + description: CPU and memory resources per controller pod + properties: + requests: + type: object + title: Resource Requests + description: Minimum resource requirements + properties: + cpu: + type: string + title: CPU Request + description: Minimum CPU requirement (e.g., 100m, 250m, 500m) + pattern: ^([0-9]*\.?[0-9]+m?|[0-9]+)$ + default: 200m + x-ui-placeholder: 200m + memory: + type: string + title: Memory Request + description: Minimum memory requirement (e.g., 256Mi, 512Mi) + pattern: ^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$ + default: 256Mi + x-ui-placeholder: 256Mi + limits: + type: object + title: Resource Limits + description: Maximum resource limits + properties: + cpu: + type: string + title: CPU Limit + description: Maximum CPU allowed (e.g., 500m, 1, 2) + pattern: ^([0-9]*\.?[0-9]+m?|[0-9]+)$ + default: 500m + x-ui-placeholder: 500m + memory: + type: string + title: Memory Limit + description: Maximum memory allowed (e.g., 512Mi, 1Gi) + pattern: ^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$ + default: 512Mi + x-ui-placeholder: 512Mi + rules: + title: Routing Rules + description: Gateway API routing configurations (HTTP and gRPC) + type: object + patternProperties: + ^[a-zA-Z0-9_.-]*$: + title: Route Object + description: HTTP route configuration + type: object + properties: + disable: + type: boolean + title: Disable Route + description: Enable/Disable this route + domain_prefix: + type: string + title: Domain Prefix + description: Subdomain prefix + pattern: ^[a-z0-9]([a-z0-9-]{0,34}[a-z0-9])?$|^[a-z0-9]$ + x-ui-placeholder: Enter the subdomain prefix + x-ui-error-message: Max 36 characters, only hyphens allowed, cannot start/end with hyphen + service_name: + type: string + title: Service Name + description: Kubernetes service name + x-ui-api-source: + endpoint: /cc-ui/v1/dropdown/stack/{{stackName}}/resources-info + method: GET + params: + includeContent: false + labelKey: resourceName + valueKey: resourceName + valueTemplate: ${service.{{value}}.out.attributes.service_name} + filterConditions: + - field: resourceType + value: service + x-ui-typeable: true + namespace: + type: string + title: Service Namespace + description: Kubernetes service namespace + x-ui-api-source: + endpoint: /cc-ui/v1/dropdown/stack/{{stackName}}/resources-info + method: GET + params: + includeContent: false + labelKey: resourceName + valueKey: resourceName + valueTemplate: ${service.{{value}}.out.attributes.namespace} + filterConditions: + - field: resourceType + value: service + x-ui-typeable: true + port: + type: string + title: Port + description: Service port number + x-ui-api-source: + endpoint: /cc-ui/v1/dropdown/stack/{{stackName}}/service/{{serviceName}}/overview + dynamicProperties: + serviceName: + key: service_name + lookup: regex + x-ui-lookup-regex: \${[^.]+\.([^.]+).* + method: GET + labelKey: port + valueKey: port + path: ports + x-ui-typeable: true + path: + type: string + title: Path + description: Path of the application (required for HTTP routes) + pattern: ^(/[^\s{};^]*)$ + x-ui-placeholder: "Enter path (e.g., / or /api or /api/v[0-9]+)" + x-ui-error-message: "Path must start with /" + x-ui-visible-if: + field: spec.rules.{{this}}.grpc_config.enabled + values: + - false + - null + disable_auth: + type: boolean + title: Disable Auth for this Route + description: Disable basic authentication for this specific route (only relevant when basic_auth is enabled) + default: false + x-ui-visible-if: + field: spec.basic_auth + values: + - true + path_type: + type: string + title: Path Type + description: "Path matching type. RegularExpression (default) matches paths using regex for ingress-nginx parity. Use PathPrefix for prefix matching or Exact for exact path matching." + enum: + - RegularExpression + - PathPrefix + - Exact + default: RegularExpression + x-ui-visible-if: + field: spec.rules.{{this}}.grpc_config.enabled + values: + - false + - null + header_matches: + type: object + title: Header-Based Routing + description: Route traffic based on HTTP headers + x-ui-toggle: true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header Match + properties: + name: + type: string + title: Header Name + description: HTTP header name to match + x-ui-placeholder: X-API-Version + value: + type: string + title: Header Value + description: Header value to match + x-ui-placeholder: v2 + type: + type: string + title: Match Type + description: How to match the header value (Exact or RegularExpression) + enum: + - Exact + - RegularExpression + default: Exact + required: + - name + - value + query_param_matches: + type: object + title: Query Parameter Matching + description: Route traffic based on query parameters + x-ui-toggle: true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Query Param Match + properties: + name: + type: string + title: Parameter Name + description: Query parameter name to match + x-ui-placeholder: version + value: + type: string + title: Parameter Value + description: Query parameter value to match + x-ui-placeholder: beta + type: + type: string + title: Match Type + description: How to match the parameter value (Exact or RegularExpression) + enum: + - Exact + - RegularExpression + default: Exact + required: + - name + - value + method: + type: string + title: HTTP Method + description: Match requests by HTTP method + enum: + - ALL + - GET + - POST + - PUT + - DELETE + - PATCH + - HEAD + - OPTIONS + default: ALL + x-ui-toggle: true + request_header_modifier: + type: object + title: Request Header Modification + description: Modify request headers + x-ui-toggle: true + properties: + add: + type: object + title: Add Headers + description: Headers to add to requests + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to add + x-ui-placeholder: X-Custom-Header + value: + type: string + title: Header Value + description: Value for the header + x-ui-placeholder: custom-value + required: + - name + - value + set: + type: object + title: Set Headers + description: Headers to set (override) in requests + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to set + x-ui-placeholder: X-Request-Source + value: + type: string + title: Header Value + description: Value for the header + x-ui-placeholder: gateway + required: + - name + - value + remove: + type: object + title: Remove Headers + description: Header names to remove from requests + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to remove + x-ui-placeholder: X-Sensitive-Header + required: + - name + response_header_modifier: + type: object + title: Response Header Modification + description: Modify response headers + x-ui-toggle: true + properties: + add: + type: object + title: Add Headers + description: Headers to add to responses + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to add + x-ui-placeholder: X-Response-ID + value: + type: string + title: Header Value + description: Value for the header + x-ui-placeholder: unique-id + required: + - name + - value + set: + type: object + title: Set Headers + description: Headers to set (override) in responses + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to set + x-ui-placeholder: Cache-Control + value: + type: string + title: Header Value + description: Value for the header + x-ui-placeholder: no-store + required: + - name + - value + remove: + type: object + title: Remove Headers + description: Header names to remove from responses + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to remove + x-ui-placeholder: Server + required: + - name + url_rewrite: + type: object + title: URL Rewriting + description: Rewrite request URLs + x-ui-toggle: true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: URL Rewrite Rule + properties: + hostname: + type: string + title: Hostname + description: New hostname for the request + x-ui-placeholder: internal-api.svc.cluster.local + path_type: + type: string + title: Path Rewrite Type + description: How to rewrite the path (ReplaceFullPath or ReplacePrefixMatch) + enum: + - ReplaceFullPath + - ReplacePrefixMatch + replace_path: + type: string + title: Replace Path Value + description: New path value (full path or prefix depending on type) + x-ui-placeholder: /v2/api + canary_deployment: + type: object + title: Canary Deployment + description: Configure traffic splitting for canary deployments + x-ui-toggle: true + x-ui-skip: true + properties: + enabled: + type: boolean + title: Enable Canary + description: Enable canary deployment with traffic splitting + default: false + canary_service: + type: string + title: Canary Service Name + description: Name of the canary version service + x-ui-visible-if: + field: spec.rules.{{this}}.canary_deployment.enabled + values: + - true + canary_weight: + type: integer + title: Canary Traffic Percentage + description: Percentage of traffic to send to canary (0-100) + minimum: 0 + maximum: 100 + default: 10 + x-ui-visible-if: + field: spec.rules.{{this}}.canary_deployment.enabled + values: + - true + request_mirror: + type: object + title: Request Mirroring + description: Mirror traffic to a secondary backend for testing + x-ui-toggle: true + x-ui-skip: true + properties: + service_name: + type: string + title: Mirror Service Name + description: Name of the service to mirror traffic to + port: + type: string + title: Mirror Service Port + description: Port of the mirror service + namespace: + type: string + title: Mirror Service Namespace + description: Namespace of the mirror service + timeouts: + type: object + title: Request Timeouts + description: Configure request and backend timeouts + x-ui-toggle: true + properties: + request: + type: string + title: Request Timeout + description: Total request timeout (e.g., 30s, 1m, 5m) + pattern: ^\d+[smh]$ + default: 300s + x-ui-placeholder: "300s" + backend_request: + type: string + title: Backend Request Timeout + description: Backend response timeout (e.g., 30s, 1m, 5m) + default: 300s + pattern: ^\d+[smh]$ + x-ui-placeholder: "300s" + cors: + type: object + title: CORS Configuration + description: Configure Cross-Origin Resource Sharing + x-ui-toggle: true + properties: + enabled: + type: boolean + title: Enable CORS + description: Enable Cross-Origin Resource Sharing headers + default: false + allow_origins: + type: object + title: Allowed Origins + description: List of allowed origins (* for all) + x-ui-visible-if: + field: spec.rules.{{this}}.cors.enabled + values: + - true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Origin + properties: + origin: + type: string + title: Origin URL + description: Allowed origin URL + x-ui-placeholder: https://example.com + required: + - origin + allow_methods: + type: object + title: Allowed Methods + description: HTTP methods allowed for CORS requests + x-ui-visible-if: + field: spec.rules.{{this}}.cors.enabled + values: + - true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Method + properties: + method: + type: string + title: HTTP Method + description: HTTP method to allow + enum: + - GET + - POST + - PUT + - DELETE + - PATCH + - OPTIONS + - HEAD + required: + - method + allow_headers: + type: object + title: Allowed Headers + description: HTTP headers allowed in CORS requests + x-ui-visible-if: + field: spec.rules.{{this}}.cors.enabled + values: + - true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + header: + type: string + title: Header Name + description: Header name to allow + x-ui-placeholder: Content-Type + required: + - header + allow_credentials: + type: boolean + title: Allow Credentials + description: Allow cookies and authentication headers in CORS requests + default: false + x-ui-visible-if: + field: spec.rules.{{this}}.cors.enabled + values: + - true + max_age: + type: integer + title: Preflight Cache Max Age + description: Seconds to cache preflight response + default: 86400 + x-ui-visible-if: + field: spec.rules.{{this}}.cors.enabled + values: + - true + grpc_config: + type: object + title: gRPC Configuration + description: Configure gRPC routing + x-ui-toggle: true + properties: + enabled: + type: boolean + title: Enable gRPC + description: Enable gRPC routing for this service + default: false + match_all_methods: + type: boolean + title: Match All Methods + description: Route all gRPC traffic (no method filtering) + default: true + x-ui-visible-if: + field: spec.rules.{{this}}.grpc_config.enabled + values: + - true + method_match: + type: object + title: gRPC Method Matching + description: Whitelist specific gRPC services/methods + x-ui-visible-if: + field: spec.rules.{{this}}.grpc_config.match_all_methods + values: + - false + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Method Match + properties: + service: + type: string + title: Service Name + description: Protobuf service name (e.g., helloworld.Greeter) + method: + type: string + title: Method Name + description: gRPC method name (e.g., SayHello) + type: + type: string + title: Match Type + description: NGINX Gateway Fabric only supports Exact matching + enum: + - Exact + default: Exact + required: + - service + - method + required: + - service_name + - port + - namespace + x-ui-order: + - disable + - disable_auth + - domain_prefix + - service_name + - namespace + - port + - path + - path_type + - method + - timeouts + - cors + - header_matches + - query_param_matches + - url_rewrite + - request_header_modifier + - response_header_modifier + - grpc_config + # Disabling below things as too advanced for initial release + # - canary_deployment + # - request_mirror + force_ssl_redirection: + type: boolean + title: Force SSL Redirection + description: Force HTTP to HTTPS redirection + basic_auth: + type: boolean + title: Basic Authentication + description: Enable/disable basic auth for all routes (individual routes can opt out with disable_auth) + default: false + body_size: + type: string + title: Max Body Size + description: Maximum allowed size of the client request body (e.g., 150m, 1g) + pattern: ^\d{1,4}(k|m|g)?$ + default: 150m + x-ui-placeholder: 150m + x-ui-toggle: true + helm_values: + type: object + title: Additional Helm Values + description: "Additional Helm values to merge with the default configuration. See available values at: https://github.com/nginxinc/nginx-gateway-fabric/blob/main/charts/nginx-gateway-fabric/values.yaml" + x-ui-toggle: true + x-ui-yaml-editor: true + domain_prefix_override: + type: string + title: Domain Prefix Override + description: Override the automatically generated domain prefix + x-ui-toggle: true + disable_endpoint_validation: + type: boolean + title: Disable Endpoint Validation + description: Disable endpoint validation for cert-manager (uses DNS validation instead of HTTP) + default: false + x-ui-toggle: true + use_dns01: + type: boolean + title: Use DNS-01 Resolver + description: Use DNS-01 challenge type for certificate issuance. When enabled, all domain listeners use wildcard hostnames (*.domain) and certificates are issued via the DNS-01 ClusterIssuer. + default: false + x-ui-toggle: true + dns01_cluster_issuer: + type: string + title: DNS-01 ClusterIssuer + description: Name of the ClusterIssuer to use for DNS-01 certificate issuance + default: gts-production + x-ui-toggle: true + x-ui-visible-if: + field: spec.use_dns01 + values: + - true + helm_wait: + type: boolean + title: Helm Wait + description: Wait for all resources to be ready before marking the release as successful + default: true + x-ui-toggle: true + required: + - private + - rules + - force_ssl_redirection + x-ui-order: + - private + - domain_prefix_override + - disable_base_domain + - domains + - force_ssl_redirection + - basic_auth + - body_size + - disable_endpoint_validation + - use_dns01 + - dns01_cluster_issuer + - data_plane + - control_plane + - helm_values + - helm_wait + - rules +sample: + kind: ingress + flavor: nginx_gateway_fabric_legacy_aws + version: "1.0" + disabled: true + metadata: + annotations: {} + spec: + private: false + disable_base_domain: false + force_ssl_redirection: true + basic_auth: false + body_size: 150m + data_plane: + scaling: + min_replicas: 2 + max_replicas: 5 + target_cpu_utilization_percentage: 70 + target_memory_utilization_percentage: 80 + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: "1" + memory: 512Mi + control_plane: + scaling: + min_replicas: 2 + max_replicas: 3 + target_cpu_utilization_percentage: 70 + target_memory_utilization_percentage: 80 + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + rules: {} diff --git a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf new file mode 100644 index 00000000..26264a2d --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf @@ -0,0 +1,274 @@ +locals { + # Compute name the same way as the base module (needed for ACM secret names) + name = lower(var.environment.namespace == "default" ? var.instance_name : "${var.environment.namespace}-${var.instance_name}") + + # --- DNS-01 configuration --- + use_dns01 = lookup(var.instance.spec, "use_dns01", false) + dns01_cluster_issuer = lookup(var.instance.spec, "dns01_cluster_issuer", "gts-production") + + # Compute base domain (mirrors utility module logic — needed to take over base domain for DNS-01) + instance_env_name = length(var.environment.unique_name) + length(var.instance_name) + length(var.cc_metadata.tenant_base_domain) >= 60 ? substr(md5("${var.instance_name}-${var.environment.unique_name}"), 0, 20) : "${var.instance_name}-${var.environment.unique_name}" + check_domain_prefix = coalesce(lookup(var.instance.spec, "domain_prefix_override", null), local.instance_env_name) + base_domain = lower("${local.check_domain_prefix}.${var.cc_metadata.tenant_base_domain}") + + # --- ACM handling --- + # Detect domains with ACM ARN as certificate_reference + acm_cert_domains = { + for domain_key, domain in lookup(var.instance.spec, "domains", {}) : + domain_key => domain + if can(domain.certificate_reference) && length(regexall("arn:aws:acm:", lookup(domain, "certificate_reference", ""))) > 0 + } + + # K8s secret name for ACM cert domains — the ACK Certificate CRD exports cert to this secret + acm_cert_secret_names = { + for domain_key, domain in local.acm_cert_domains : + domain_key => "${local.name}-${domain_key}-acm-tls" + } + + # Rewrite instance domains: replace ACM ARN certificate_reference with K8s secret name + # The base module only sees K8s secret names — never ACM ARNs + acm_modified_domains = { + for domain_key, domain in lookup(var.instance.spec, "domains", {}) : + domain_key => contains(keys(local.acm_cert_secret_names), domain_key) ? merge(domain, { + certificate_reference = local.acm_cert_secret_names[domain_key] + }) : domain + } + + # --- DNS-01 domain handling --- + # User-configured domains eligible for DNS-01: use_dns01 enabled + no certificate_reference after ACM rewrite + dns01_domains = { + for domain_key, domain in local.acm_modified_domains : + domain_key => domain + if local.use_dns01 && lookup(domain, "certificate_reference", "") == "" + } + + # Base domain for DNS-01: when use_dns01 is enabled and base domain is not disabled, + # we take over the base domain from the utility module (set disable_base_domain=true + # and re-add it ourselves with certificate_reference for wildcard listener) + dns01_base_domain = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? { + "facets" = { + domain = local.base_domain + alias = "base" + } + } : {} + + # All domains that need DNS-01 wildcard certs (user domains + base domain) + all_dns01_domains = merge(local.dns01_domains, local.dns01_base_domain) + + # K8s secret names for DNS-01 wildcard certs + dns01_cert_secret_names = { + for domain_key, domain in local.all_dns01_domains : + domain_key => "${local.name}-${domain_key}-dns01-tls" + } + + # Final domain rewrite: apply DNS-01 certificate_reference on top of ACM rewrites + # Setting certificate_reference causes the utility module to use wildcard listeners (*.domain) + modified_domains = merge( + { + for domain_key, domain in local.acm_modified_domains : + domain_key => contains(keys(local.dns01_cert_secret_names), domain_key) ? merge(domain, { + certificate_reference = local.dns01_cert_secret_names[domain_key] + }) : domain + }, + # Add base domain with certificate_reference when DNS-01 is active + { + for domain_key, domain in local.dns01_base_domain : + domain_key => merge(domain, { + certificate_reference = local.dns01_cert_secret_names[domain_key] + }) + } + ) + + # Build modified instance with rewritten domains + # When DNS-01 is active: disable_base_domain=true (we re-add it ourselves with certificate_reference) + modified_instance = merge(var.instance, { + spec = merge(var.instance.spec, { + domains = local.modified_domains + disable_base_domain = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? true : lookup(var.instance.spec, "disable_base_domain", false) + }) + }) + + + # AWS NLB annotations + aws_annotations = merge( + lookup(var.instance.spec, "private", false) ? { + "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internal" + "service.beta.kubernetes.io/aws-load-balancer-internal" = "true" + } : { + "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internet-facing" + }, + { + "service.beta.kubernetes.io/aws-load-balancer-type" = "external" + "service.beta.kubernetes.io/aws-load-balancer-nlb-target-type" = "ip" + "service.beta.kubernetes.io/aws-load-balancer-backend-protocol" = "tcp" + "service.beta.kubernetes.io/aws-load-balancer-target-group-attributes" = lookup(var.instance.spec, "private", false) ? "proxy_protocol_v2.enabled=true,preserve_client_ip.enabled=false" : "proxy_protocol_v2.enabled=true,preserve_client_ip.enabled=true" + } + ) +} + +# Call the base utility module +module "nginx_gateway_fabric" { + source = "github.com/Facets-cloud/facets-utility-modules//nginx_gateway_fabric" + + instance = local.modified_instance + instance_name = var.instance_name + environment = var.environment + inputs = var.inputs + + service_annotations = local.aws_annotations + + load_balancer_class = "service.k8s.aws/nlb" + + nginx_proxy_extra_config = { + rewriteClientIP = { + mode = "ProxyProtocol" + trustedAddresses = [{ + type = "CIDR" + value = "0.0.0.0/0" + }] + } + } +} + +# ACK ACM Certificate CRD resources — creates ACM certificates via ACK controller +# and exports them to K8s TLS secrets for Gateway listener consumption. +# Only created for domains whose certificate_reference is an ACM ARN. +# Requires the ack_acm_controller to be deployed (optional input). +module "ack_acm_certificate" { + for_each = local.acm_cert_domains + + source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resource" + name = "${local.name}-acm-cert-${each.key}" + namespace = var.environment.namespace + advanced_config = {} + + data = { + apiVersion = "acm.services.k8s.aws/v1alpha1" + kind = "Certificate" + metadata = { + name = "${local.name}-acm-cert-${each.key}" + namespace = var.environment.namespace + } + spec = { + domainName = "*.${each.value.domain}" + subjectAlternativeNames = [ + each.value.domain, + "*.${each.value.domain}" + ] + validationMethod = "DNS" + options = { + certificateTransparencyLoggingPreference = "ENABLED" + } + exportTo = { + namespace = var.environment.namespace + name = local.acm_cert_secret_names[each.key] + key = "tls.crt" + } + } + } +} + +# Pre-create empty TLS secrets for ACK ACM certificate export +# ACK ACM controller requires the target secret to exist before it can export +resource "kubernetes_secret_v1" "acm_cert" { + for_each = local.acm_cert_domains + + metadata { + name = local.acm_cert_secret_names[each.key] + namespace = var.environment.namespace + } + + data = { + "tls.crt" = "" + "tls.key" = "" + } + + type = "kubernetes.io/tls" + + lifecycle { + ignore_changes = [data, metadata[0].annotations, metadata[0].labels] + } +} + +# --- DNS-01 wildcard certificate resources --- +# cert-manager Certificate CRDs for DNS-01 wildcard domains. +# Issues wildcard certs (*.domain + domain) via the dns01 ClusterIssuer (e.g. gts-production). +# Only created when use_dns01 is enabled, for domains without existing certificate_reference. +module "dns01_certificate" { + for_each = local.all_dns01_domains + + source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resource" + name = "${local.name}-dns01-cert-${each.key}" + namespace = var.environment.namespace + advanced_config = {} + + data = { + apiVersion = "cert-manager.io/v1" + kind = "Certificate" + metadata = { + name = "${local.name}-dns01-cert-${each.key}" + namespace = var.environment.namespace + } + spec = { + secretName = local.dns01_cert_secret_names[each.key] + issuerRef = { + name = local.dns01_cluster_issuer + kind = "ClusterIssuer" + } + dnsNames = [ + each.value.domain, + "*.${each.value.domain}" + ] + } + } +} + +# Bootstrap TLS secrets for DNS-01 domains — Gateway 443 listeners need a TLS secret +# to start. cert-manager will overwrite these once the DNS-01 challenge succeeds. +resource "tls_private_key" "dns01_bootstrap" { + for_each = local.all_dns01_domains + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_self_signed_cert" "dns01_bootstrap" { + for_each = local.all_dns01_domains + private_key_pem = tls_private_key.dns01_bootstrap[each.key].private_key_pem + + subject { + common_name = each.value.domain + } + + validity_period_hours = 8760 # 1 year + + dns_names = [ + each.value.domain, + "*.${each.value.domain}" + ] + + allowed_uses = [ + "key_encipherment", + "digital_signature", + "server_auth" + ] +} + +resource "kubernetes_secret_v1" "dns01_bootstrap_tls" { + for_each = local.all_dns01_domains + + metadata { + name = local.dns01_cert_secret_names[each.key] + namespace = var.environment.namespace + } + + data = { + "tls.crt" = tls_self_signed_cert.dns01_bootstrap[each.key].cert_pem + "tls.key" = tls_private_key.dns01_bootstrap[each.key].private_key_pem + } + + type = "kubernetes.io/tls" + + lifecycle { + ignore_changes = [data, metadata[0].annotations, metadata[0].labels] + } +} diff --git a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/outputs.tf b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/outputs.tf new file mode 100644 index 00000000..e61aea06 --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/outputs.tf @@ -0,0 +1,90 @@ +locals { + output_attributes = module.nginx_gateway_fabric.output_attributes + output_interfaces = module.nginx_gateway_fabric.output_interfaces +} + +output "domains" { + value = module.nginx_gateway_fabric.domains +} + +output "nginx_gateway_fabric" { + value = module.nginx_gateway_fabric.nginx_gateway_fabric +} + +output "domain" { + value = module.nginx_gateway_fabric.domain +} + +output "secure_endpoint" { + value = module.nginx_gateway_fabric.secure_endpoint +} + +output "gateway_class" { + value = module.nginx_gateway_fabric.gateway_class + description = "The GatewayClass name used by this gateway" +} + +output "gateway_name" { + value = module.nginx_gateway_fabric.gateway_name + description = "The Gateway resource name" +} + +output "subdomain" { + value = module.nginx_gateway_fabric.subdomain +} + +output "tls_secret" { + value = module.nginx_gateway_fabric.tls_secret + description = "Map of domain keys to their TLS certificate secret names" +} + +output "load_balancer_hostname" { + value = module.nginx_gateway_fabric.load_balancer_hostname + description = "Load balancer hostname (for CNAME records)" +} + +output "load_balancer_ip" { + value = module.nginx_gateway_fabric.load_balancer_ip + description = "Load balancer IP address (for A records)" +} + +output "lb_record_value" { + value = module.nginx_gateway_fabric.lb_record_value + description = "The value to use in DNS records (hostname or IP)" +} + +output "record_type" { + value = module.nginx_gateway_fabric.record_type + description = "DNS record type (CNAME or A)" +} + +output "name" { + value = module.nginx_gateway_fabric.name + description = "The computed resource name" +} + +output "base_domain" { + value = module.nginx_gateway_fabric.base_domain + description = "The computed base domain" +} + +output "base_subdomain" { + value = module.nginx_gateway_fabric.base_subdomain + description = "The wildcard base subdomain" +} + +output "username" { + value = module.nginx_gateway_fabric.username + description = "Basic auth username (empty if disabled)" +} + +output "password" { + value = module.nginx_gateway_fabric.password + sensitive = true + description = "Basic auth password (empty if disabled)" +} + +output "cloud_provider" { + value = module.nginx_gateway_fabric.cloud_provider + description = "Detected cloud provider (AWS, GCP, AZURE)" +} diff --git a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/variables.tf b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/variables.tf new file mode 100644 index 00000000..0d889c8c --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/variables.tf @@ -0,0 +1,37 @@ +variable "cluster" { + type = any + default = {} +} + +variable "baseinfra" { + type = any + default = {} +} + +variable "cc_metadata" { + type = any + default = { + tenant_base_domain = "tenant.facets.cloud" + } +} + +variable "instance" { + type = any +} + +variable "instance_name" { + type = string + default = "test_instance" +} + +variable "environment" { + type = any + default = { + namespace = "default" + } +} + +variable "inputs" { + type = any + default = {} +} From 8c9f78cfd65f6a68488146604474b77f546e4302 Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Mon, 16 Mar 2026 14:11:07 +0530 Subject: [PATCH 04/13] fix: add Route53 records for base domain when use_dns01 is active When use_dns01=true, we set disable_base_domain=true internally to take over base domain management, which causes the utility module to skip Route53 record creation. Create the records in the AWS wrapper instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../1.0/main.tf | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf index 26264a2d..8935042a 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf @@ -272,3 +272,39 @@ resource "kubernetes_secret_v1" "dns01_bootstrap_tls" { ignore_changes = [data, metadata[0].annotations, metadata[0].labels] } } + +# --- Route53 DNS records for DNS-01 base domain --- +# When use_dns01 is active, we set disable_base_domain=true in the modified instance +# which causes the utility module to skip Route53 record creation. +# We must create them ourselves to maintain DNS resolution. +resource "aws_route53_record" "dns01_base_domain" { + count = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? 1 : 0 + depends_on = [ + module.nginx_gateway_fabric + ] + zone_id = var.cc_metadata.tenant_base_domain_id + name = local.base_domain + type = module.nginx_gateway_fabric.record_type + ttl = "300" + records = [module.nginx_gateway_fabric.lb_record_value] + provider = aws3tooling + lifecycle { + prevent_destroy = true + } +} + +resource "aws_route53_record" "dns01_base_domain_wildcard" { + count = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? 1 : 0 + depends_on = [ + module.nginx_gateway_fabric + ] + zone_id = var.cc_metadata.tenant_base_domain_id + name = "*.${local.base_domain}" + type = module.nginx_gateway_fabric.record_type + ttl = "300" + records = [module.nginx_gateway_fabric.lb_record_value] + provider = aws3tooling + lifecycle { + prevent_destroy = true + } +} From b35bff28f03306d9a0ad82e3ece633fc71bf132e Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Mon, 16 Mar 2026 14:37:39 +0530 Subject: [PATCH 05/13] fix: rename Route53 resources to match utility module naming convention Co-Authored-By: Claude Opus 4.6 (1M context) --- modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf index 8935042a..4bda0449 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf @@ -277,7 +277,7 @@ resource "kubernetes_secret_v1" "dns01_bootstrap_tls" { # When use_dns01 is active, we set disable_base_domain=true in the modified instance # which causes the utility module to skip Route53 record creation. # We must create them ourselves to maintain DNS resolution. -resource "aws_route53_record" "dns01_base_domain" { +resource "aws_route53_record" "cluster-base-domain" { count = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? 1 : 0 depends_on = [ module.nginx_gateway_fabric @@ -293,7 +293,7 @@ resource "aws_route53_record" "dns01_base_domain" { } } -resource "aws_route53_record" "dns01_base_domain_wildcard" { +resource "aws_route53_record" "cluster-base-domain-wildcard" { count = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? 1 : 0 depends_on = [ module.nginx_gateway_fabric From 77555135663f08059117d655cf583dd6bb21df60 Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Mon, 16 Mar 2026 18:44:22 +0530 Subject: [PATCH 06/13] feat: add GCP and Azure legacy modules, pass default_tolerations, remove old legacy module - Add nginx_gateway_fabric_legacy_gcp and nginx_gateway_fabric_legacy_azure modules with DNS-01 wildcard cert support (no ACM) - Pass var.environment.default_tolerations to utility module via merged inputs in all three wrapper modules (AWS, GCP, Azure) - Update migration script and docs to target nginx_gateway_fabric_legacy_aws - Remove standalone nginx_gateway_fabric_legacy module (replaced by cloud-specific wrappers) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../1.0/MIGRATION.md | 277 ++++ .../nginx_gateway_fabric_legacy/1.0/README.md | 573 -------- .../1.0/charts/nginx-gateway-fabric-2.3.0.tgz | Bin 104025 -> 0 bytes .../1.0/charts/nginx-gateway-fabric-2.4.1.tgz | Bin 112504 -> 0 bytes .../1.0/convert_nginx_ingress.py | 345 +++++ .../nginx_gateway_fabric_legacy/1.0/main.tf | 1261 ----------------- .../1.0/outputs.tf | 161 --- .../1.0/main.tf | 19 +- .../1.0/README.md | 200 +++ .../1.0/facets.yaml | 922 ++++++++++++ .../1.0/main.tf | 220 +++ .../1.0/outputs.tf | 90 ++ .../1.0/variables.tf | 0 .../1.0/README.md | 200 +++ .../1.0/facets.yaml | 29 +- .../1.0/main.tf | 222 +++ .../1.0/outputs.tf | 90 ++ .../1.0/variables.tf | 37 + 18 files changed, 2643 insertions(+), 2003 deletions(-) create mode 100644 modules/ingress/nginx_gateway_fabric_legacy/1.0/MIGRATION.md delete mode 100644 modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md delete mode 100644 modules/ingress/nginx_gateway_fabric_legacy/1.0/charts/nginx-gateway-fabric-2.3.0.tgz delete mode 100644 modules/ingress/nginx_gateway_fabric_legacy/1.0/charts/nginx-gateway-fabric-2.4.1.tgz create mode 100644 modules/ingress/nginx_gateway_fabric_legacy/1.0/convert_nginx_ingress.py delete mode 100644 modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf delete mode 100644 modules/ingress/nginx_gateway_fabric_legacy/1.0/outputs.tf create mode 100644 modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/README.md create mode 100644 modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/facets.yaml create mode 100644 modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/main.tf create mode 100644 modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/outputs.tf rename modules/ingress/{nginx_gateway_fabric_legacy => nginx_gateway_fabric_legacy_azure}/1.0/variables.tf (100%) create mode 100644 modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/README.md rename modules/ingress/{nginx_gateway_fabric_legacy => nginx_gateway_fabric_legacy_gcp}/1.0/facets.yaml (97%) create mode 100644 modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/main.tf create mode 100644 modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/outputs.tf create mode 100644 modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/variables.tf diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/MIGRATION.md b/modules/ingress/nginx_gateway_fabric_legacy/1.0/MIGRATION.md new file mode 100644 index 00000000..6a3f90da --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/MIGRATION.md @@ -0,0 +1,277 @@ +# Migration Guide: `nginx_ingress_controller` to `nginx_gateway_fabric_legacy_aws` + +## Why migrate? + +The community-maintained **ingress-nginx** controller has reached **end-of-life**. Continued use means no security patches, no bug fixes, and increasing incompatibility with newer Kubernetes versions. The old ingress-nginx setup worked fine, but with the project at EOL we are moving to the **Kubernetes Gateway API**, which is the successor to the Ingress API -- it is now GA, natively supported by the Kubernetes project, and provides a standardised, role-oriented model for traffic routing. NGINX Gateway Fabric is the Gateway API implementation used by this module. + +Because NGINX Gateway Fabric's implementation of the Gateway API is still evolving, regex-based routing showed **unreliable and unpredictable behaviour** during our testing -- path ordering conflicts, capture-group interactions, and inconsistent matching made it difficult to reason about which backend ultimately received a request. For this reason, `nginx_gateway_fabric_legacy_aws` **enforces path-prefix-based routing only**. Regex paths (`~`, `~*`, `^` anchors) are not supported. Only `PathPrefix` and `Exact` path types are allowed. This keeps routing rules simple, auditable, and reproducible across environments. + +--- + +## What changes? + +| Area | Old (`nginx_ingress_controller`) | New (`nginx_gateway_fabric_legacy_aws`) | +|------|----------------------------------|-------------------------------------| +| **Flavor** | `nginx_ingress_controller` | `nginx_gateway_fabric_legacy_aws` | +| **Routing model** | Regex + prefix (annotation-driven) | **Path prefix only** (`PathPrefix` / `Exact`) | +| **Annotations on rules** | Supported (custom NGINX snippets, rewrites, etc.) | **Not supported** -- must be translated to native Gateway API features (header matching, URL rewrite, CORS, etc.) | +| **TLS certificates** | ACM ARNs or manual | ACM ARNs supported natively (via ACK), DNS-01 wildcard certs enabled by default (`use_dns01: true`) | +| **`basicAuth` / `basic_auth`** | Supported | Dropped (not natively supported in NGF) | +| **`equivalent_prefixes`** | Supported on domains | Dropped | +| **`grpc` (boolean)** | Global flag in `spec` | Per-rule `grpc_config` block | +| **`port_name`** | Accepted | Dropped -- use `port` (number) instead | +| **`advanced.nginx_ingress_controller.values`** | Helm overrides | Moved to `spec.helm_values` | +| **`advanced.nginx_ingress_controller.domain_prefix_override`** | Advanced block | Moved to `spec.domain_prefix_override` | +| **`advanced.nginx_ingress_controller.disable_endpoint_validation`** | Advanced block | Moved to `spec.disable_endpoint_validation` | +| **Domain-level `rules`** | Nested inside each domain object | Flattened into global `spec.rules` (keyed as `_`) | +| **`certificate_reference`** | Can be an AWS ACM ARN | ACM ARNs are supported natively; alternatively use a K8s TLS Secret name | +| **Namespace** | Optional | Required on every rule (derived automatically when using `${service..out.interfaces.main.name}` templates) | + +--- + +## Conversion script + +A Python helper (`convert_nginx_ingress.py`) is included in this module to automate the mechanical parts of the migration: + +```bash +python3 convert_nginx_ingress.py [-o output.json] [--default-namespace default] +``` + +### What the script does automatically + +- Updates `flavor` to `nginx_gateway_fabric_legacy_aws`, `kind`, and `version`. +- Enables `use_dns01: true` and `dns01_cluster_issuer: "gts-production"` for DNS-01 wildcard certificates. +- Moves `advanced.nginx_ingress_controller.*` fields into `spec`. +- Flattens domain-level rules into `spec.rules`. +- Adds `path_type: "PathPrefix"` to every rule. +- Strips `^` anchors from paths. +- Converts `grpc: true` to `grpc_config: { "enabled": true, "match_all_methods": true }`. +- Derives `namespace` from `${service..out.interfaces.main.name}` templates, or falls back to `--default-namespace`. +- Preserves ACM ARN `certificate_reference` values (the AWS module handles them natively via ACK). +- Adds `ack_acm_controller_details` input (required for ACM ARN domains). +- Drops unsupported fields (`basicAuth`, `equivalent_prefixes`, `annotations`, `disable_auth`, `port_name`, `allow_wildcard`, etc.) and prints warnings to stderr. + +### What requires manual effort from your side + +1. **Custom annotations** -- The old format allowed arbitrary NGINX annotations on each rule (snippets, rewrite targets, rate limits, proxy settings, etc.). These have **no automatic translation**. You must review each dropped annotation and re-implement the behaviour using native Gateway API features (header matching, URL rewriting, CORS, request/response header modifiers, timeouts, etc.). Refer to the [README](README.md) for available options. + +2. **Regex-based paths** -- Any rule whose `path` contained regex syntax (`~`, `~*`, capture groups, character classes) must be rewritten as one or more `PathPrefix` or `Exact` rules. There is no regex path support in this module. + +3. **Certificate references** -- With `use_dns01: true` (enabled by default), certificates are issued automatically as wildcard certs via the `gts-production` ClusterIssuer using DNS-01 validation. ACM ARN `certificate_reference` values are preserved and handled natively by the AWS module via the ACK ACM controller. Ensure the `ack_acm_controller` module is deployed if you have ACM ARN domains. + +4. **`port_name` without `port`** -- If any rule only specified `port_name` (no numeric `port`), you must look up the correct port number and add it. + +--- + +## Migration steps + +1. **Run the converter:** + ```bash + python3 convert_nginx_ingress.py my-instance.json -o my-instance-converted.json + ``` + +2. **Review warnings** printed to stderr. Each warning identifies a field that was dropped or needs attention. + +3. **Handle annotations manually.** For each rule that had custom annotations, decide how to express the same behaviour using the examples below. + +### Proxy timeouts + +Old (annotation): +```json +{ + "annotations": { + "nginx.ingress.kubernetes.io/proxy-read-timeout": "60", + "nginx.ingress.kubernetes.io/proxy-send-timeout": "30" + } +} +``` + +New (`timeouts` block on the rule): +```json +{ + "rules": { + "api": { + "service_name": "api-service", + "namespace": "default", + "port": 8080, + "path": "/api", + "path_type": "PathPrefix", + "timeouts": { + "request": "60s", + "backend_request": "30s" + } + } + } +} +``` + +### Rewrite target + +Old (annotation): +```json +{ + "annotations": { + "nginx.ingress.kubernetes.io/rewrite-target": "/v2/$1" + } +} +``` + +New (`url_rewrite` block on the rule): +```json +{ + "rules": { + "legacy_api": { + "service_name": "new-api-service", + "namespace": "default", + "port": 8080, + "path": "/old-api", + "path_type": "PathPrefix", + "url_rewrite": { + "rewrite_rule": { + "path_type": "ReplacePrefixMatch", + "replace_path": "/v2/api" + } + } + } + } +} +``` + +> **Note:** Regex capture groups (`$1`, `$2`) are not supported. You must express rewrites as prefix replacements (`ReplacePrefixMatch`) or full path replacements (`ReplaceFullPath`). + +### CORS headers + +Old (annotations): +```json +{ + "annotations": { + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": "https://example.com,https://app.example.com", + "nginx.ingress.kubernetes.io/cors-allow-methods": "GET,POST,PUT", + "nginx.ingress.kubernetes.io/cors-allow-headers": "Content-Type,Authorization", + "nginx.ingress.kubernetes.io/cors-allow-credentials": "true", + "nginx.ingress.kubernetes.io/cors-max-age": "86400" + } +} +``` + +New (`cors` block on the rule): +```json +{ + "rules": { + "api": { + "service_name": "api-service", + "namespace": "default", + "port": 8080, + "path": "/", + "path_type": "PathPrefix", + "cors": { + "enabled": true, + "allow_origins": { + "origin1": { "origin": "https://example.com" }, + "origin2": { "origin": "https://app.example.com" } + }, + "allow_methods": { + "get": { "method": "GET" }, + "post": { "method": "POST" }, + "put": { "method": "PUT" } + }, + "allow_headers": { + "content_type": { "header": "Content-Type" }, + "auth": { "header": "Authorization" } + }, + "allow_credentials": true, + "max_age": 86400 + } + } + } +} +``` + +### Custom request/response headers + +Old (annotations): +```json +{ + "annotations": { + "nginx.ingress.kubernetes.io/configuration-snippet": "proxy_set_header X-Custom-Header custom-value;", + "nginx.ingress.kubernetes.io/server-snippet": "add_header X-Response-ID unique-id always;" + } +} +``` + +New (`request_header_modifier` / `response_header_modifier` on the rule): +```json +{ + "rules": { + "api": { + "service_name": "api-service", + "namespace": "default", + "port": 8080, + "path": "/", + "path_type": "PathPrefix", + "request_header_modifier": { + "add": { + "custom_header": { + "name": "X-Custom-Header", + "value": "custom-value" + } + }, + "set": { + "source_header": { + "name": "X-Request-Source", + "value": "gateway" + } + }, + "remove": { + "sensitive_header": { + "name": "X-Sensitive-Header" + } + } + }, + "response_header_modifier": { + "add": { + "response_id": { + "name": "X-Response-ID", + "value": "unique-id" + } + }, + "remove": { + "server_header": { + "name": "Server" + } + } + } + } + } +} +``` + +### No native equivalent -- use `spec.helm_values` + +If an annotation has no direct Gateway API equivalent (e.g., custom NGINX snippets, rate limiting, IP whitelisting), you can apply global NGINX configuration via `spec.helm_values`: + +```json +{ + "spec": { + "helm_values": { + "nginx": { + "config": { + "entries": { + "proxy-buffer-size": "16k", + "client-max-body-size": "50m" + } + } + } + } + } +} +``` + +> **Note:** `helm_values` applies globally, not per-rule. Features like per-rule rate limiting and IP whitelisting are not natively supported in NGINX Gateway Fabric. + +4. **Replace regex paths.** Split complex regex rules into multiple prefix rules if needed. + +5. **Verify ACM setup.** If you have domains with ACM ARN `certificate_reference`, ensure the `ack_acm_controller` module is deployed and the `ack_acm_controller_details` input is configured. + +6. **Validate** the output JSON and deploy to a staging environment first. diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md b/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md deleted file mode 100644 index fb12f900..00000000 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md +++ /dev/null @@ -1,573 +0,0 @@ -# NGINX Gateway Fabric (Legacy) - -Kubernetes Gateway API implementation for advanced ingress traffic management. - -## Overview - -This module deploys **NGINX Gateway Fabric**, NGINX's implementation of the Kubernetes Gateway API specification. It provides a modern, declarative approach to configuring ingress traffic with native support for advanced routing features. - -This is the **legacy** flavor that uses `cc_metadata` and legacy input conventions. For the redesigned module, see `nginx_gateway_fabric`. - -### Features - -- **Gateway API Resources**: GatewayClass, Gateway, HTTPRoute, GRPCRoute -- **Advanced Routing**: Header matching, query parameter matching, HTTP method matching, RegularExpression path matching -- **URL Rewriting**: Path and hostname rewriting (ReplaceFullPath, ReplacePrefixMatch) -- **Traffic Management**: Canary deployments with weighted traffic splitting, request mirroring -- **Multi-Domain Support**: Routes work across all configured domains -- **TLS Management**: Automatic SSL certificates via cert-manager (HTTP-01 or DNS-01) or custom certificates -- **Multi-Cloud**: AWS (NLB), Azure (LB), GCP (GCLB) -- **gRPC Support**: Native GRPCRoute resources with method-level matching -- **CORS**: Per-route Cross-Origin Resource Sharing configuration -- **Basic Authentication**: Native NGF AuthenticationFilter with per-route opt-out -- **Observability**: Prometheus metrics via PodMonitor -- **Security Headers**: Automatic HSTS, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection -- **Cross-Namespace Backends**: ReferenceGrant support for services in other namespaces - ---- - -## Architecture - -``` - ┌─────────────────────────────────────────┐ - │ NGINX Gateway Fabric │ - │ │ - Internet ──────► │ Control Plane (Gateway Controller) │ - │ - Watches Gateway/Route resources │ - │ - Configures NGINX data plane │ - │ │ - │ Data Plane (NGINX Pods) │ - │ - Handles actual traffic │ - │ - Auto-scaled via HPA │ - │ - LoadBalancer Service │ - └─────────────────────────────────────────┘ -``` - -### cert-manager Validation Modes - -When in cert-manager mode, two validation strategies are available: - -| Validation | When Used | How It Works | -|------------|-----------|-------------| -| **HTTP-01** (default) | `disable_endpoint_validation: false` | Creates bootstrap self-signed certs, cert-manager validates via HTTP challenge | -| **DNS-01** | `disable_endpoint_validation: true` or `private: true` | Uses DNS challenges, required for private/internal LBs | - ---- - -## Configuration - -### Basic Example - -```json -{ - "kind": "ingress", - "flavor": "nginx_gateway_fabric_legacy", - "version": "1.0", - "spec": { - "private": false, - "force_ssl_redirection": true, - "rules": { - "api": { - "service_name": "api-service", - "namespace": "default", - "port": "8080", - "path": "/api", - "path_type": "PathPrefix" - } - } - } -} -``` - -### Required Fields (per rule) - -| Field | Description | -|-------|-------------| -| `service_name` | Kubernetes service name | -| `namespace` | Service namespace | -| `port` | Service port number | -| `path` | URL path (required for HTTP routes, not needed for gRPC) | - -### Path Type Options - -| Type | Default | Description | -|------|---------|-------------| -| `RegularExpression` | Yes | Matches paths using regex | -| `PathPrefix` | No | Matches paths starting with the specified prefix | -| `Exact` | No | Matches the exact path only | - ---- - -## Routing Options - -### Header-Based Routing - -```json -{ - "rules": { - "api_v2": { - "service_name": "api-v2", - "namespace": "default", - "port": "8080", - "path": "/", - "path_type": "PathPrefix", - "header_matches": { - "version_header": { - "name": "X-API-Version", - "value": "v2", - "type": "Exact" - } - } - } - } -} -``` - -### Query Parameter Matching - -```json -{ - "rules": { - "api_beta": { - "service_name": "api-beta", - "namespace": "default", - "port": "8080", - "path": "/api", - "query_param_matches": { - "version_param": { - "name": "version", - "value": "beta", - "type": "Exact" - } - } - } - } -} -``` - -### HTTP Method Matching - -```json -{ - "rules": { - "api_readonly": { - "service_name": "api-readonly", - "namespace": "default", - "port": "8080", - "path": "/api", - "method": "GET" - } - } -} -``` - -Options: `ALL` (default), `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` - ---- - -## URL Rewriting - -### Prefix Replacement - -```json -{ - "url_rewrite": { - "rewrite_rule": { - "hostname": "internal-api.svc.cluster.local", - "path_type": "ReplacePrefixMatch", - "replace_path": "/new-api" - } - } -} -``` - -### Full Path Replacement - -```json -{ - "url_rewrite": { - "rewrite_rule": { - "path_type": "ReplaceFullPath", - "replace_path": "/v2/api" - } - } -} -``` - ---- - -## Header Modification - -### Request Headers - -```json -{ - "request_header_modifier": { - "add": { - "custom_header": { "name": "X-Custom-Header", "value": "custom-value" } - }, - "set": { - "source_header": { "name": "X-Request-Source", "value": "gateway" } - }, - "remove": { - "sensitive_header": { "name": "X-Sensitive-Header" } - } - } -} -``` - -### Response Headers - -Security headers (HSTS, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection) are automatically added to all responses. - -```json -{ - "response_header_modifier": { - "add": { - "response_id": { "name": "X-Response-ID", "value": "unique-id" } - }, - "set": { - "cache_header": { "name": "Cache-Control", "value": "no-store" } - }, - "remove": { - "server_header": { "name": "Server" } - } - } -} -``` - ---- - -## Request Timeouts - -```json -{ - "timeouts": { - "request": "60s", - "backend_request": "30s" - } -} -``` - -Default: 300s for both request and backend_request. - ---- - -## CORS Configuration - -```json -{ - "cors": { - "enabled": true, - "allow_origins": { - "origin1": { "origin": "https://example.com" } - }, - "allow_methods": { - "get": { "method": "GET" }, - "post": { "method": "POST" } - }, - "allow_headers": { - "content_type": { "header": "Content-Type" }, - "auth": { "header": "Authorization" } - }, - "allow_credentials": true, - "max_age": 86400 - } -} -``` - ---- - -## gRPC Support - -### Route All gRPC Traffic - -```json -{ - "rules": { - "grpc_service": { - "service_name": "grpc-backend", - "namespace": "default", - "port": "50051", - "grpc_config": { - "enabled": true, - "match_all_methods": true - } - } - } -} -``` - -### Specific Method Matching - -```json -{ - "grpc_config": { - "enabled": true, - "match_all_methods": false, - "method_match": { - "get_user": { - "service": "myapp.v1.UserService", - "method": "GetUser", - "type": "Exact" - } - } - } -} -``` - ---- - -## Basic Authentication - -Enable basic auth globally with per-route opt-out: - -```json -{ - "spec": { - "basic_auth": true, - "rules": { - "protected_api": { - "service_name": "api", - "port": "8080", - "path": "/api", - "namespace": "default" - }, - "health_check": { - "service_name": "api", - "port": "8080", - "path": "/health", - "namespace": "default", - "disable_auth": true - } - } - } -} -``` - -Uses NGF's native `AuthenticationFilter` CRD. Credentials are auto-generated (`{instance_name}user` / random 10-char password). - ---- - -## Canary Deployments - -```json -{ - "canary_deployment": { - "enabled": true, - "canary_service": "api-v2", - "canary_weight": 20 - } -} -``` - -Sends 20% of traffic to `api-v2` and 80% to primary service. - ---- - -## Request Mirroring - -```json -{ - "request_mirror": { - "service_name": "api-shadow", - "port": "8080", - "namespace": "testing" - } -} -``` - ---- - -## Multi-Domain Configuration - -```json -{ - "spec": { - "disable_base_domain": true, - "domains": { - "production": { - "domain": "api.example.com", - "alias": "prod" - }, - "staging": { - "domain": "staging-api.example.com", - "alias": "staging", - "certificate_reference": "staging-tls" - } - } - } -} -``` - -All routes are accessible on all configured domains. - ---- - -## TLS Certificate Management - -### HTTP-01 Validation (Default -- cert-manager mode) - -- Creates bootstrap self-signed certificates for Gateway startup -- cert-manager replaces them with valid Let's Encrypt certificates via HTTP-01 challenge -- Requires port 80 accessible from internet - -### DNS-01 Validation (cert-manager mode) - -Used when `disable_endpoint_validation: true` or `private: true`: - -```json -{ - "spec": { - "private": true, - "disable_endpoint_validation": true, - "dns_issuer": "gts-production" - } -} -``` - -- Uses DNS challenges instead of HTTP -- Required for private/internal load balancers -- All managed domains share a single wildcard certificate -- Requires cert-manager DNS provider (e.g., Route53) configuration - -### Custom Certificates (cert-manager mode) - -Use an existing K8s TLS secret: - -```json -{ - "domains": { - "custom": { - "domain": "api.example.com", - "alias": "api", - "certificate_reference": "my-existing-tls-secret" - } - } -} -``` - -## Private Load Balancer - -```json -{ - "spec": { - "private": true, - "disable_endpoint_validation": true, - "force_ssl_redirection": true - } -} -``` - ---- - -## Custom Helm Values - -```json -{ - "spec": { - "helm_values": { - "nginx": { - "config": { - "logging": { - "errorLevel": "debug" - } - } - } - } - } -} -``` - ---- - -## Spec Options - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `private` | boolean | `false` | Use internal load balancer | -| `force_ssl_redirection` | boolean | `true` | Redirect HTTP to HTTPS | -| `disable_base_domain` | boolean | `false` | Disable auto-generated base domain | -| `disable_endpoint_validation` | boolean | `false` | Use DNS-01 instead of HTTP-01 validation | -| `domain_prefix_override` | string | - | Override auto-generated domain prefix | -| `dns_issuer` | string | `gts-production` | ClusterIssuer name for DNS-01 validation | -| `acme_email` | string | `systems@facets.cloud` | Email for Let's Encrypt ACME account | -| `cluster_issuer_override` | string | - | Override the auto-selected ClusterIssuer | -| `renew_cert_before` | string | `720h` | Renew certificate before expiry | -| `basic_auth` | boolean | `false` | Enable basic authentication | -| `body_size` | string | `150m` | Maximum client request body size | -| `helm_wait` | boolean | `true` | Wait for Helm release to be ready | -| `helm_values` | object | - | Additional Helm values | - ---- - -## Cloud Provider Support - -| Provider | Load Balancer | DNS | Annotations | -|----------|--------------|-----|-------------| -| AWS | Network Load Balancer (NLB) | Route53 | Proxy Protocol v2, NLB target type IP | -| Azure | Azure Load Balancer | - | Internal LB support | -| GCP | Google Cloud Load Balancer | - | Internal LB with global access | - ---- - -## Outputs - -| Output | Description | -|--------|-------------| -| `domains` | List of all configured domains | -| `domain` | Base domain (null if disabled) | -| `secure_endpoint` | HTTPS endpoint for base domain | -| `gateway_class` | GatewayClass name | -| `gateway_name` | Gateway resource name | -| `load_balancer_hostname` | LB hostname (for CNAME records) | -| `load_balancer_ip` | LB IP address (for A records) | -| `tls_secret` | TLS certificate secret name for base domain | -| `subdomain` | Subdomain mappings | - ---- - -## Troubleshooting - -### Check Gateway Status - -```bash -kubectl get gateway -n -kubectl describe gateway -n -``` - -### Check Routes - -```bash -kubectl get httproute -n -kubectl get grpcroute -n -kubectl describe httproute -n -``` - -### Controller Logs - -```bash -kubectl logs -n -l app.kubernetes.io/name=nginx-gateway-fabric -c nginx-gateway -``` - -### Data Plane Logs - -```bash -kubectl logs -n -l app.kubernetes.io/name=nginx-gateway-fabric -c nginx -``` - -### Certificate Issues - -```bash -kubectl get certificate -n -kubectl describe certificate -n -kubectl get clusterissuer -``` - ---- - -## Resources - -- [NGINX Gateway Fabric Documentation](https://docs.nginx.com/nginx-gateway-fabric/) -- [Kubernetes Gateway API Specification](https://gateway-api.sigs.k8s.io/) -- [NGINX Gateway Fabric GitHub](https://github.com/nginxinc/nginx-gateway-fabric) diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/charts/nginx-gateway-fabric-2.3.0.tgz b/modules/ingress/nginx_gateway_fabric_legacy/1.0/charts/nginx-gateway-fabric-2.3.0.tgz deleted file mode 100644 index dbdf7d2cf532d17a59c1ce4adab31c333b25ac91..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 104025 zcmV)YK&-zXiwG0|00000|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0POv3f7>>)Fb?l)eF_|<`)jhtl;k)MU0ru~FRz_8-c4Lxw%a}D zj&oXwge0sff+avZYLf5%|6%YzkOV18vYm8K^rDSP5Eu*wgPFlRBy*a)3+7mm8@vi; zc$(30c(lNo7_9Iz{=;qjMx)W_*^?*m|Iuhv{r~>x@$*0IKiz-4KiYr#{K@_wM*C0q zpFjBn8r{n1YkoNwIQzqB=dr4j`&HEuruNUrgM-1S7ZDz2G!^jZ z^!t<3|3cp@YC+!twe%KjNyca)M9Rm*;hc&^J{^Q?IRx4o`Y5bNL)o4ACc`%a^9>V{D*@fS{Y6 zd})#J^vje`v=b-KY#vQ^8Y7KpMBx~_wjpxC zi?gKHdw_xfT`UMXK0QZaoO3}k6fwzl5SyVD%QN&3%M(qDK~MkV7+Q|)^O%J=c6tSp zG-nJaq2y0U9txNyT>fK5=XnNf3(Z)D#DaJaCio^}F-l{c5EPL#W~-$ym^U@p!_E_updo`?N7FeJg8pmi# zP?!-cNQ5v)Db9rYfLg!@=*2b3R%LX+C>kZpoC~zT*92jdC-m2xppa!5;VDZZInUD2 z8Br(jz>i;8y)ByateA)G3KsGDm_TD&!WBod$>w$Up%hC)=MpL!RO#>7yAX zab){LvJ=@HI*}_-{Y=XcmPF18peM|^U`vFPgb7$Z-o9N@7D4wZiw=u##_0XW zYR@sAl9)@lrUcPugr4bYZWg-!oOLQ%tk7h0FlQY-|?k}>)p{n
w0$ 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 dvQYx*bxbOBTK;-Q4*;Ba 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 z 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 E1yWy&wg3PC diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/convert_nginx_ingress.py b/modules/ingress/nginx_gateway_fabric_legacy/1.0/convert_nginx_ingress.py new file mode 100644 index 00000000..fc7dd1ce --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/convert_nginx_ingress.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python3 +""" +Converts a var.instance JSON from the old nginx_k8s_native (ingress controller) +format to the new nginx_gateway_fabric_legacy_aws/1.0 format. + +Enables use_dns01 by default with gts-production ClusterIssuer for wildcard +certificate issuance via DNS-01 validation. ACM ARN certificate_references +are preserved (the AWS module handles them natively via ACK). + +Usage: + python3 convert_nginx_ingress.py [-o output.json] [--default-namespace default] +""" + +import argparse +import json +import re +import sys + + +CERT_GUIDE_URL = ( + "https://docs.facets.cloud/docs/nginx-gateway-fabric-legacy#custom-certificates" +) + +SERVICE_TEMPLATE_RE = re.compile( + r"^\$\{service\.(?P[^.]+)\.out\.interfaces\.[^.]+\.name\}$" +) + +SUPPORTED_DOMAIN_FIELDS = {"domain", "alias", "certificate_reference", "rules"} +SUPPORTED_SPEC_FIELDS = { + "private", + "force_ssl_redirection", + "domains", + "rules", + "subdomains", + "grpc", + "basicAuth", + "basic_auth", + "allow_wildcard", + "ingress_chart_version", + "annotations_risk_level", + "body_size", + "data_plane", + "control_plane", +} + +DEFAULT_INPUTS = { + "kubernetes_details": { + "resource_name": "default", + "resource_type": "kubernetes_cluster", + }, + "gateway_api_crd_details": { + "resource_name": "", + "resource_type": "gateway_api_crd", + }, + "prometheus_details": { + "resource_name": "prometheus", + "resource_type": "configuration", + }, + "ack_acm_controller_details": { + "resource_name": "", + "resource_type": "ack_acm_controller", + }, +} + +DEFAULT_SPEC_FIELDS = { + "body_size": "150m", + "data_plane": { + "scaling": { + "min_replicas": 2, + "max_replicas": 5, + "target_cpu_utilization_percentage": 70, + "target_memory_utilization_percentage": 80, + }, + "resources": { + "requests": {"cpu": "250m", "memory": "256Mi"}, + "limits": {"cpu": "1", "memory": "512Mi"}, + }, + }, + "control_plane": { + "scaling": { + "min_replicas": 2, + "max_replicas": 3, + "target_cpu_utilization_percentage": 70, + "target_memory_utilization_percentage": 80, + }, + "resources": { + "requests": {"cpu": "200m", "memory": "256Mi"}, + "limits": {"cpu": "500m", "memory": "512Mi"}, + }, + }, +} +DROP_SPEC_FIELDS_WARN = {"basicAuth", "basic_auth", "allow_wildcard"} +DROP_SPEC_FIELDS_SILENT = {"ingress_chart_version", "annotations_risk_level", "grpc"} + + +def warn(msg): + print(f" [!] {msg}", file=sys.stderr) + + +def section(title): + print(f"\n{'=' * 60}", file=sys.stderr) + print(f" {title}", file=sys.stderr) + print(f"{'=' * 60}", file=sys.stderr) + + +def derive_namespace(service_name, existing_namespace, default_namespace): + if existing_namespace: + return existing_namespace + m = SERVICE_TEMPLATE_RE.match(service_name or "") + if m: + name = m.group("name") + return f"${{service.{name}.out.attributes.namespace}}" + return default_namespace + + +def convert_rule(key, rule, global_grpc, default_namespace): + new_rule = {} + + new_rule["service_name"] = rule.get("service_name", "") + + if "port" in rule: + new_rule["port"] = rule["port"] + elif "port_name" in rule: + warn(f"Rule '{key}': DROPPED 'port_name' (no numeric 'port' found).") + warn(f" ACTION REQUIRED: Look up the correct port number and add it.") + + path = rule.get("path", "/") + if path.startswith("^"): + path = path[1:] + new_rule["path"] = path + new_rule["path_type"] = "PathPrefix" + + ns = derive_namespace( + rule.get("service_name"), rule.get("namespace"), default_namespace + ) + new_rule["namespace"] = ns + + if rule.get("domain_prefix"): + new_rule["domain_prefix"] = rule["domain_prefix"] + + rule_grpc = rule.get("grpc", global_grpc) + if rule_grpc: + new_rule["grpc_config"] = {"enabled": True, "match_all_methods": True} + + if "annotations" in rule: + annotations = rule["annotations"] + if isinstance(annotations, dict) and annotations: + warn(f"Rule '{key}': DROPPED annotations (not supported in gateway fabric):") + for akey, aval in annotations.items(): + warn(f" - {akey}: {aval}") + else: + warn(f"Rule '{key}': DROPPED annotations (not supported in gateway fabric).") + if "domains" in rule: + warn(f"Rule '{key}': DROPPED 'domains' from rule.") + warn(f" Domains must be defined at spec.domains level and apply to all rules.") + warn(f" ACTION REQUIRED: Add required domains under spec.domains.") + if "disable_auth" in rule: + pass # silently drop + if "port_name" in rule and "port" in rule: + pass # port_name is redundant when port exists + + return new_rule + + +def convert(input_data, default_namespace): + output = {} + + # Top-level fields + output["flavor"] = "nginx_gateway_fabric_legacy_aws" + if "kind" in input_data: + output["kind"] = "ingress" + if "version" in input_data: + output["version"] = "1.0" + if "metadata" in input_data: + metadata = dict(input_data["metadata"]) + annotations = metadata.get("annotations", {}) + if annotations: + nginx_annotations = {k: v for k, v in annotations.items() if "nginx" in k.lower()} + if nginx_annotations: + section("Metadata Annotations") + for akey in nginx_annotations: + warn(f"DROPPED: '{akey}' -- nginx annotations are not supported in gateway fabric.") + filtered = {k: v for k, v in annotations.items() if "nginx" not in k.lower()} + metadata["annotations"] = filtered + output["metadata"] = metadata + + # Set default inputs, adding only missing keys + inputs = dict(input_data.get("inputs", {})) + missing_inputs = [k for k in DEFAULT_INPUTS if k not in inputs] + if missing_inputs: + section("Inputs") + for key, default_value in DEFAULT_INPUTS.items(): + if key not in inputs: + inputs[key] = default_value + if key in ("gateway_api_crd_details", "ack_acm_controller_details"): + warn(f"ADDED: '{key}' with placeholder resource_name ''.") + warn(f" ACTION REQUIRED: Update resource_name with the actual resource name.") + else: + warn(f"ADDED: '{key}' with default value.") + output["inputs"] = inputs + + spec = input_data.get("spec", {}) + advanced = input_data.get("advanced", {}) + adv_nic = advanced.get("nginx_ingress_controller", {}) + + out_spec = {} + + # Carry over supported simple fields + for field in ("private", "force_ssl_redirection", "subdomains"): + if field in spec: + out_spec[field] = spec[field] + + # Enable DNS-01 wildcard certs by default for AWS + out_spec["use_dns01"] = True + out_spec["dns01_cluster_issuer"] = "gts-production" + + # Move advanced fields into spec + if "domain_prefix_override" in adv_nic: + out_spec["domain_prefix_override"] = adv_nic["domain_prefix_override"] + if "disable_endpoint_validation" in adv_nic: + out_spec["disable_endpoint_validation"] = adv_nic["disable_endpoint_validation"] + + # Warn about helm values, dropped fields, unknown fields + has_adv = bool(adv_nic) + dropped_fields = [f for f in DROP_SPEC_FIELDS_WARN if spec.get(f)] + unknown_fields = [f for f in spec if f not in SUPPORTED_SPEC_FIELDS] + if has_adv or dropped_fields or unknown_fields: + section("Spec & Advanced") + if has_adv: + warn("advanced.nginx_ingress_controller contains helm values.") + warn(" These are NOT automatically migrated -- the helm charts are different.") + warn(" ACTION REQUIRED: Review and configure equivalent settings under spec.helm_values.") + for field in dropped_fields: + warn(f"DROPPED: spec.{field} -- not supported in gateway fabric.") + for field in unknown_fields: + warn(f"DROPPED: spec.{field} -- unknown field.") + + global_grpc = spec.get("grpc", False) + + # Process domains + domains_in = spec.get("domains", {}) + out_domains = {} + extracted_rules = {} + + if domains_in: + section("Domains") + + for dkey, dval in domains_in.items(): + out_domain = {} + for field in ("domain", "alias", "certificate_reference"): + if field in dval: + out_domain[field] = dval[field] + + cert = dval.get("certificate_reference", "") + if cert and "arn:aws:acm" in cert: + warn(f"Domain '{dkey}': KEPT ACM ARN certificate_reference.") + warn(f" The AWS module handles ACM ARNs natively via ACK ACM controller.") + warn(f" Ensure ack_acm_controller is deployed (see inputs).") + + if "equivalent_prefixes" in dval: + warn(f"Domain '{dkey}': DROPPED 'equivalent_prefixes' (not supported).") + + # Extract domain-level rules + if "rules" in dval: + for rkey, rval in dval["rules"].items(): + composed_key = f"{dkey}_{rkey}" + extracted_rules[composed_key] = rval + + # Warn about unsupported domain fields + for field in dval: + if field not in SUPPORTED_DOMAIN_FIELDS: + warn(f"Domain '{dkey}': DROPPED field '{field}' (not supported).") + + out_domains[dkey] = out_domain + + if out_domains: + out_spec["domains"] = out_domains + + # Process rules: global + domain-extracted + all_rules_in = {} + all_rules_in.update(spec.get("rules", {})) + all_rules_in.update(extracted_rules) + + # Check if any rules have annotations or domains that will be dropped + rules_with_issues = any( + "annotations" in r or "domains" in r or ("port_name" in r and "port" not in r) + for r in all_rules_in.values() + ) + if rules_with_issues: + section("Rules") + + out_rules = {} + for rkey, rval in all_rules_in.items(): + out_rules[rkey] = convert_rule(rkey, rval, global_grpc, default_namespace) + + if out_rules: + out_spec["rules"] = out_rules + + # Set defaults for fields that are new in gateway fabric + for field, default_value in DEFAULT_SPEC_FIELDS.items(): + if field in spec: + out_spec[field] = spec[field] + else: + out_spec[field] = default_value + + output["spec"] = out_spec + + # Summary + section("Summary") + print(f" Converted {len(out_rules)} rules, {len(out_domains)} domains.", file=sys.stderr) + print(f" Review all [!] warnings above before deploying.\n", file=sys.stderr) + + return output + + +def main(): + parser = argparse.ArgumentParser( + description="Convert nginx_k8s_native instance JSON to nginx_gateway_fabric_legacy_aws/1.0" + ) + parser.add_argument("input", help="Path to input JSON file") + parser.add_argument("-o", "--output", help="Path to output JSON file (default: stdout)") + parser.add_argument( + "--default-namespace", + default="default", + help="Default namespace for rules without a derivable namespace (default: 'default')", + ) + args = parser.parse_args() + + with open(args.input, "r") as f: + input_data = json.load(f) + + result = convert(input_data, args.default_namespace) + + output_json = json.dumps(result, indent=2) + "\n" + + if args.output: + with open(args.output, "w") as f: + f.write(output_json) + print(f"Output written to {args.output}", file=sys.stderr) + else: + print(output_json) + + +if __name__ == "__main__": + main() diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf deleted file mode 100644 index 2fb8e38b..00000000 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf +++ /dev/null @@ -1,1261 +0,0 @@ -locals { - tenant_provider = lower(lookup(var.cc_metadata, "cc_tenant_provider", "aws")) - base_helm_values = lookup(var.instance.spec, "helm_values", {}) - - # Load balancer configuration - determine record type based on what's actually available - lb_hostname = try(data.kubernetes_service.gateway_lb.status[0].load_balancer[0].ingress[0].hostname, "") - lb_ip = try(data.kubernetes_service.gateway_lb.status[0].load_balancer[0].ingress[0].ip, "") - record_type = local.lb_hostname != "" ? "CNAME" : "A" - lb_record_value = local.lb_hostname != "" ? local.lb_hostname : local.lb_ip - - # Rules configuration - rulesRaw = lookup(var.instance.spec, "rules", {}) - - # Domain configuration (same as nginx_k8s) - instance_env_name = length(var.environment.unique_name) + length(var.instance_name) + length(var.cc_metadata.tenant_base_domain) >= 60 ? substr(md5("${var.instance_name}-${var.environment.unique_name}"), 0, 20) : "${var.instance_name}-${var.environment.unique_name}" - check_domain_prefix = coalesce(lookup(var.instance.spec, "domain_prefix_override", null), local.instance_env_name) - base_domain = lower("${local.check_domain_prefix}.${var.cc_metadata.tenant_base_domain}") - base_subdomain = "*.${local.base_domain}" - name = lower(var.environment.namespace == "default" ? "${var.instance_name}" : "${var.environment.namespace}-${var.instance_name}") - dns_validation_secret_name = lower("nginx-gateway-fabric-cert-${local.name}") - gateway_class_name = local.name - - # Conditionally append base domain - # Note: Not setting certificate_reference - the listener uses dns_validation_secret_name as fallback - # This ensures base domain is included in certmanager_managed_domains for DNS-01 - add_base_domain = lookup(var.instance.spec, "disable_base_domain", false) ? {} : { - "facets" = { - "domain" = "${local.base_domain}" - "alias" = "base" - } - } - - domains = merge(lookup(var.instance.spec, "domains", {}), local.add_base_domain) - - # List of all domain hostnames for HTTPRoutes - all_domain_hostnames = [for domain_key, domain in local.domains : domain.domain] - - # Filter rules - rulesFiltered = { - for k, v in local.rulesRaw : length(k) < 175 ? k : md5(k) => merge(v, { - host = lookup(v, "domain_prefix", null) == null || lookup(v, "domain_prefix", null) == "" ? "${local.base_domain}" : "${lookup(v, "domain_prefix", null)}.${local.base_domain}" - domain_key = "facets" - namespace = lookup(v, "namespace", var.environment.namespace) - }) - if( - (lookup(v, "port", null) != null && lookup(v, "port", null) != "") && - (lookup(v, "service_name", null) != null && lookup(v, "service_name", "") != "") && - ( - # gRPC routes don't need path/path_type - they use method matching - lookup(lookup(v, "grpc_config", {}), "enabled", false) || - # HTTP routes require path (path_type defaults to PathPrefix) - (lookup(v, "path", null) != null && lookup(v, "path", "") != "") - ) && - (lookup(v, "disable", false) == false) - ) - } - - # Generate all unique hostnames from rules (domain_prefix + domain combinations) - # This is needed to create listeners for each hostname - all_route_hostnames = distinct(flatten([ - for rule_key, rule in local.rulesFiltered : [ - for domain_key, domain in local.domains : - lookup(rule, "domain_prefix", null) == null || lookup(rule, "domain_prefix", null) == "" ? - domain.domain : - "${lookup(rule, "domain_prefix", null)}.${domain.domain}" - ] - ])) - - # Hostnames that need additional listeners (not already covered by base domain listeners) - additional_hostnames = [ - for hostname in local.all_route_hostnames : - hostname if !contains(local.all_domain_hostnames, hostname) - ] - - # Domains that have a certificate_reference (wildcard cert covers subdomains) - domains_with_cert_ref = { - for domain_key, domain in local.domains : - domain.domain => lookup(domain, "certificate_reference", "") - if lookup(domain, "certificate_reference", "") != "" - } - - # Map of additional hostnames to their config for listeners and certs - # Subdomains of domains with certificate_reference are excluded — the wildcard listener covers them - additional_hostname_configs = { - for hostname in local.additional_hostnames : - replace(replace(hostname, ".", "-"), "*", "wildcard") => { - hostname = hostname - secret_name = "${local.name}-${replace(replace(hostname, ".", "-"), "*", "wildcard")}-tls-cert" - } - # Exclude subdomains of domains with certificate_reference (covered by wildcard listener) - if !anytrue([for parent_domain, cert_ref in local.domains_with_cert_ref : endswith(hostname, ".${parent_domain}")]) - } - - # Tolerations: merge environment defaults with facets dedicated tolerations - ingress_tolerations = concat( - lookup(var.environment, "default_tolerations", []), - try(var.inputs.kubernetes_details.attributes.legacy_outputs.facets_dedicated_tolerations, []) - ) - - # Node selector from kubernetes_details legacy outputs - nodepool_labels = try(var.inputs.kubernetes_details.attributes.legacy_outputs.facets_dedicated_node_selectors, {}) - - disable_endpoint_validation = lookup(var.instance.spec, "disable_endpoint_validation", false) || lookup(var.instance.spec, "private", false) - - # Common labels for all resources - common_labels = { - "app.kubernetes.io/managed-by" = "facets" - "facets.cloud/module" = "nginx_gateway_fabric" - "facets.cloud/instance" = var.instance_name - } - - # Domains that need bootstrap TLS certificates for HTTP-01 validation - # Bootstrap cert is needed when HTTP-01 validation is used (disable_endpoint_validation = false) - # AND domain doesn't have certificate_reference (custom certs don't need bootstrap) - # Bootstrap cert is NOT needed for DNS-01 (uses dns_validation_secret_name) - bootstrap_tls_domains = { - for domain_key, domain in local.domains : - domain_key => domain - if !local.disable_endpoint_validation && can(domain.domain) && lookup(domain, "certificate_reference", "") == "" - } - - # Domains that need cert-manager to issue certificates - # Only domains WITHOUT certificate_reference - cert-manager should NOT manage domains with custom certs - # Applies to both HTTP-01 and DNS-01 validation - certmanager_managed_domains = { - for domain_key, domain in local.domains : - domain_key => domain - if can(domain.domain) && lookup(domain, "certificate_reference", "") == "" - } - - # Use gateway-shim only when ALL domains are managed by cert-manager - # When false (some domains have certificate_reference), we create explicit Certificate resources - use_gateway_shim = length(local.certmanager_managed_domains) == length(local.domains) - - # Cloud-specific service annotations - aws_annotations = merge( - lookup(var.instance.spec, "private", false) ? { - "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internal" - "service.beta.kubernetes.io/aws-load-balancer-internal" = "true" - } : { - "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internet-facing" - }, - { - "service.beta.kubernetes.io/aws-load-balancer-type" = "external" - "service.beta.kubernetes.io/aws-load-balancer-nlb-target-type" = "ip" - "service.beta.kubernetes.io/aws-load-balancer-backend-protocol" = "http" - "service.beta.kubernetes.io/aws-load-balancer-target-group-attributes" = lookup(var.instance.spec, "private", false) ? "proxy_protocol_v2.enabled=true,preserve_client_ip.enabled=false" : "proxy_protocol_v2.enabled=true,preserve_client_ip.enabled=true" - } - ) - - azure_annotations = { - "service.beta.kubernetes.io/azure-load-balancer-internal" = lookup(var.instance.spec, "private", false) ? "true" : "false" - } - - gcp_annotations = lookup(var.instance.spec, "private", false) ? { - "cloud.google.com/load-balancer-type" = "Internal" - "networking.gke.io/load-balancer-type" = "Internal" - "networking.gke.io/internal-load-balancer-allow-global-access" = "true" - } : {} - - ovh_annotations = { - "loadbalancer.ovhcloud.com/flavor" = "small" - } - - cloud_provider = upper(try(var.inputs.kubernetes_details.attributes.cloud_provider, "aws")) - - service_annotations = merge( - local.cloud_provider == "AWS" ? local.aws_annotations : {}, - local.cloud_provider == "AZURE" ? local.azure_annotations : {}, - local.cloud_provider == "GCP" ? local.gcp_annotations : {}, - local.cloud_provider == "OVH" ? local.ovh_annotations : {} - ) - - # Get ClusterIssuer names and config from cert-manager - cluster_issuer_dns = lookup(var.instance.spec, "dns_issuer", "gts-production") - cluster_issuer_gateway_http = "${local.name}-gateway-http01" - acme_email = lookup(var.instance.spec, "acme_email", "systems@facets.cloud") - - # Allow override of ClusterIssuer - useful for staging, custom issuers, or rate limit bypass - cluster_issuer_override = lookup(var.instance.spec, "cluster_issuer_override", null) - effective_cluster_issuer = coalesce( - local.cluster_issuer_override, - local.disable_endpoint_validation ? local.cluster_issuer_dns : local.cluster_issuer_gateway_http - ) - - # Security headers (always enabled with sensible defaults) - security_headers = { - "Strict-Transport-Security" = "max-age=31536000; includeSubDomains" - "X-Frame-Options" = "DENY" - "X-Content-Type-Options" = "nosniff" - "X-XSS-Protection" = "1; mode=block" - } - - # CORS headers per route - cors_headers = { - for k, v in local.rulesFiltered : k => merge( - lookup(lookup(v, "cors", {}), "enabled", false) ? { - "Access-Control-Allow-Origin" = join(", ", length(lookup(lookup(v, "cors", {}), "allow_origins", {})) > 0 ? - [for key, origin in lookup(lookup(v, "cors", {}), "allow_origins", {}) : origin.origin] : - ["*"] - ) - "Access-Control-Allow-Methods" = join(", ", length(lookup(lookup(v, "cors", {}), "allow_methods", {})) > 0 ? - [for key, m in lookup(lookup(v, "cors", {}), "allow_methods", {}) : m.method] : - ["GET", "POST", "PUT", "DELETE", "OPTIONS"] - ) - "Access-Control-Allow-Headers" = join(", ", length(lookup(lookup(v, "cors", {}), "allow_headers", {})) > 0 ? - [for key, h in lookup(lookup(v, "cors", {}), "allow_headers", {}) : h.header] : - ["Content-Type", "Authorization"] - ) - "Access-Control-Max-Age" = tostring(lookup(lookup(v, "cors", {}), "max_age", 86400)) - } : {}, - lookup(lookup(v, "cors", {}), "allow_credentials", false) ? { - "Access-Control-Allow-Credentials" = "true" - } : {} - ) - } - - # HTTP to HTTPS Redirect Route (only created when force_ssl_redirection is enabled) - # Single route that handles ALL HTTP (port 80) traffic and redirects to HTTPS - # MUST NOT have backendRefs - only RequestRedirect filter - http_redirect_resources = lookup(var.instance.spec, "force_ssl_redirection", false) ? { - "httproute-redirect-${local.name}" = { - apiVersion = "gateway.networking.k8s.io/v1" - kind = "HTTPRoute" - metadata = { - name = "${local.name}-http-redirect" - namespace = var.environment.namespace - } - spec = { - parentRefs = [{ - name = local.name - namespace = var.environment.namespace - sectionName = "http" # Reference HTTP listener (port 80) - }] - - rules = [{ - matches = [{ - path = { - type = "PathPrefix" - value = "/" - } - }] - filters = [{ - type = "RequestRedirect" - requestRedirect = { - scheme = "https" - statusCode = 301 - } - }] - # No backendRefs - redirect only - }] - } - } - } : {} - - # HTTPRoute Resources (HTTPS traffic - port 443, and HTTP - port 80 when force_ssl_redirection is disabled) - # Note: GatewayClass, Gateway, and NginxProxy are created by the Helm chart - force_ssl_redirection = lookup(var.instance.spec, "force_ssl_redirection", false) - - httproute_resources = { - for k, v in local.rulesFiltered : "httproute-${lower(var.instance_name)}-${k}" => { - apiVersion = "gateway.networking.k8s.io/v1" - kind = "HTTPRoute" - metadata = { - name = "${lower(var.instance_name)}-${k}" - namespace = var.environment.namespace - } - spec = { - # Reference the correct listener(s) for this route's hostnames - # Routes attach to HTTPS listeners per domain/hostname — TLS always terminates at the Gateway - # When force_ssl_redirection is disabled, also attach to HTTP listener so traffic is served on port 80 - parentRefs = concat( - lookup(v, "domain_prefix", null) == null || lookup(v, "domain_prefix", null) == "" ? [ - # No domain_prefix - use base domain listeners - for domain_key, domain in local.domains : { - name = local.name - namespace = var.environment.namespace - sectionName = "https-${domain_key}" - } - ] : [ - # Has domain_prefix: - # - If parent domain has certificate_reference → use wildcard domain listener (*.domain covers subdomains) - # - Otherwise → use additional hostname listener - for domain_key, domain in local.domains : { - name = local.name - namespace = var.environment.namespace - sectionName = lookup(domain, "certificate_reference", "") != "" ? "https-${domain_key}" : "https-${replace(replace("${lookup(v, "domain_prefix", null)}.${domain.domain}", ".", "-"), "*", "wildcard")}" - } - ], - # Also attach to HTTP listener when SSL redirection is disabled - !local.force_ssl_redirection ? [{ - name = local.name - namespace = var.environment.namespace - sectionName = "http" - }] : [] - ) - - # Include all domains in hostnames - Gateway API supports multiple hostnames per route - hostnames = distinct([ - for domain_key, domain in local.domains : - lookup(v, "domain_prefix", null) == null || lookup(v, "domain_prefix", null) == "" ? - domain.domain : - "${lookup(v, "domain_prefix", null)}.${domain.domain}" - ]) - - rules = [{ - matches = concat( - # Path matching (with optional method and query params) - [merge( - { - path = { - type = lookup(v, "path_type", "RegularExpression") - value = lookup(v, "path", "/") - } - }, - # Method matching (ALL or null means match all methods) - lookup(v, "method", null) != null && lookup(v, "method", "ALL") != "ALL" ? { - method = v.method - } : {}, - # Query parameter matching - length(lookup(v, "query_param_matches", {})) > 0 ? { - queryParams = [ - for key, qp in v.query_param_matches : { - name = qp.name - value = qp.value - type = lookup(qp, "type", "Exact") - } - ] - } : {}, - # Header matching - length(lookup(v, "header_matches", {})) > 0 ? { - headers = [ - for key, header in v.header_matches : { - name = header.name - value = header.value - type = lookup(header, "type", "Exact") - } - ] - } : {} - )] - ) - - filters = concat( - # Basic auth filter (applied when basic_auth is enabled and route doesn't have disable_auth) - lookup(var.instance.spec, "basic_auth", false) && !lookup(v, "disable_auth", false) ? [{ - type = "ExtensionRef" - extensionRef = { - group = "gateway.nginx.org" - kind = "AuthenticationFilter" - name = "${local.name}-basic-auth" - } - }] : [], - # Static filters - [ - for filter in [ - # Request header modification - lookup(v, "request_header_modifier", null) != null ? { - type = "RequestHeaderModifier" - requestHeaderModifier = merge( - lookup(v.request_header_modifier, "add", null) != null ? { - add = [for key, header in v.request_header_modifier.add : { name = header.name, value = header.value }] - } : {}, - lookup(v.request_header_modifier, "set", null) != null ? { - set = [for key, header in v.request_header_modifier.set : { name = header.name, value = header.value }] - } : {}, - lookup(v.request_header_modifier, "remove", null) != null ? { - remove = [for key, header in v.request_header_modifier.remove : header.name] - } : {} - ) - } : null, - - # Response header modification (security headers + CORS + custom headers) - { - type = "ResponseHeaderModifier" - responseHeaderModifier = merge( - { - add = [for name, value in merge( - local.security_headers, - { for key, header in lookup(lookup(v, "response_header_modifier", {}), "add", {}) : header.name => header.value }, - local.cors_headers[k] - ) : { name = name, value = value }] - }, - lookup(lookup(v, "response_header_modifier", {}), "set", null) != null ? { - set = [for key, header in v.response_header_modifier.set : { name = header.name, value = header.value }] - } : {}, - lookup(lookup(v, "response_header_modifier", {}), "remove", null) != null ? { - remove = [for key, header in v.response_header_modifier.remove : header.name] - } : {} - ) - }, - - # Request mirroring - lookup(v, "request_mirror", null) != null ? { - type = "RequestMirror" - requestMirror = { - backendRef = { - name = v.request_mirror.service_name - port = tonumber(v.request_mirror.port) - namespace = lookup(v.request_mirror, "namespace", v.namespace) - } - } - } : null - # Note: SSL redirection is handled by separate http_redirect_resources HTTPRoutes - # RequestRedirect filter cannot be used together with backendRefs in the same rule - ] : filter if filter != null - ], - # URL rewriting (from patternProperties) - [ - for key, rewrite in lookup(v, "url_rewrite", {}) : { - type = "URLRewrite" - urlRewrite = merge( - lookup(rewrite, "hostname", null) != null ? { - hostname = rewrite.hostname - } : {}, - lookup(rewrite, "path_type", null) != null && lookup(rewrite, "replace_path", null) != null ? { - path = merge( - { type = rewrite.path_type }, - rewrite.path_type == "ReplaceFullPath" ? { - replaceFullPath = rewrite.replace_path - } : {}, - rewrite.path_type == "ReplacePrefixMatch" ? { - replacePrefixMatch = rewrite.replace_path - } : {} - ) - } : {} - ) - } - ] - ) - - # Request/backend timeouts - default 300s (equivalent to proxy-read-timeout/proxy-send-timeout) - timeouts = { - request = lookup(lookup(v, "timeouts", {}), "request", "300s") - backendRequest = lookup(lookup(v, "timeouts", {}), "backend_request", "300s") - } - - backendRefs = concat( - # Primary backend - [{ - name = v.service_name - port = tonumber(v.port) - weight = lookup(lookup(v, "canary_deployment", {}), "enabled", false) ? 100 - lookup(lookup(v, "canary_deployment", {}), "canary_weight", 10) : 100 - namespace = v.namespace - }], - # Canary backend (if enabled) - lookup(lookup(v, "canary_deployment", {}), "enabled", false) ? [{ - name = lookup(lookup(v, "canary_deployment", {}), "canary_service", "") - port = tonumber(v.port) - weight = lookup(lookup(v, "canary_deployment", {}), "canary_weight", 10) - namespace = v.namespace - }] : [] - ) - }] - } - } if !lookup(lookup(v, "grpc_config", {}), "enabled", false) - } - - # GRPCRoute Resources - grpcroute_resources = { - for k, v in local.rulesFiltered : "grpcroute-${lower(var.instance_name)}-${k}" => { - apiVersion = "gateway.networking.k8s.io/v1" - kind = "GRPCRoute" - metadata = { - name = "${lower(var.instance_name)}-${k}-grpc" - namespace = var.environment.namespace - } - spec = { - # Reference the correct listener(s) for this route's hostnames - # Routes attach to HTTPS listeners per domain/hostname — TLS always terminates at the Gateway - # When force_ssl_redirection is disabled, also attach to HTTP listener - parentRefs = concat( - lookup(v, "domain_prefix", null) == null || lookup(v, "domain_prefix", null) == "" ? [ - # No domain_prefix - use base domain listeners - for domain_key, domain in local.domains : { - name = local.name - namespace = var.environment.namespace - sectionName = "https-${domain_key}" - } - ] : [ - # Has domain_prefix: - # - If parent domain has certificate_reference → use wildcard domain listener (*.domain covers subdomains) - # - Otherwise → use additional hostname listener - for domain_key, domain in local.domains : { - name = local.name - namespace = var.environment.namespace - sectionName = lookup(domain, "certificate_reference", "") != "" ? "https-${domain_key}" : "https-${replace(replace("${lookup(v, "domain_prefix", null)}.${domain.domain}", ".", "-"), "*", "wildcard")}" - } - ], - # Also attach to HTTP listener when SSL redirection is disabled - !local.force_ssl_redirection ? [{ - name = local.name - namespace = var.environment.namespace - sectionName = "http" - }] : [] - ) - - # Include all domains in hostnames - Gateway API supports multiple hostnames per route - hostnames = distinct([ - for domain_key, domain in local.domains : - lookup(v, "domain_prefix", null) == null || lookup(v, "domain_prefix", null) == "" ? - domain.domain : - "${lookup(v, "domain_prefix", null)}.${domain.domain}" - ]) - - rules = [{ - # If match_all_methods is true (default) or method_match is empty, match all gRPC traffic - matches = !lookup(lookup(v, "grpc_config", {}), "match_all_methods", true) && lookup(lookup(v, "grpc_config", {}), "method_match", null) != null ? [ - for key, method in lookup(v.grpc_config, "method_match", {}) : { - method = { - type = lookup(method, "type", "Exact") - service = lookup(method, "service", "") - method = lookup(method, "method", "") - } - } - ] : [] - - # Basic auth filter (applied when basic_auth is enabled and route doesn't have disable_auth) - filters = lookup(var.instance.spec, "basic_auth", false) && !lookup(v, "disable_auth", false) ? [{ - type = "ExtensionRef" - extensionRef = { - group = "gateway.nginx.org" - kind = "AuthenticationFilter" - name = "${local.name}-basic-auth" - } - }] : [] - - backendRefs = [{ - name = v.service_name - port = tonumber(v.port) - namespace = v.namespace - }] - }] - } - } if lookup(lookup(v, "grpc_config", {}), "enabled", false) - } - - # Helm release name - keep under 63 chars for k8s label limit - helm_release_name = substr(local.name, 0, min(length(local.name), 63)) - - # PodMonitor (only created when prometheus_details input is provided) - # Scrapes both control plane and data plane pods using common instance label - podmonitor_resources = lookup(var.inputs, "prometheus_details", null) != null ? { - "podmonitor-${local.name}" = { - apiVersion = "monitoring.coreos.com/v1" - kind = "PodMonitor" - metadata = { - name = "${local.name}-metrics" - namespace = var.environment.namespace - labels = { - # Label required by Prometheus Operator to discover this PodMonitor - release = try(var.inputs.prometheus_details.attributes.helm_release_id, "prometheus") - } - } - spec = { - selector = { - matchLabels = { - # Common label shared by both control plane and data plane pods - "app.kubernetes.io/instance" = local.helm_release_name - } - } - podMetricsEndpoints = [{ - port = "metrics" - interval = "30s" - path = "/metrics" - }] - } - } - } : {} - - # Collect unique namespaces that need ReferenceGrants (for cross-namespace backends) - cross_namespace_backends = { - for ns in distinct([for k, v in local.rulesFiltered : v.namespace if v.namespace != var.environment.namespace]) : ns => ns - } - - # ReferenceGrant resources for cross-namespace backends - # Allows HTTPRoutes in Gateway namespace to reference Services in other namespaces - referencegrant_resources = { - for ns in local.cross_namespace_backends : "referencegrant-${ns}" => { - apiVersion = "gateway.networking.k8s.io/v1beta1" - kind = "ReferenceGrant" - metadata = { - name = "${local.name}-allow-routes" - namespace = ns - } - spec = { - from = [ - { - group = "gateway.networking.k8s.io" - kind = "HTTPRoute" - namespace = var.environment.namespace - }, - { - group = "gateway.networking.k8s.io" - kind = "GRPCRoute" - namespace = var.environment.namespace - } - ] - to = [{ - group = "" - kind = "Service" - }] - } - } - } - - # ClientSettingsPolicy - applies body size limit to all traffic through the Gateway - # Equivalent to nginx.ingress.kubernetes.io/proxy-body-size - clientsettingspolicy_resources = { - "clientsettingspolicy-${local.name}" = { - apiVersion = "gateway.nginx.org/v1alpha1" - kind = "ClientSettingsPolicy" - metadata = { - name = "${local.name}-client-settings" - namespace = var.environment.namespace - } - spec = { - targetRef = { - group = "gateway.networking.k8s.io" - kind = "Gateway" - name = local.name - } - body = { - maxSize = lookup(var.instance.spec, "body_size", "150m") - } - } - } - } - - # AuthenticationFilter for basic auth (NGF native CRD) - authenticationfilter_resources = lookup(var.instance.spec, "basic_auth", false) ? { - "authfilter-${local.name}" = { - apiVersion = "gateway.nginx.org/v1alpha1" - kind = "AuthenticationFilter" - metadata = { - name = "${local.name}-basic-auth" - namespace = var.environment.namespace - } - spec = { - type = "Basic" - basic = { - realm = "Authentication required" - secretRef = { - name = "${local.name}-basic-auth" - } - } - } - } - } : {} - - # Merge all Gateway API resources - gateway_api_resources = merge( - local.http_redirect_resources, - local.httproute_resources, - local.grpcroute_resources, - local.podmonitor_resources, - local.referencegrant_resources, - local.clientsettingspolicy_resources, - local.authenticationfilter_resources - ) -} - -# Bootstrap TLS Private Key for HTTP-01 validation -# Creates a temporary self-signed cert so Gateway 443 listener can start -# cert-manager will overwrite this secret once HTTP-01 challenge succeeds -resource "tls_private_key" "bootstrap" { - for_each = local.bootstrap_tls_domains - algorithm = "RSA" - rsa_bits = 2048 -} - -resource "tls_self_signed_cert" "bootstrap" { - for_each = local.bootstrap_tls_domains - private_key_pem = tls_private_key.bootstrap[each.key].private_key_pem - - subject { - common_name = each.value.domain - } - - validity_period_hours = 8760 # 1 year - - dns_names = [ - each.value.domain, - "*.${each.value.domain}" - ] - - allowed_uses = [ - "key_encipherment", - "digital_signature", - "server_auth" - ] -} - -resource "kubernetes_secret_v1" "bootstrap_tls" { - for_each = local.bootstrap_tls_domains - - metadata { - name = "${local.name}-${each.key}-tls-cert" - namespace = var.environment.namespace - } - - data = { - "tls.crt" = tls_self_signed_cert.bootstrap[each.key].cert_pem - "tls.key" = tls_private_key.bootstrap[each.key].private_key_pem - } - - type = "kubernetes.io/tls" - - lifecycle { - ignore_changes = [data, metadata[0].annotations, metadata[0].labels] - } -} - -# Bootstrap TLS for additional hostnames (from domain_prefix in rules) -# Only for additional hostnames that did NOT inherit a certificate_reference from parent domain -resource "tls_private_key" "bootstrap_additional" { - for_each = local.additional_hostname_configs - algorithm = "RSA" - rsa_bits = 2048 -} - -resource "tls_self_signed_cert" "bootstrap_additional" { - for_each = local.additional_hostname_configs - private_key_pem = tls_private_key.bootstrap_additional[each.key].private_key_pem - - subject { - common_name = each.value.hostname - } - - validity_period_hours = 8760 # 1 year - - dns_names = [each.value.hostname] - - allowed_uses = [ - "key_encipherment", - "digital_signature", - "server_auth" - ] -} - -resource "kubernetes_secret_v1" "bootstrap_tls_additional" { - for_each = local.additional_hostname_configs - - metadata { - name = each.value.secret_name - namespace = var.environment.namespace - } - - data = { - "tls.crt" = tls_self_signed_cert.bootstrap_additional[each.key].cert_pem - "tls.key" = tls_private_key.bootstrap_additional[each.key].private_key_pem - } - - type = "kubernetes.io/tls" - - lifecycle { - ignore_changes = [data, metadata[0].annotations, metadata[0].labels] - } -} - -# Explicit Certificate resource for DNS-01 managed domains -# Created when NOT using gateway-shim (i.e., when some domains have certificate_reference) -# This ensures cert-manager only manages domains that need it, leaving custom certs alone -# For DNS-01, all managed domains share the same wildcard certificate (dns_validation_secret_name) -module "dns01_certificate" { - count = (!local.use_gateway_shim && local.disable_endpoint_validation && length(local.certmanager_managed_domains) > 0) ? 1 : 0 - - source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resource" - name = "${local.name}-dns01-certificate" - namespace = var.environment.namespace - advanced_config = {} - - data = { - apiVersion = "cert-manager.io/v1" - kind = "Certificate" - metadata = { - name = "${local.name}-dns01-cert" - namespace = var.environment.namespace - } - spec = { - secretName = local.dns_validation_secret_name - issuerRef = { - name = local.effective_cluster_issuer - kind = "ClusterIssuer" - } - # Include all managed domains (no wildcards - wildcards cause issues with cnameStrategy: Follow) - dnsNames = [ - for domain in values(local.certmanager_managed_domains) : domain.domain - ] - renewBefore = lookup(var.instance.spec, "renew_cert_before", "720h") - } - } - - depends_on = [helm_release.nginx_gateway_fabric] -} - -# Explicit Certificate resources for HTTP-01 managed domains -# Created when NOT using gateway-shim (i.e., when some domains have certificate_reference) -# For HTTP-01, each domain needs its own Certificate (unlike DNS-01 which can share) -module "http01_certificate" { - for_each = !local.use_gateway_shim && !local.disable_endpoint_validation ? local.certmanager_managed_domains : {} - - source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resource" - name = "${local.name}-http01-cert-${each.key}" - namespace = var.environment.namespace - advanced_config = {} - - data = { - apiVersion = "cert-manager.io/v1" - kind = "Certificate" - metadata = { - name = "${local.name}-http01-cert-${each.key}" - namespace = var.environment.namespace - } - spec = { - secretName = "${local.name}-${each.key}-tls-cert" - issuerRef = { - name = local.cluster_issuer_gateway_http - kind = "ClusterIssuer" - } - dnsNames = [ - each.value.domain - ] - renewBefore = lookup(var.instance.spec, "renew_cert_before", "720h") - } - } - - depends_on = [ - helm_release.nginx_gateway_fabric, - module.cluster-issuer-gateway-http01 - ] -} - -# Name module for additional hostname certificates (keeps helm release names under 53 chars) -# Only created when NOT using gateway-shim (same as http01_certificate for base domains) -module "http01_certificate_additional_name" { - for_each = !local.use_gateway_shim ? local.additional_hostname_configs : {} - - source = "github.com/Facets-cloud/facets-utility-modules//name" - environment = var.environment - limit = 53 - globally_unique = true - resource_name = "${local.name}-cert-${each.key}" - resource_type = "certificate" - is_k8s = true -} - -# Explicit Certificate resources for additional hostnames (domain_prefix + domain) -# Created when NOT using gateway-shim (gateway-shim handles certs automatically when enabled) -module "http01_certificate_additional" { - for_each = !local.use_gateway_shim ? local.additional_hostname_configs : {} - - source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resource" - name = module.http01_certificate_additional_name[each.key].name - namespace = var.environment.namespace - advanced_config = {} - - data = { - apiVersion = "cert-manager.io/v1" - kind = "Certificate" - metadata = { - name = module.http01_certificate_additional_name[each.key].name - namespace = var.environment.namespace - } - spec = { - secretName = each.value.secret_name - issuerRef = { - name = local.cluster_issuer_gateway_http - kind = "ClusterIssuer" - } - dnsNames = [ - each.value.hostname - ] - renewBefore = lookup(var.instance.spec, "renew_cert_before", "720h") - } - } - - depends_on = [ - helm_release.nginx_gateway_fabric, - module.cluster-issuer-gateway-http01 - ] -} - -# NGINX Gateway Fabric Helm Chart -# Note: Gateway API CRDs are installed by the gateway_api_crd module (dependency) -resource "helm_release" "nginx_gateway_fabric" { - name = "${local.name}-nginx-fabric" - wait = lookup(var.instance.spec, "helm_wait", true) - chart = "${path.module}/charts/nginx-gateway-fabric-2.4.1.tgz" - namespace = var.environment.namespace - max_history = 10 - skip_crds = false - create_namespace = false - timeout = 600 - - values = [ - yamlencode({ - # Use release-specific TLS secret names to support multiple instances in the same namespace - certGenerator = { - serverTLSSecretName = "${local.name}-server-tls" - agentTLSSecretName = "${local.name}-agent-tls" - overwrite = true - tolerations = local.ingress_tolerations - nodeSelector = local.nodepool_labels - } - - nginxGateway = { - # Configure the GatewayClass name - gatewayClassName = local.gateway_class_name - - # Labels for control plane deployment - labels = local.common_labels - - image = { - repository = "facetscloud/nginx-gateway-fabric" - tag = "v2.4.1" - pullPolicy = "IfNotPresent" - } - imagePullSecrets = lookup(var.inputs, "artifactories", null) != null ? var.inputs.artifactories.attributes.registry_secrets_list : [] - - # Control plane resources - resources = { - requests = { - cpu = lookup(lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "resources", {}), "requests", {}), "cpu", "200m") - memory = lookup(lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "resources", {}), "requests", {}), "memory", "256Mi") - } - limits = { - cpu = lookup(lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "resources", {}), "limits", {}), "cpu", "500m") - memory = lookup(lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "resources", {}), "limits", {}), "memory", "512Mi") - } - } - - # Control plane autoscaling - always enabled - autoscaling = { - enable = true - minReplicas = lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "scaling", {}), "min_replicas", 2) - maxReplicas = lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "scaling", {}), "max_replicas", 3) - targetCPUUtilizationPercentage = lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "scaling", {}), "target_cpu_utilization_percentage", 70) - targetMemoryUtilizationPercentage = lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "scaling", {}), "target_memory_utilization_percentage", 80) - } - - tolerations = local.ingress_tolerations - nodeSelector = local.nodepool_labels - - # Labels for control plane service - service = { - labels = local.common_labels - } - } - - # NGINX data plane configuration (NginxProxy) - # Note: The following fields are NOT supported in NginxProxy CRD (NGF 2.3.0): - # - clientMaxBodySize (use ClientSettingsPolicy body.maxSize instead) - # - proxyConnectTimeout, proxySendTimeout, proxyReadTimeout (not exposed in any CRD) - nginx = { - # Enable Proxy Protocol to get real client IP with externalTrafficPolicy: Cluster (AWS only) - # Access logs are always enabled with upstream service name for debugging - config = merge( - local.cloud_provider == "AWS" ? { - rewriteClientIP = { - mode = "ProxyProtocol" - trustedAddresses = [ - { - type = "CIDR" - value = "0.0.0.0/0" - } - ] - } - } : {}, - { - logging = { - errorLevel = "info" - agentLevel = "info" - accessLog = { - disable = false - format = "$remote_addr - $remote_user [$time_local] \"$request\" $status $body_bytes_sent \"$http_referer\" \"$http_user_agent\" $request_length $request_time [$proxy_host] $upstream_addr $upstream_response_length $upstream_response_time $upstream_status" - } - } - } - ) - - # Data plane autoscaling - always enabled - autoscaling = { - enable = true - minReplicas = lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "scaling", {}), "min_replicas", 2) - maxReplicas = lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "scaling", {}), "max_replicas", 5) - targetCPUUtilizationPercentage = lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "scaling", {}), "target_cpu_utilization_percentage", 70) - targetMemoryUtilizationPercentage = lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "scaling", {}), "target_memory_utilization_percentage", 80) - } - - # Data plane container resources - container = { - resources = { - requests = { - cpu = lookup(lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "resources", {}), "requests", {}), "cpu", "250m") - memory = lookup(lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "resources", {}), "requests", {}), "memory", "256Mi") - } - limits = { - cpu = lookup(lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "resources", {}), "limits", {}), "cpu", "1") - memory = lookup(lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "resources", {}), "limits", {}), "memory", "512Mi") - } - } - } - - # Data plane pod configuration - pod = { - tolerations = local.ingress_tolerations - nodeSelector = local.nodepool_labels - } - - # Labels for data plane deployment via patches - patches = [ - { - type = "StrategicMerge" - value = { - metadata = { - labels = local.common_labels - } - } - } - ] - - service = { - type = "LoadBalancer" - # loadBalancerClass = local.cloud_provider == "AWS" ? "service.k8s.aws/nlb" : "" - externalTrafficPolicy = "Cluster" - # Service patches for annotations and labels - patches = [ - { - type = "StrategicMerge" - value = { - metadata = { - labels = local.common_labels - annotations = local.service_annotations - } - } - } - ] - } - } - - # Gateway configuration - gateways = [{ - name = local.name - namespace = var.environment.namespace - labels = merge(local.common_labels, { - "gateway.networking.k8s.io/gateway-name" = local.name - }) - # Only add cert-manager annotations when using gateway-shim - # When not using gateway-shim (custom certs present), we create explicit Certificate resources - annotations = local.use_gateway_shim ? { - "cert-manager.io/cluster-issuer" = local.effective_cluster_issuer - "cert-manager.io/renew-before" = lookup(var.instance.spec, "renew_cert_before", "720h") - } : {} - spec = { - gatewayClassName = local.gateway_class_name - listeners = concat( - # HTTP Listener - [{ - name = "http" - protocol = "HTTP" - port = 80 - allowedRoutes = { - namespaces = { - from = "All" - } - } - }], - # HTTPS Listeners per domain — TLS always terminates at the Gateway - # When certificate_reference is provided, use wildcard hostname (*.domain) to cover all subdomains - # Otherwise use exact hostname and rely on additional hostname listeners for subdomains - [for domain_key, domain in local.domains : { - name = "https-${domain_key}" - protocol = "HTTPS" - port = 443 - hostname = lookup(domain, "certificate_reference", "") != "" ? "*.${domain.domain}" : domain.domain - tls = { - mode = "Terminate" - certificateRefs = [{ - kind = "Secret" - # If certificate_reference is provided, use it (custom cert or K8s secret name) - # Otherwise: DNS-01 uses shared dns_validation_secret_name, HTTP-01 uses per-domain bootstrap cert - name = lookup(domain, "certificate_reference", "") != "" ? domain.certificate_reference : ( - local.disable_endpoint_validation ? local.dns_validation_secret_name : "${local.name}-${domain_key}-tls-cert" - ) - }] - } - allowedRoutes = { - namespaces = { - from = "All" - } - } - } if can(domain.domain)], - # HTTPS Listeners for additional hostnames - [for hostname_key, config in local.additional_hostname_configs : { - name = "https-${hostname_key}" - protocol = "HTTPS" - port = 443 - hostname = config.hostname - tls = { - mode = "Terminate" - certificateRefs = [{ - kind = "Secret" - name = config.secret_name - }] - } - allowedRoutes = { - namespaces = { - from = "All" - } - } - }] - ) - } - }] - }), - yamlencode(local.base_helm_values) - ] - - depends_on = [ - kubernetes_secret_v1.bootstrap_tls, - kubernetes_secret_v1.bootstrap_tls_additional - ] -} - -# Gateway API HTTP-01 ClusterIssuer - bundled here as it requires parentRefs to the Gateway -# See: https://github.com/cert-manager/cert-manager/issues/7890 -module "cluster-issuer-gateway-http01" { - count = length(local.certmanager_managed_domains) > 0 ? 1 : 0 - depends_on = [helm_release.nginx_gateway_fabric] - source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resource" - name = local.cluster_issuer_gateway_http - namespace = var.environment.namespace - advanced_config = {} - - data = { - apiVersion = "cert-manager.io/v1" - kind = "ClusterIssuer" - metadata = { - name = local.cluster_issuer_gateway_http - } - spec = { - acme = { - email = local.acme_email - server = "https://acme-v02.api.letsencrypt.org/directory" - privateKeySecretRef = { - name = "${local.cluster_issuer_gateway_http}-account-key" - } - solvers = [ - { - http01 = { - gatewayHTTPRoute = { - parentRefs = [ - { - name = local.name - namespace = var.environment.namespace - kind = "Gateway" - sectionName = "http" # Must target HTTP listener for HTTP-01 challenges - } - ] - } - } - }, - ] - } - } - } -} - -# Deploy all Gateway API resources using facets-utility-modules -module "gateway_api_resources" { - source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resources" - - name = "${local.name}-gateway-api" - release_name = "${local.name}-gateway-api" - namespace = var.environment.namespace - resources_data = local.gateway_api_resources - advanced_config = {} - - depends_on = [helm_release.nginx_gateway_fabric, kubernetes_secret_v1.basic_auth] -} - -# Basic Authentication using NGF AuthenticationFilter CRD -# NGF 2.4.1 supports native basic auth via AuthenticationFilter (gateway.nginx.org/v1alpha1) -# When basic_auth is enabled: auto-generates credentials, creates htpasswd Secret, -# and applies AuthenticationFilter to all HTTPRoute rules (per-rule disable_auth to exempt) - -resource "random_string" "basic_auth_password" { - count = lookup(var.instance.spec, "basic_auth", false) ? 1 : 0 - length = 10 - special = false -} - -resource "kubernetes_secret_v1" "basic_auth" { - count = lookup(var.instance.spec, "basic_auth", false) ? 1 : 0 - - metadata { - name = "${local.name}-basic-auth" - namespace = var.environment.namespace - } - - data = { - auth = "${var.instance_name}user:${bcrypt(random_string.basic_auth_password[0].result)}" - } - - type = "nginx.org/htpasswd" - - lifecycle { - ignore_changes = [data] - create_before_destroy = true - } -} - -# Load Balancer Service Discovery -# Note: The LoadBalancer service is created by NGINX Gateway Fabric controller -# when it processes the Gateway resource from the Helm chart -data "kubernetes_service" "gateway_lb" { - depends_on = [ - helm_release.nginx_gateway_fabric - ] - metadata { - # Service is created by controller with pattern: - - # Since both release name and gateway name are local.name, it becomes: - - name = "${local.name}-${local.name}" - namespace = var.environment.namespace - } -} - -# Route53 DNS Records (AWS) -resource "aws_route53_record" "cluster-base-domain" { - count = local.tenant_provider == "aws" && !lookup(var.instance.spec, "disable_base_domain", false) ? 1 : 0 - depends_on = [ - helm_release.nginx_gateway_fabric, - data.kubernetes_service.gateway_lb - ] - zone_id = var.cc_metadata.tenant_base_domain_id - name = local.base_domain - type = local.record_type - ttl = "300" - records = [local.lb_record_value] - provider = "aws3tooling" - lifecycle { - prevent_destroy = true - } -} - -resource "aws_route53_record" "cluster-base-domain-wildcard" { - count = local.tenant_provider == "aws" && !lookup(var.instance.spec, "disable_base_domain", false) ? 1 : 0 - depends_on = [ - helm_release.nginx_gateway_fabric, - data.kubernetes_service.gateway_lb - ] - zone_id = var.cc_metadata.tenant_base_domain_id - name = local.base_subdomain - type = local.record_type - ttl = "300" - records = [local.lb_record_value] - provider = "aws3tooling" - lifecycle { - prevent_destroy = true - } -} diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/outputs.tf b/modules/ingress/nginx_gateway_fabric_legacy/1.0/outputs.tf deleted file mode 100644 index 072a801d..00000000 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/outputs.tf +++ /dev/null @@ -1,161 +0,0 @@ -locals { - username = lookup(var.instance.spec, "basic_auth", false) ? "${var.instance_name}user" : "" - password = lookup(var.instance.spec, "basic_auth", false) && length(random_string.basic_auth_password) > 0 ? random_string.basic_auth_password[0].result : "" - is_auth_enabled = length(local.username) > 0 && length(local.password) > 0 - - output_attributes = merge( - { - # Always include base_domain for backward compatibility - base_domain = local.base_domain - gateway_class = local.gateway_class_name - gateway_name = local.name - # Load balancer DNS information - loadbalancer_dns = try(data.kubernetes_service.gateway_lb.status.0.load_balancer.0.ingress.0.hostname, - data.kubernetes_service.gateway_lb.status.0.load_balancer.0.ingress.0.ip, - null) - loadbalancer_hostname = try(data.kubernetes_service.gateway_lb.status.0.load_balancer.0.ingress.0.hostname, null) - loadbalancer_ip = try(data.kubernetes_service.gateway_lb.status.0.load_balancer.0.ingress.0.ip, null) - }, - # Only include base_domain_enabled if base domain is not disabled - !lookup(var.instance.spec, "disable_base_domain", false) ? { - base_domain_enabled = true - } : { - base_domain_enabled = false - } - ) - - # Generate output interfaces for every rule × domain combination - # Each rule is expanded across all configured domains (not just the base domain) - output_interfaces = length(local.rulesFiltered) == 0 || length(local.domains) == 0 ? {} : merge([ - for route_key, route in local.rulesFiltered : { - for domain_key, domain in local.domains : - "${route_key}--${domain_key}" => { - connection_string = local.is_auth_enabled ? "https://${local.username}:${local.password}@${ - lookup(route, "domain_prefix", null) == null || lookup(route, "domain_prefix", null) == "" ? - domain.domain : - "${lookup(route, "domain_prefix", null)}.${domain.domain}" - }" : "https://${ - lookup(route, "domain_prefix", null) == null || lookup(route, "domain_prefix", null) == "" ? - domain.domain : - "${lookup(route, "domain_prefix", null)}.${domain.domain}" - }" - host = ( - lookup(route, "domain_prefix", null) == null || lookup(route, "domain_prefix", null) == "" ? - domain.domain : - "${lookup(route, "domain_prefix", null)}.${domain.domain}" - ) - port = 443 - username = local.username - password = local.password - secrets = local.is_auth_enabled ? ["connection_string", "password"] : [] - } - } - ]...) -} - -output "domains" { - value = concat( - # Only include base domain if not disabled - !lookup(var.instance.spec, "disable_base_domain", false) ? [local.base_domain] : [], - [for d in values(lookup(var.instance.spec, "domains", {})) : d.domain if can(d.domain)] - ) -} - -output "nginx_gateway_fabric" { - value = { - resource_type = "ingress" - resource_name = var.instance_name - } -} - -output "domain" { - value = !lookup(var.instance.spec, "disable_base_domain", false) ? local.base_domain : null -} - -output "secure_endpoint" { - value = !lookup(var.instance.spec, "disable_base_domain", false) ? "https://${local.base_domain}" : null -} - -output "gateway_class" { - value = local.gateway_class_name - description = "The GatewayClass name used by this gateway" -} - -output "gateway_name" { - value = local.name - description = "The Gateway resource name" -} - -output "subdomain" { - value = !lookup(var.instance.spec, "disable_base_domain", false) ? { - (var.instance_name) = merge( - { - for s in try(var.instance.spec.subdomains, []) : - "${s}.domain" => "${s}.${local.base_domain}" - }, - { - for s in try(var.instance.spec.subdomains, []) : - "${s}.secure_endpoint" => "https://${s}.${local.base_domain}" - } - ) - } : {} -} - -output "tls_secret" { - value = { - for domain_key, domain in local.domains : - domain_key => lookup(domain, "certificate_reference", "") != "" ? domain.certificate_reference : "${local.name}-${domain_key}-tls-cert" - } - description = "Map of domain keys to their TLS certificate secret names" -} - -output "load_balancer_hostname" { - value = local.lb_hostname - description = "Load balancer hostname (for CNAME records)" -} - -output "load_balancer_ip" { - value = local.lb_ip - description = "Load balancer IP address (for A records)" -} - -output "lb_record_value" { - value = local.lb_record_value - description = "The value to use in DNS records (hostname or IP)" -} - -output "record_type" { - value = local.record_type - description = "DNS record type (CNAME or A)" -} - -output "name" { - value = local.name - description = "The computed resource name" -} - -output "base_domain" { - value = local.base_domain - description = "The computed base domain" -} - -output "base_subdomain" { - value = local.base_subdomain - description = "The wildcard base subdomain" -} - -output "username" { - value = local.username - description = "Basic auth username (empty if disabled)" -} - -output "password" { - value = local.password - sensitive = true - description = "Basic auth password (empty if disabled)" -} - -output "cloud_provider" { - value = local.cloud_provider - description = "Detected cloud provider (AWS, GCP, AZURE, OVH)" -} diff --git a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf index 4bda0449..acd9ca8f 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf @@ -89,6 +89,23 @@ locals { }) + # Merge default_tolerations into inputs so the utility module picks them up via kubernetes_node_pool_details.attributes.taints + default_tolerations = lookup(var.environment, "default_tolerations", []) + existing_taints = try(var.inputs.kubernetes_node_pool_details.attributes.taints, []) + merged_inputs = merge(var.inputs, { + kubernetes_node_pool_details = merge( + lookup(var.inputs, "kubernetes_node_pool_details", {}), + { + attributes = merge( + try(var.inputs.kubernetes_node_pool_details.attributes, {}), + { + taints = concat(local.default_tolerations, local.existing_taints) + } + ) + } + ) + }) + # AWS NLB annotations aws_annotations = merge( lookup(var.instance.spec, "private", false) ? { @@ -113,7 +130,7 @@ module "nginx_gateway_fabric" { instance = local.modified_instance instance_name = var.instance_name environment = var.environment - inputs = var.inputs + inputs = local.merged_inputs service_annotations = local.aws_annotations diff --git a/modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/README.md b/modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/README.md new file mode 100644 index 00000000..b85e9a07 --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/README.md @@ -0,0 +1,200 @@ +# NGINX Gateway Fabric (Azure Legacy) + +Kubernetes Gateway API implementation for Azure with Azure Load Balancer and DNS-01 support. + +## Overview + +This module is an **Azure-specific wrapper** around the base `nginx_gateway_fabric` utility module. It adds: + +- **Azure Load Balancer**: Internal/external LB configuration via annotations +- **DNS-01 Wildcard Certificates**: Optional DNS-01 validation via a pre-existing ClusterIssuer (e.g., `gts-production`) for wildcard listeners + +This is the **legacy** flavor that uses `cc_metadata` and legacy input conventions. + +--- + +## Architecture + +``` + ┌──────────────────────────────────────────────┐ + │ Azure Wrapper (this module) │ + │ │ + │ 1. DNS-01 → certificate_reference rewrite │ + │ 2. Azure LB annotations │ + │ │ + │ ┌──────────────────────────────────┐ │ + │ │ Base Utility Module │ │ + Internet ────────► │ │ - Gateway + Listeners │ │ + (Azure LB) │ │ - HTTPRoute / GRPCRoute │ │ + │ │ - Helm chart deployment │ │ + │ │ - HTTP-01 certs (if needed) │ │ + │ └──────────────────────────────────┘ │ + └──────────────────────────────────────────────┘ +``` + +--- + +## TLS Certificate Flows + +Two certificate strategies per domain: + +| Domain has | Flow | Listener | Managed by | +|---|---|---|---| +| No cert ref + `use_dns01: true` | cert-manager DNS-01 via ClusterIssuer | Wildcard (`*.domain`) | Azure wrapper | +| No cert ref + `use_dns01: false` | cert-manager HTTP-01 (default) | Exact hostname | Utility module | +| K8s secret in `certificate_reference` | User-managed | Wildcard (`*.domain`) | User | + +### HTTP-01 (Default) + +No extra configuration needed. The utility module creates a bundled HTTP-01 ClusterIssuer and issues certificates automatically via Let's Encrypt. + +### DNS-01 Wildcard Certificates + +When `use_dns01: true`, the module: + +1. Creates cert-manager `Certificate` resources requesting wildcard certs (`*.domain` + `domain`) from the specified ClusterIssuer +2. Creates bootstrap self-signed TLS secrets so Gateway listeners can start immediately +3. Sets `certificate_reference` on all domains, causing the utility module to create wildcard listeners (`*.domain`) +4. Disables the utility module's HTTP-01 ClusterIssuer and bootstrap cert creation (no domains left without `certificate_reference`) +5. Creates Route53 records for the base domain (since the utility module skips them when `disable_base_domain` is set internally) + +**Prerequisite**: The ClusterIssuer (default: `gts-production`) must already exist in the cluster with a DNS-01 solver configured. + +```json +{ + "spec": { + "use_dns01": true, + "dns01_cluster_issuer": "gts-production", + "domains": { + "production": { + "domain": "api.example.com", + "alias": "prod" + } + } + } +} +``` + +This creates: +- Wildcard listener: `*.api.example.com` +- Certificate: `*.api.example.com` + `api.example.com` via `gts-production` +- Base domain also gets a wildcard listener and DNS-01 cert + +--- + +## Configuration + +### Basic Example + +```json +{ + "kind": "ingress", + "flavor": "nginx_gateway_fabric_legacy_azure", + "version": "1.0", + "spec": { + "private": false, + "force_ssl_redirection": true, + "rules": { + "api": { + "service_name": "api-service", + "namespace": "default", + "port": "8080", + "path": "/api", + "path_type": "PathPrefix" + } + } + } +} +``` + +### DNS-01 + Private LB Example + +```json +{ + "kind": "ingress", + "flavor": "nginx_gateway_fabric_legacy_azure", + "version": "1.0", + "spec": { + "private": true, + "force_ssl_redirection": true, + "use_dns01": true, + "dns01_cluster_issuer": "gts-production", + "domains": { + "internal": { + "domain": "internal.example.com", + "alias": "internal" + } + }, + "rules": { + "api": { + "service_name": "api-service", + "namespace": "default", + "port": "8080", + "path": "/api" + } + } + } +} +``` + +--- + +## Spec Options + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `private` | boolean | `false` | Use internal load balancer | +| `force_ssl_redirection` | boolean | `true` | Redirect HTTP to HTTPS | +| `disable_base_domain` | boolean | `false` | Disable auto-generated base domain | +| `domain_prefix_override` | string | - | Override auto-generated domain prefix | +| `use_dns01` | boolean | `false` | Enable DNS-01 wildcard certificates for all domains | +| `dns01_cluster_issuer` | string | `gts-production` | ClusterIssuer name for DNS-01 validation | +| `disable_endpoint_validation` | boolean | `false` | Disable HTTP endpoint validation | +| `basic_auth` | boolean | `false` | Enable basic authentication | +| `body_size` | string | `150m` | Maximum client request body size | +| `helm_wait` | boolean | `true` | Wait for Helm release to be ready | +| `helm_values` | object | - | Additional Helm values | + +--- + +## Inputs + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `kubernetes_details` | `@outputs/kubernetes` | Yes | Kubernetes cluster connection | +| `gateway_api_crd_details` | `@outputs/gateway_api_crd` | Yes | Gateway API CRD installation | +| `prometheus_details` | `@outputs/prometheus` | No | Prometheus for PodMonitor | + +--- + +## Azure-Specific Behavior + +### Load Balancer Configuration + +- **Public**: `azure-load-balancer-internal: false` (external Azure LB) +- **Private**: `azure-load-balancer-internal: true` (internal Azure LB) + +--- + +## Troubleshooting + +### Check DNS-01 Certificate Status + +```bash +kubectl get certificate -n +kubectl describe certificate -dns01-cert- -n +kubectl get clusterissuer gts-production -o yaml +``` + +### Check Gateway Listeners + +```bash +kubectl get gateway -n -o yaml | grep -A 20 listeners +``` + +### Load Balancer Issues + +```bash +kubectl get svc -n -l app.kubernetes.io/name=nginx-gateway-fabric +kubectl describe svc -n +``` diff --git a/modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/facets.yaml b/modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/facets.yaml new file mode 100644 index 00000000..32545cd8 --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/facets.yaml @@ -0,0 +1,922 @@ +intent: ingress +flavor: nginx_gateway_fabric_legacy_azure +version: "1.0" +description: NGINX Gateway Fabric for Azure - Gateway API with Azure Load Balancer and DNS-01 support (Legacy module format) +clouds: + - azure + - kubernetes +inputs: + kubernetes_details: + type: "@outputs/kubernetes" + optional: false + default: + resource_type: kubernetes_cluster + resource_name: default + providers: + - kubernetes + - kubernetes-alpha + - helm + gateway_api_crd_details: + type: "@outputs/gateway_api_crd" + optional: false + default: + resource_type: gateway_api_crd + resource_name: default + prometheus_details: + type: "@outputs/prometheus" + optional: true + default: + resource_type: configuration + resource_name: prometheus +outputs: + default: + type: "@outputs/ingress" +spec: + title: NGINX Gateway Fabric (Azure) + type: object + properties: + private: + type: boolean + title: Private Load Balancer + description: Set load balancer as private/internal + disable_base_domain: + type: boolean + title: Disable Base Domain + description: Disable automatic creation of base domain for this gateway + default: false + domains: + title: Domains + description: Map of domain key to rules + type: object + x-ui-overrides-only: true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + title: Domain Object + description: Name of the domain object + type: object + properties: + domain: + type: string + title: Domain + description: Host name of the domain + pattern: ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*\.[A-Za-z]{2,6}$ + x-ui-unique: true + x-ui-placeholder: "Domain to map ingress. Eg: example.com, sub.example.com, my-domain.co.uk" + x-ui-error-message: "Value doesn't match the format. Eg: example.com, my-domain.co.uk" + alias: + type: string + title: Alias + description: Alias for the domain + certificate_reference: + type: string + title: Certificate Reference + description: Name of an existing TLS secret to use instead of cert-manager managed certificates + x-ui-toggle: true + required: + - domain + - alias + data_plane: + type: object + title: Data Plane (NGINX) + description: Configuration for NGINX pods that handle actual traffic + properties: + scaling: + type: object + title: Autoscaling + description: Horizontal Pod Autoscaler configuration + properties: + min_replicas: + type: integer + title: Minimum Replicas + description: Minimum number of NGINX pods (HPA lower bound) + default: 2 + max_replicas: + type: integer + title: Maximum Replicas + description: Maximum number of NGINX pods (HPA upper bound) + default: 10 + target_cpu_utilization_percentage: + type: integer + title: Target CPU Utilization (%) + description: Target CPU utilization percentage for HPA scaling + default: 70 + target_memory_utilization_percentage: + type: integer + title: Target Memory Utilization (%) + description: Target memory utilization percentage for HPA scaling + default: 80 + resources: + type: object + title: Resources + description: CPU and memory resources per NGINX pod + properties: + requests: + type: object + title: Resource Requests + description: Minimum resource requirements + properties: + cpu: + type: string + title: CPU Request + description: Minimum CPU requirement (e.g., 250m, 500m, 1) + pattern: ^([0-9]*\.?[0-9]+m?|[0-9]+)$ + default: 250m + x-ui-placeholder: 250m + memory: + type: string + title: Memory Request + description: Minimum memory requirement (e.g., 256Mi, 512Mi, 1Gi) + pattern: ^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$ + default: 256Mi + x-ui-placeholder: 256Mi + limits: + type: object + title: Resource Limits + description: Maximum resource limits + properties: + cpu: + type: string + title: CPU Limit + description: Maximum CPU allowed (e.g., 1, 2, 500m) + pattern: ^([0-9]*\.?[0-9]+m?|[0-9]+)$ + default: "1" + x-ui-placeholder: "1" + memory: + type: string + title: Memory Limit + description: Maximum memory allowed (e.g., 512Mi, 1Gi, 2Gi) + pattern: ^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$ + default: 512Mi + x-ui-placeholder: 512Mi + control_plane: + type: object + title: Control Plane (Gateway Controller) + description: Configuration for the NGINX Gateway Fabric controller that manages configuration + x-ui-toggle: true + properties: + scaling: + type: object + title: Autoscaling + description: Horizontal Pod Autoscaler configuration + properties: + min_replicas: + type: integer + title: Minimum Replicas + description: Minimum number of controller pods (HPA lower bound) + default: 2 + max_replicas: + type: integer + title: Maximum Replicas + description: Maximum number of controller pods (HPA upper bound) + default: 3 + target_cpu_utilization_percentage: + type: integer + title: Target CPU Utilization (%) + description: Target CPU utilization percentage for HPA scaling + default: 70 + target_memory_utilization_percentage: + type: integer + title: Target Memory Utilization (%) + description: Target memory utilization percentage for HPA scaling + default: 80 + resources: + type: object + title: Resources + description: CPU and memory resources per controller pod + properties: + requests: + type: object + title: Resource Requests + description: Minimum resource requirements + properties: + cpu: + type: string + title: CPU Request + description: Minimum CPU requirement (e.g., 100m, 250m, 500m) + pattern: ^([0-9]*\.?[0-9]+m?|[0-9]+)$ + default: 200m + x-ui-placeholder: 200m + memory: + type: string + title: Memory Request + description: Minimum memory requirement (e.g., 256Mi, 512Mi) + pattern: ^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$ + default: 256Mi + x-ui-placeholder: 256Mi + limits: + type: object + title: Resource Limits + description: Maximum resource limits + properties: + cpu: + type: string + title: CPU Limit + description: Maximum CPU allowed (e.g., 500m, 1, 2) + pattern: ^([0-9]*\.?[0-9]+m?|[0-9]+)$ + default: 500m + x-ui-placeholder: 500m + memory: + type: string + title: Memory Limit + description: Maximum memory allowed (e.g., 512Mi, 1Gi) + pattern: ^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$ + default: 512Mi + x-ui-placeholder: 512Mi + rules: + title: Routing Rules + description: Gateway API routing configurations (HTTP and gRPC) + type: object + patternProperties: + ^[a-zA-Z0-9_.-]*$: + title: Route Object + description: HTTP route configuration + type: object + properties: + disable: + type: boolean + title: Disable Route + description: Enable/Disable this route + domain_prefix: + type: string + title: Domain Prefix + description: Subdomain prefix + pattern: ^[a-z0-9]([a-z0-9-]{0,34}[a-z0-9])?$|^[a-z0-9]$ + x-ui-placeholder: Enter the subdomain prefix + x-ui-error-message: Max 36 characters, only hyphens allowed, cannot start/end with hyphen + service_name: + type: string + title: Service Name + description: Kubernetes service name + x-ui-api-source: + endpoint: /cc-ui/v1/dropdown/stack/{{stackName}}/resources-info + method: GET + params: + includeContent: false + labelKey: resourceName + valueKey: resourceName + valueTemplate: ${service.{{value}}.out.attributes.service_name} + filterConditions: + - field: resourceType + value: service + x-ui-typeable: true + namespace: + type: string + title: Service Namespace + description: Kubernetes service namespace + x-ui-api-source: + endpoint: /cc-ui/v1/dropdown/stack/{{stackName}}/resources-info + method: GET + params: + includeContent: false + labelKey: resourceName + valueKey: resourceName + valueTemplate: ${service.{{value}}.out.attributes.namespace} + filterConditions: + - field: resourceType + value: service + x-ui-typeable: true + port: + type: string + title: Port + description: Service port number + x-ui-api-source: + endpoint: /cc-ui/v1/dropdown/stack/{{stackName}}/service/{{serviceName}}/overview + dynamicProperties: + serviceName: + key: service_name + lookup: regex + x-ui-lookup-regex: \${[^.]+\.([^.]+).* + method: GET + labelKey: port + valueKey: port + path: ports + x-ui-typeable: true + path: + type: string + title: Path + description: Path of the application (required for HTTP routes) + pattern: ^(/[^\s{};^]*)$ + x-ui-placeholder: "Enter path (e.g., / or /api or /api/v[0-9]+)" + x-ui-error-message: "Path must start with /" + x-ui-visible-if: + field: spec.rules.{{this}}.grpc_config.enabled + values: + - false + - null + disable_auth: + type: boolean + title: Disable Auth for this Route + description: Disable basic authentication for this specific route (only relevant when basic_auth is enabled) + default: false + x-ui-visible-if: + field: spec.basic_auth + values: + - true + path_type: + type: string + title: Path Type + description: "Path matching type. RegularExpression (default) matches paths using regex for ingress-nginx parity. Use PathPrefix for prefix matching or Exact for exact path matching." + enum: + - RegularExpression + - PathPrefix + - Exact + default: RegularExpression + x-ui-visible-if: + field: spec.rules.{{this}}.grpc_config.enabled + values: + - false + - null + header_matches: + type: object + title: Header-Based Routing + description: Route traffic based on HTTP headers + x-ui-toggle: true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header Match + properties: + name: + type: string + title: Header Name + description: HTTP header name to match + x-ui-placeholder: X-API-Version + value: + type: string + title: Header Value + description: Header value to match + x-ui-placeholder: v2 + type: + type: string + title: Match Type + description: How to match the header value (Exact or RegularExpression) + enum: + - Exact + - RegularExpression + default: Exact + required: + - name + - value + query_param_matches: + type: object + title: Query Parameter Matching + description: Route traffic based on query parameters + x-ui-toggle: true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Query Param Match + properties: + name: + type: string + title: Parameter Name + description: Query parameter name to match + x-ui-placeholder: version + value: + type: string + title: Parameter Value + description: Query parameter value to match + x-ui-placeholder: beta + type: + type: string + title: Match Type + description: How to match the parameter value (Exact or RegularExpression) + enum: + - Exact + - RegularExpression + default: Exact + required: + - name + - value + method: + type: string + title: HTTP Method + description: Match requests by HTTP method + enum: + - ALL + - GET + - POST + - PUT + - DELETE + - PATCH + - HEAD + - OPTIONS + default: ALL + x-ui-toggle: true + request_header_modifier: + type: object + title: Request Header Modification + description: Modify request headers + x-ui-toggle: true + properties: + add: + type: object + title: Add Headers + description: Headers to add to requests + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to add + x-ui-placeholder: X-Custom-Header + value: + type: string + title: Header Value + description: Value for the header + x-ui-placeholder: custom-value + required: + - name + - value + set: + type: object + title: Set Headers + description: Headers to set (override) in requests + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to set + x-ui-placeholder: X-Request-Source + value: + type: string + title: Header Value + description: Value for the header + x-ui-placeholder: gateway + required: + - name + - value + remove: + type: object + title: Remove Headers + description: Header names to remove from requests + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to remove + x-ui-placeholder: X-Sensitive-Header + required: + - name + response_header_modifier: + type: object + title: Response Header Modification + description: Modify response headers + x-ui-toggle: true + properties: + add: + type: object + title: Add Headers + description: Headers to add to responses + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to add + x-ui-placeholder: X-Response-ID + value: + type: string + title: Header Value + description: Value for the header + x-ui-placeholder: unique-id + required: + - name + - value + set: + type: object + title: Set Headers + description: Headers to set (override) in responses + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to set + x-ui-placeholder: Cache-Control + value: + type: string + title: Header Value + description: Value for the header + x-ui-placeholder: no-store + required: + - name + - value + remove: + type: object + title: Remove Headers + description: Header names to remove from responses + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to remove + x-ui-placeholder: Server + required: + - name + url_rewrite: + type: object + title: URL Rewriting + description: Rewrite request URLs + x-ui-toggle: true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: URL Rewrite Rule + properties: + hostname: + type: string + title: Hostname + description: New hostname for the request + x-ui-placeholder: internal-api.svc.cluster.local + path_type: + type: string + title: Path Rewrite Type + description: How to rewrite the path (ReplaceFullPath or ReplacePrefixMatch) + enum: + - ReplaceFullPath + - ReplacePrefixMatch + replace_path: + type: string + title: Replace Path Value + description: New path value (full path or prefix depending on type) + x-ui-placeholder: /v2/api + canary_deployment: + type: object + title: Canary Deployment + description: Configure traffic splitting for canary deployments + x-ui-toggle: true + x-ui-skip: true + properties: + enabled: + type: boolean + title: Enable Canary + description: Enable canary deployment with traffic splitting + default: false + canary_service: + type: string + title: Canary Service Name + description: Name of the canary version service + x-ui-visible-if: + field: spec.rules.{{this}}.canary_deployment.enabled + values: + - true + canary_weight: + type: integer + title: Canary Traffic Percentage + description: Percentage of traffic to send to canary (0-100) + minimum: 0 + maximum: 100 + default: 10 + x-ui-visible-if: + field: spec.rules.{{this}}.canary_deployment.enabled + values: + - true + request_mirror: + type: object + title: Request Mirroring + description: Mirror traffic to a secondary backend for testing + x-ui-toggle: true + x-ui-skip: true + properties: + service_name: + type: string + title: Mirror Service Name + description: Name of the service to mirror traffic to + port: + type: string + title: Mirror Service Port + description: Port of the mirror service + namespace: + type: string + title: Mirror Service Namespace + description: Namespace of the mirror service + timeouts: + type: object + title: Request Timeouts + description: Configure request and backend timeouts + x-ui-toggle: true + properties: + request: + type: string + title: Request Timeout + description: Total request timeout (e.g., 30s, 1m, 5m) + pattern: ^\d+[smh]$ + default: 300s + x-ui-placeholder: "300s" + backend_request: + type: string + title: Backend Request Timeout + description: Backend response timeout (e.g., 30s, 1m, 5m) + default: 300s + pattern: ^\d+[smh]$ + x-ui-placeholder: "300s" + cors: + type: object + title: CORS Configuration + description: Configure Cross-Origin Resource Sharing + x-ui-toggle: true + properties: + enabled: + type: boolean + title: Enable CORS + description: Enable Cross-Origin Resource Sharing headers + default: false + allow_origins: + type: object + title: Allowed Origins + description: List of allowed origins (* for all) + x-ui-visible-if: + field: spec.rules.{{this}}.cors.enabled + values: + - true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Origin + properties: + origin: + type: string + title: Origin URL + description: Allowed origin URL + x-ui-placeholder: https://example.com + required: + - origin + allow_methods: + type: object + title: Allowed Methods + description: HTTP methods allowed for CORS requests + x-ui-visible-if: + field: spec.rules.{{this}}.cors.enabled + values: + - true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Method + properties: + method: + type: string + title: HTTP Method + description: HTTP method to allow + enum: + - GET + - POST + - PUT + - DELETE + - PATCH + - OPTIONS + - HEAD + required: + - method + allow_headers: + type: object + title: Allowed Headers + description: HTTP headers allowed in CORS requests + x-ui-visible-if: + field: spec.rules.{{this}}.cors.enabled + values: + - true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + header: + type: string + title: Header Name + description: Header name to allow + x-ui-placeholder: Content-Type + required: + - header + allow_credentials: + type: boolean + title: Allow Credentials + description: Allow cookies and authentication headers in CORS requests + default: false + x-ui-visible-if: + field: spec.rules.{{this}}.cors.enabled + values: + - true + max_age: + type: integer + title: Preflight Cache Max Age + description: Seconds to cache preflight response + default: 86400 + x-ui-visible-if: + field: spec.rules.{{this}}.cors.enabled + values: + - true + grpc_config: + type: object + title: gRPC Configuration + description: Configure gRPC routing + x-ui-toggle: true + properties: + enabled: + type: boolean + title: Enable gRPC + description: Enable gRPC routing for this service + default: false + match_all_methods: + type: boolean + title: Match All Methods + description: Route all gRPC traffic (no method filtering) + default: true + x-ui-visible-if: + field: spec.rules.{{this}}.grpc_config.enabled + values: + - true + method_match: + type: object + title: gRPC Method Matching + description: Whitelist specific gRPC services/methods + x-ui-visible-if: + field: spec.rules.{{this}}.grpc_config.match_all_methods + values: + - false + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Method Match + properties: + service: + type: string + title: Service Name + description: Protobuf service name (e.g., helloworld.Greeter) + method: + type: string + title: Method Name + description: gRPC method name (e.g., SayHello) + type: + type: string + title: Match Type + description: NGINX Gateway Fabric only supports Exact matching + enum: + - Exact + default: Exact + required: + - service + - method + required: + - service_name + - port + - namespace + x-ui-order: + - disable + - disable_auth + - domain_prefix + - service_name + - namespace + - port + - path + - path_type + - method + - timeouts + - cors + - header_matches + - query_param_matches + - url_rewrite + - request_header_modifier + - response_header_modifier + - grpc_config + # Disabling below things as too advanced for initial release + # - canary_deployment + # - request_mirror + force_ssl_redirection: + type: boolean + title: Force SSL Redirection + description: Force HTTP to HTTPS redirection + basic_auth: + type: boolean + title: Basic Authentication + description: Enable/disable basic auth for all routes (individual routes can opt out with disable_auth) + default: false + body_size: + type: string + title: Max Body Size + description: Maximum allowed size of the client request body (e.g., 150m, 1g) + pattern: ^\d{1,4}(k|m|g)?$ + default: 150m + x-ui-placeholder: 150m + x-ui-toggle: true + helm_values: + type: object + title: Additional Helm Values + description: "Additional Helm values to merge with the default configuration. See available values at: https://github.com/nginxinc/nginx-gateway-fabric/blob/main/charts/nginx-gateway-fabric/values.yaml" + x-ui-toggle: true + x-ui-yaml-editor: true + domain_prefix_override: + type: string + title: Domain Prefix Override + description: Override the automatically generated domain prefix + x-ui-toggle: true + disable_endpoint_validation: + type: boolean + title: Disable Endpoint Validation + description: Disable endpoint validation for cert-manager (uses DNS validation instead of HTTP) + default: false + x-ui-toggle: true + use_dns01: + type: boolean + title: Use DNS-01 Resolver + description: Use DNS-01 challenge type for certificate issuance. When enabled, all domain listeners use wildcard hostnames (*.domain) and certificates are issued via the DNS-01 ClusterIssuer. + default: false + x-ui-toggle: true + dns01_cluster_issuer: + type: string + title: DNS-01 ClusterIssuer + description: Name of the ClusterIssuer to use for DNS-01 certificate issuance + default: gts-production + x-ui-toggle: true + x-ui-visible-if: + field: spec.use_dns01 + values: + - true + helm_wait: + type: boolean + title: Helm Wait + description: Wait for all resources to be ready before marking the release as successful + default: true + x-ui-toggle: true + required: + - private + - rules + - force_ssl_redirection + x-ui-order: + - private + - domain_prefix_override + - disable_base_domain + - domains + - force_ssl_redirection + - basic_auth + - body_size + - disable_endpoint_validation + - use_dns01 + - dns01_cluster_issuer + - data_plane + - control_plane + - helm_values + - helm_wait + - rules +sample: + kind: ingress + flavor: nginx_gateway_fabric_legacy_azure + version: "1.0" + disabled: true + metadata: + annotations: {} + spec: + private: false + disable_base_domain: false + force_ssl_redirection: true + basic_auth: false + body_size: 150m + data_plane: + scaling: + min_replicas: 2 + max_replicas: 5 + target_cpu_utilization_percentage: 70 + target_memory_utilization_percentage: 80 + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: "1" + memory: 512Mi + control_plane: + scaling: + min_replicas: 2 + max_replicas: 3 + target_cpu_utilization_percentage: 70 + target_memory_utilization_percentage: 80 + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + rules: {} diff --git a/modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/main.tf new file mode 100644 index 00000000..97cc5c7a --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/main.tf @@ -0,0 +1,220 @@ +locals { + # Compute name the same way as the base module + name = lower(var.environment.namespace == "default" ? var.instance_name : "${var.environment.namespace}-${var.instance_name}") + + # --- DNS-01 configuration --- + use_dns01 = lookup(var.instance.spec, "use_dns01", false) + dns01_cluster_issuer = lookup(var.instance.spec, "dns01_cluster_issuer", "gts-production") + + # Compute base domain (mirrors utility module logic — needed to take over base domain for DNS-01) + instance_env_name = length(var.environment.unique_name) + length(var.instance_name) + length(var.cc_metadata.tenant_base_domain) >= 60 ? substr(md5("${var.instance_name}-${var.environment.unique_name}"), 0, 20) : "${var.instance_name}-${var.environment.unique_name}" + check_domain_prefix = coalesce(lookup(var.instance.spec, "domain_prefix_override", null), local.instance_env_name) + base_domain = lower("${local.check_domain_prefix}.${var.cc_metadata.tenant_base_domain}") + + # --- DNS-01 domain handling --- + # User-configured domains eligible for DNS-01: use_dns01 enabled + no certificate_reference + dns01_domains = { + for domain_key, domain in lookup(var.instance.spec, "domains", {}) : + domain_key => domain + if local.use_dns01 && lookup(domain, "certificate_reference", "") == "" + } + + # Base domain for DNS-01: when use_dns01 is enabled and base domain is not disabled, + # we take over the base domain from the utility module (set disable_base_domain=true + # and re-add it ourselves with certificate_reference for wildcard listener) + dns01_base_domain = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? { + "facets" = { + domain = local.base_domain + alias = "base" + } + } : {} + + # All domains that need DNS-01 wildcard certs (user domains + base domain) + all_dns01_domains = merge(local.dns01_domains, local.dns01_base_domain) + + # K8s secret names for DNS-01 wildcard certs + dns01_cert_secret_names = { + for domain_key, domain in local.all_dns01_domains : + domain_key => "${local.name}-${domain_key}-dns01-tls" + } + + # Final domain rewrite: apply DNS-01 certificate_reference + # Setting certificate_reference causes the utility module to use wildcard listeners (*.domain) + modified_domains = merge( + { + for domain_key, domain in lookup(var.instance.spec, "domains", {}) : + domain_key => contains(keys(local.dns01_cert_secret_names), domain_key) ? merge(domain, { + certificate_reference = local.dns01_cert_secret_names[domain_key] + }) : domain + }, + # Add base domain with certificate_reference when DNS-01 is active + { + for domain_key, domain in local.dns01_base_domain : + domain_key => merge(domain, { + certificate_reference = local.dns01_cert_secret_names[domain_key] + }) + } + ) + + # Build modified instance with rewritten domains + # When DNS-01 is active: disable_base_domain=true (we re-add it ourselves with certificate_reference) + modified_instance = merge(var.instance, { + spec = merge(var.instance.spec, { + domains = local.modified_domains + disable_base_domain = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? true : lookup(var.instance.spec, "disable_base_domain", false) + }) + }) + + # Merge default_tolerations into inputs so the utility module picks them up via kubernetes_node_pool_details.attributes.taints + default_tolerations = lookup(var.environment, "default_tolerations", []) + existing_taints = try(var.inputs.kubernetes_node_pool_details.attributes.taints, []) + merged_inputs = merge(var.inputs, { + kubernetes_node_pool_details = merge( + lookup(var.inputs, "kubernetes_node_pool_details", {}), + { + attributes = merge( + try(var.inputs.kubernetes_node_pool_details.attributes, {}), + { + taints = concat(local.default_tolerations, local.existing_taints) + } + ) + } + ) + }) + + # Azure Load Balancer annotations + azure_annotations = { + "service.beta.kubernetes.io/azure-load-balancer-internal" = lookup(var.instance.spec, "private", false) ? "true" : "false" + } +} + +# Call the base utility module +module "nginx_gateway_fabric" { + source = "github.com/Facets-cloud/facets-utility-modules//nginx_gateway_fabric" + + instance = local.modified_instance + instance_name = var.instance_name + environment = var.environment + inputs = local.merged_inputs + + service_annotations = local.azure_annotations +} + +# --- DNS-01 wildcard certificate resources --- +# cert-manager Certificate CRDs for DNS-01 wildcard domains. +# Issues wildcard certs (*.domain + domain) via the dns01 ClusterIssuer (e.g. gts-production). +# Only created when use_dns01 is enabled, for domains without existing certificate_reference. +module "dns01_certificate" { + for_each = local.all_dns01_domains + + source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resource" + name = "${local.name}-dns01-cert-${each.key}" + namespace = var.environment.namespace + advanced_config = {} + + data = { + apiVersion = "cert-manager.io/v1" + kind = "Certificate" + metadata = { + name = "${local.name}-dns01-cert-${each.key}" + namespace = var.environment.namespace + } + spec = { + secretName = local.dns01_cert_secret_names[each.key] + issuerRef = { + name = local.dns01_cluster_issuer + kind = "ClusterIssuer" + } + dnsNames = [ + each.value.domain, + "*.${each.value.domain}" + ] + } + } +} + +# Bootstrap TLS secrets for DNS-01 domains — Gateway 443 listeners need a TLS secret +# to start. cert-manager will overwrite these once the DNS-01 challenge succeeds. +resource "tls_private_key" "dns01_bootstrap" { + for_each = local.all_dns01_domains + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_self_signed_cert" "dns01_bootstrap" { + for_each = local.all_dns01_domains + private_key_pem = tls_private_key.dns01_bootstrap[each.key].private_key_pem + + subject { + common_name = each.value.domain + } + + validity_period_hours = 8760 # 1 year + + dns_names = [ + each.value.domain, + "*.${each.value.domain}" + ] + + allowed_uses = [ + "key_encipherment", + "digital_signature", + "server_auth" + ] +} + +resource "kubernetes_secret_v1" "dns01_bootstrap_tls" { + for_each = local.all_dns01_domains + + metadata { + name = local.dns01_cert_secret_names[each.key] + namespace = var.environment.namespace + } + + data = { + "tls.crt" = tls_self_signed_cert.dns01_bootstrap[each.key].cert_pem + "tls.key" = tls_private_key.dns01_bootstrap[each.key].private_key_pem + } + + type = "kubernetes.io/tls" + + lifecycle { + ignore_changes = [data, metadata[0].annotations, metadata[0].labels] + } +} + +# --- Route53 DNS records for DNS-01 base domain --- +# When use_dns01 is active, we set disable_base_domain=true in the modified instance +# which causes the utility module to skip Route53 record creation. +# We must create them ourselves to maintain DNS resolution. +resource "aws_route53_record" "cluster-base-domain" { + count = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? 1 : 0 + depends_on = [ + module.nginx_gateway_fabric + ] + zone_id = var.cc_metadata.tenant_base_domain_id + name = local.base_domain + type = module.nginx_gateway_fabric.record_type + ttl = "300" + records = [module.nginx_gateway_fabric.lb_record_value] + provider = aws3tooling + lifecycle { + prevent_destroy = true + } +} + +resource "aws_route53_record" "cluster-base-domain-wildcard" { + count = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? 1 : 0 + depends_on = [ + module.nginx_gateway_fabric + ] + zone_id = var.cc_metadata.tenant_base_domain_id + name = "*.${local.base_domain}" + type = module.nginx_gateway_fabric.record_type + ttl = "300" + records = [module.nginx_gateway_fabric.lb_record_value] + provider = aws3tooling + lifecycle { + prevent_destroy = true + } +} diff --git a/modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/outputs.tf b/modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/outputs.tf new file mode 100644 index 00000000..e61aea06 --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/outputs.tf @@ -0,0 +1,90 @@ +locals { + output_attributes = module.nginx_gateway_fabric.output_attributes + output_interfaces = module.nginx_gateway_fabric.output_interfaces +} + +output "domains" { + value = module.nginx_gateway_fabric.domains +} + +output "nginx_gateway_fabric" { + value = module.nginx_gateway_fabric.nginx_gateway_fabric +} + +output "domain" { + value = module.nginx_gateway_fabric.domain +} + +output "secure_endpoint" { + value = module.nginx_gateway_fabric.secure_endpoint +} + +output "gateway_class" { + value = module.nginx_gateway_fabric.gateway_class + description = "The GatewayClass name used by this gateway" +} + +output "gateway_name" { + value = module.nginx_gateway_fabric.gateway_name + description = "The Gateway resource name" +} + +output "subdomain" { + value = module.nginx_gateway_fabric.subdomain +} + +output "tls_secret" { + value = module.nginx_gateway_fabric.tls_secret + description = "Map of domain keys to their TLS certificate secret names" +} + +output "load_balancer_hostname" { + value = module.nginx_gateway_fabric.load_balancer_hostname + description = "Load balancer hostname (for CNAME records)" +} + +output "load_balancer_ip" { + value = module.nginx_gateway_fabric.load_balancer_ip + description = "Load balancer IP address (for A records)" +} + +output "lb_record_value" { + value = module.nginx_gateway_fabric.lb_record_value + description = "The value to use in DNS records (hostname or IP)" +} + +output "record_type" { + value = module.nginx_gateway_fabric.record_type + description = "DNS record type (CNAME or A)" +} + +output "name" { + value = module.nginx_gateway_fabric.name + description = "The computed resource name" +} + +output "base_domain" { + value = module.nginx_gateway_fabric.base_domain + description = "The computed base domain" +} + +output "base_subdomain" { + value = module.nginx_gateway_fabric.base_subdomain + description = "The wildcard base subdomain" +} + +output "username" { + value = module.nginx_gateway_fabric.username + description = "Basic auth username (empty if disabled)" +} + +output "password" { + value = module.nginx_gateway_fabric.password + sensitive = true + description = "Basic auth password (empty if disabled)" +} + +output "cloud_provider" { + value = module.nginx_gateway_fabric.cloud_provider + description = "Detected cloud provider (AWS, GCP, AZURE)" +} diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/variables.tf b/modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/variables.tf similarity index 100% rename from modules/ingress/nginx_gateway_fabric_legacy/1.0/variables.tf rename to modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/variables.tf diff --git a/modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/README.md b/modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/README.md new file mode 100644 index 00000000..29812a13 --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/README.md @@ -0,0 +1,200 @@ +# NGINX Gateway Fabric (GCP Legacy) + +Kubernetes Gateway API implementation for GCP with GKE Load Balancer and DNS-01 support. + +## Overview + +This module is a **GCP-specific wrapper** around the base `nginx_gateway_fabric` utility module. It adds: + +- **GCP Load Balancer**: Internal LB with global access support for private deployments +- **DNS-01 Wildcard Certificates**: Optional DNS-01 validation via a pre-existing ClusterIssuer (e.g., `gts-production`) for wildcard listeners + +This is the **legacy** flavor that uses `cc_metadata` and legacy input conventions. + +--- + +## Architecture + +``` + ┌──────────────────────────────────────────────┐ + │ GCP Wrapper (this module) │ + │ │ + │ 1. DNS-01 → certificate_reference rewrite │ + │ 2. GCP LB annotations │ + │ │ + │ ┌──────────────────────────────────┐ │ + │ │ Base Utility Module │ │ + Internet ────────► │ │ - Gateway + Listeners │ │ + (GCP LB) │ │ - HTTPRoute / GRPCRoute │ │ + │ │ - Helm chart deployment │ │ + │ │ - HTTP-01 certs (if needed) │ │ + │ └──────────────────────────────────┘ │ + └──────────────────────────────────────────────┘ +``` + +--- + +## TLS Certificate Flows + +Two certificate strategies per domain: + +| Domain has | Flow | Listener | Managed by | +|---|---|---|---| +| No cert ref + `use_dns01: true` | cert-manager DNS-01 via ClusterIssuer | Wildcard (`*.domain`) | GCP wrapper | +| No cert ref + `use_dns01: false` | cert-manager HTTP-01 (default) | Exact hostname | Utility module | +| K8s secret in `certificate_reference` | User-managed | Wildcard (`*.domain`) | User | + +### HTTP-01 (Default) + +No extra configuration needed. The utility module creates a bundled HTTP-01 ClusterIssuer and issues certificates automatically via Let's Encrypt. + +### DNS-01 Wildcard Certificates + +When `use_dns01: true`, the module: + +1. Creates cert-manager `Certificate` resources requesting wildcard certs (`*.domain` + `domain`) from the specified ClusterIssuer +2. Creates bootstrap self-signed TLS secrets so Gateway listeners can start immediately +3. Sets `certificate_reference` on all domains, causing the utility module to create wildcard listeners (`*.domain`) +4. Disables the utility module's HTTP-01 ClusterIssuer and bootstrap cert creation (no domains left without `certificate_reference`) +5. Creates Route53 records for the base domain (since the utility module skips them when `disable_base_domain` is set internally) + +**Prerequisite**: The ClusterIssuer (default: `gts-production`) must already exist in the cluster with a DNS-01 solver configured. + +```json +{ + "spec": { + "use_dns01": true, + "dns01_cluster_issuer": "gts-production", + "domains": { + "production": { + "domain": "api.example.com", + "alias": "prod" + } + } + } +} +``` + +This creates: +- Wildcard listener: `*.api.example.com` +- Certificate: `*.api.example.com` + `api.example.com` via `gts-production` +- Base domain also gets a wildcard listener and DNS-01 cert + +--- + +## Configuration + +### Basic Example + +```json +{ + "kind": "ingress", + "flavor": "nginx_gateway_fabric_legacy_gcp", + "version": "1.0", + "spec": { + "private": false, + "force_ssl_redirection": true, + "rules": { + "api": { + "service_name": "api-service", + "namespace": "default", + "port": "8080", + "path": "/api", + "path_type": "PathPrefix" + } + } + } +} +``` + +### DNS-01 + Private LB Example + +```json +{ + "kind": "ingress", + "flavor": "nginx_gateway_fabric_legacy_gcp", + "version": "1.0", + "spec": { + "private": true, + "force_ssl_redirection": true, + "use_dns01": true, + "dns01_cluster_issuer": "gts-production", + "domains": { + "internal": { + "domain": "internal.example.com", + "alias": "internal" + } + }, + "rules": { + "api": { + "service_name": "api-service", + "namespace": "default", + "port": "8080", + "path": "/api" + } + } + } +} +``` + +--- + +## Spec Options + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `private` | boolean | `false` | Use internal load balancer | +| `force_ssl_redirection` | boolean | `true` | Redirect HTTP to HTTPS | +| `disable_base_domain` | boolean | `false` | Disable auto-generated base domain | +| `domain_prefix_override` | string | - | Override auto-generated domain prefix | +| `use_dns01` | boolean | `false` | Enable DNS-01 wildcard certificates for all domains | +| `dns01_cluster_issuer` | string | `gts-production` | ClusterIssuer name for DNS-01 validation | +| `disable_endpoint_validation` | boolean | `false` | Disable HTTP endpoint validation | +| `basic_auth` | boolean | `false` | Enable basic authentication | +| `body_size` | string | `150m` | Maximum client request body size | +| `helm_wait` | boolean | `true` | Wait for Helm release to be ready | +| `helm_values` | object | - | Additional Helm values | + +--- + +## Inputs + +| Input | Type | Required | Description | +|-------|------|----------|-------------| +| `kubernetes_details` | `@outputs/kubernetes` | Yes | Kubernetes cluster connection | +| `gateway_api_crd_details` | `@outputs/gateway_api_crd` | Yes | Gateway API CRD installation | +| `prometheus_details` | `@outputs/prometheus` | No | Prometheus for PodMonitor | + +--- + +## GCP-Specific Behavior + +### Load Balancer Configuration + +- **Public**: No additional annotations (default GKE external LB) +- **Private**: Internal LB with global access enabled (`cloud.google.com/load-balancer-type: Internal`, `networking.gke.io/internal-load-balancer-allow-global-access: true`) + +--- + +## Troubleshooting + +### Check DNS-01 Certificate Status + +```bash +kubectl get certificate -n +kubectl describe certificate -dns01-cert- -n +kubectl get clusterissuer gts-production -o yaml +``` + +### Check Gateway Listeners + +```bash +kubectl get gateway -n -o yaml | grep -A 20 listeners +``` + +### Load Balancer Issues + +```bash +kubectl get svc -n -l app.kubernetes.io/name=nginx-gateway-fabric +kubectl describe svc -n +``` diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml b/modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/facets.yaml similarity index 97% rename from modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml rename to modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/facets.yaml index 6e77338c..111abb75 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml +++ b/modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/facets.yaml @@ -1,10 +1,8 @@ intent: ingress -flavor: nginx_gateway_fabric_legacy +flavor: nginx_gateway_fabric_legacy_gcp version: "1.0" -description: NGINX Gateway Fabric - Kubernetes Gateway API implementation (Legacy module format) +description: NGINX Gateway Fabric for GCP - Gateway API with GKE Load Balancer and DNS-01 support (Legacy module format) clouds: - - aws - - azure - gcp - kubernetes inputs: @@ -34,7 +32,7 @@ outputs: default: type: "@outputs/ingress" spec: - title: NGINX Gateway Fabric + title: NGINX Gateway Fabric (GCP) type: object properties: private: @@ -227,7 +225,6 @@ spec: rules: title: Routing Rules description: Gateway API routing configurations (HTTP and gRPC) - type: object patternProperties: ^[a-zA-Z0-9_.-]*$: @@ -841,6 +838,22 @@ spec: description: Disable endpoint validation for cert-manager (uses DNS validation instead of HTTP) default: false x-ui-toggle: true + use_dns01: + type: boolean + title: Use DNS-01 Resolver + description: Use DNS-01 challenge type for certificate issuance. When enabled, all domain listeners use wildcard hostnames (*.domain) and certificates are issued via the DNS-01 ClusterIssuer. + default: false + x-ui-toggle: true + dns01_cluster_issuer: + type: string + title: DNS-01 ClusterIssuer + description: Name of the ClusterIssuer to use for DNS-01 certificate issuance + default: gts-production + x-ui-toggle: true + x-ui-visible-if: + field: spec.use_dns01 + values: + - true helm_wait: type: boolean title: Helm Wait @@ -860,6 +873,8 @@ spec: - basic_auth - body_size - disable_endpoint_validation + - use_dns01 + - dns01_cluster_issuer - data_plane - control_plane - helm_values @@ -867,7 +882,7 @@ spec: - rules sample: kind: ingress - flavor: nginx_gateway_fabric_legacy + flavor: nginx_gateway_fabric_legacy_gcp version: "1.0" disabled: true metadata: diff --git a/modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/main.tf new file mode 100644 index 00000000..42567d39 --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/main.tf @@ -0,0 +1,222 @@ +locals { + # Compute name the same way as the base module (needed for cert secret names) + name = lower(var.environment.namespace == "default" ? var.instance_name : "${var.environment.namespace}-${var.instance_name}") + + # --- DNS-01 configuration --- + use_dns01 = lookup(var.instance.spec, "use_dns01", false) + dns01_cluster_issuer = lookup(var.instance.spec, "dns01_cluster_issuer", "gts-production") + + # Compute base domain (mirrors utility module logic — needed to take over base domain for DNS-01) + instance_env_name = length(var.environment.unique_name) + length(var.instance_name) + length(var.cc_metadata.tenant_base_domain) >= 60 ? substr(md5("${var.instance_name}-${var.environment.unique_name}"), 0, 20) : "${var.instance_name}-${var.environment.unique_name}" + check_domain_prefix = coalesce(lookup(var.instance.spec, "domain_prefix_override", null), local.instance_env_name) + base_domain = lower("${local.check_domain_prefix}.${var.cc_metadata.tenant_base_domain}") + + # --- DNS-01 domain handling --- + # User-configured domains eligible for DNS-01: use_dns01 enabled + no certificate_reference + dns01_domains = { + for domain_key, domain in lookup(var.instance.spec, "domains", {}) : + domain_key => domain + if local.use_dns01 && lookup(domain, "certificate_reference", "") == "" + } + + # Base domain for DNS-01: when use_dns01 is enabled and base domain is not disabled, + # we take over the base domain from the utility module (set disable_base_domain=true + # and re-add it ourselves with certificate_reference for wildcard listener) + dns01_base_domain = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? { + "facets" = { + domain = local.base_domain + alias = "base" + } + } : {} + + # All domains that need DNS-01 wildcard certs (user domains + base domain) + all_dns01_domains = merge(local.dns01_domains, local.dns01_base_domain) + + # K8s secret names for DNS-01 wildcard certs + dns01_cert_secret_names = { + for domain_key, domain in local.all_dns01_domains : + domain_key => "${local.name}-${domain_key}-dns01-tls" + } + + # Final domain rewrite: apply DNS-01 certificate_reference + # Setting certificate_reference causes the utility module to use wildcard listeners (*.domain) + modified_domains = merge( + { + for domain_key, domain in lookup(var.instance.spec, "domains", {}) : + domain_key => contains(keys(local.dns01_cert_secret_names), domain_key) ? merge(domain, { + certificate_reference = local.dns01_cert_secret_names[domain_key] + }) : domain + }, + # Add base domain with certificate_reference when DNS-01 is active + { + for domain_key, domain in local.dns01_base_domain : + domain_key => merge(domain, { + certificate_reference = local.dns01_cert_secret_names[domain_key] + }) + } + ) + + # Build modified instance with rewritten domains + # When DNS-01 is active: disable_base_domain=true (we re-add it ourselves with certificate_reference) + modified_instance = merge(var.instance, { + spec = merge(var.instance.spec, { + domains = local.modified_domains + disable_base_domain = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? true : lookup(var.instance.spec, "disable_base_domain", false) + }) + }) + + # Merge default_tolerations into inputs so the utility module picks them up via kubernetes_node_pool_details.attributes.taints + default_tolerations = lookup(var.environment, "default_tolerations", []) + existing_taints = try(var.inputs.kubernetes_node_pool_details.attributes.taints, []) + merged_inputs = merge(var.inputs, { + kubernetes_node_pool_details = merge( + lookup(var.inputs, "kubernetes_node_pool_details", {}), + { + attributes = merge( + try(var.inputs.kubernetes_node_pool_details.attributes, {}), + { + taints = concat(local.default_tolerations, local.existing_taints) + } + ) + } + ) + }) + + # GCP Load Balancer annotations + gcp_annotations = lookup(var.instance.spec, "private", false) ? { + "cloud.google.com/load-balancer-type" = "Internal" + "networking.gke.io/load-balancer-type" = "Internal" + "networking.gke.io/internal-load-balancer-allow-global-access" = "true" + } : {} +} + +# Call the base utility module +module "nginx_gateway_fabric" { + source = "github.com/Facets-cloud/facets-utility-modules//nginx_gateway_fabric" + + instance = local.modified_instance + instance_name = var.instance_name + environment = var.environment + inputs = local.merged_inputs + + service_annotations = local.gcp_annotations +} + +# --- DNS-01 wildcard certificate resources --- +# cert-manager Certificate CRDs for DNS-01 wildcard domains. +# Issues wildcard certs (*.domain + domain) via the dns01 ClusterIssuer (e.g. gts-production). +# Only created when use_dns01 is enabled, for domains without existing certificate_reference. +module "dns01_certificate" { + for_each = local.all_dns01_domains + + source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resource" + name = "${local.name}-dns01-cert-${each.key}" + namespace = var.environment.namespace + advanced_config = {} + + data = { + apiVersion = "cert-manager.io/v1" + kind = "Certificate" + metadata = { + name = "${local.name}-dns01-cert-${each.key}" + namespace = var.environment.namespace + } + spec = { + secretName = local.dns01_cert_secret_names[each.key] + issuerRef = { + name = local.dns01_cluster_issuer + kind = "ClusterIssuer" + } + dnsNames = [ + each.value.domain, + "*.${each.value.domain}" + ] + } + } +} + +# Bootstrap TLS secrets for DNS-01 domains — Gateway 443 listeners need a TLS secret +# to start. cert-manager will overwrite these once the DNS-01 challenge succeeds. +resource "tls_private_key" "dns01_bootstrap" { + for_each = local.all_dns01_domains + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_self_signed_cert" "dns01_bootstrap" { + for_each = local.all_dns01_domains + private_key_pem = tls_private_key.dns01_bootstrap[each.key].private_key_pem + + subject { + common_name = each.value.domain + } + + validity_period_hours = 8760 # 1 year + + dns_names = [ + each.value.domain, + "*.${each.value.domain}" + ] + + allowed_uses = [ + "key_encipherment", + "digital_signature", + "server_auth" + ] +} + +resource "kubernetes_secret_v1" "dns01_bootstrap_tls" { + for_each = local.all_dns01_domains + + metadata { + name = local.dns01_cert_secret_names[each.key] + namespace = var.environment.namespace + } + + data = { + "tls.crt" = tls_self_signed_cert.dns01_bootstrap[each.key].cert_pem + "tls.key" = tls_private_key.dns01_bootstrap[each.key].private_key_pem + } + + type = "kubernetes.io/tls" + + lifecycle { + ignore_changes = [data, metadata[0].annotations, metadata[0].labels] + } +} + +# --- Route53 DNS records for DNS-01 base domain --- +# When use_dns01 is active, we set disable_base_domain=true in the modified instance +# which causes the utility module to skip Route53 record creation. +# We must create them ourselves to maintain DNS resolution. +resource "aws_route53_record" "cluster-base-domain" { + count = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? 1 : 0 + depends_on = [ + module.nginx_gateway_fabric + ] + zone_id = var.cc_metadata.tenant_base_domain_id + name = local.base_domain + type = module.nginx_gateway_fabric.record_type + ttl = "300" + records = [module.nginx_gateway_fabric.lb_record_value] + provider = aws3tooling + lifecycle { + prevent_destroy = true + } +} + +resource "aws_route53_record" "cluster-base-domain-wildcard" { + count = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? 1 : 0 + depends_on = [ + module.nginx_gateway_fabric + ] + zone_id = var.cc_metadata.tenant_base_domain_id + name = "*.${local.base_domain}" + type = module.nginx_gateway_fabric.record_type + ttl = "300" + records = [module.nginx_gateway_fabric.lb_record_value] + provider = aws3tooling + lifecycle { + prevent_destroy = true + } +} diff --git a/modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/outputs.tf b/modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/outputs.tf new file mode 100644 index 00000000..e61aea06 --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/outputs.tf @@ -0,0 +1,90 @@ +locals { + output_attributes = module.nginx_gateway_fabric.output_attributes + output_interfaces = module.nginx_gateway_fabric.output_interfaces +} + +output "domains" { + value = module.nginx_gateway_fabric.domains +} + +output "nginx_gateway_fabric" { + value = module.nginx_gateway_fabric.nginx_gateway_fabric +} + +output "domain" { + value = module.nginx_gateway_fabric.domain +} + +output "secure_endpoint" { + value = module.nginx_gateway_fabric.secure_endpoint +} + +output "gateway_class" { + value = module.nginx_gateway_fabric.gateway_class + description = "The GatewayClass name used by this gateway" +} + +output "gateway_name" { + value = module.nginx_gateway_fabric.gateway_name + description = "The Gateway resource name" +} + +output "subdomain" { + value = module.nginx_gateway_fabric.subdomain +} + +output "tls_secret" { + value = module.nginx_gateway_fabric.tls_secret + description = "Map of domain keys to their TLS certificate secret names" +} + +output "load_balancer_hostname" { + value = module.nginx_gateway_fabric.load_balancer_hostname + description = "Load balancer hostname (for CNAME records)" +} + +output "load_balancer_ip" { + value = module.nginx_gateway_fabric.load_balancer_ip + description = "Load balancer IP address (for A records)" +} + +output "lb_record_value" { + value = module.nginx_gateway_fabric.lb_record_value + description = "The value to use in DNS records (hostname or IP)" +} + +output "record_type" { + value = module.nginx_gateway_fabric.record_type + description = "DNS record type (CNAME or A)" +} + +output "name" { + value = module.nginx_gateway_fabric.name + description = "The computed resource name" +} + +output "base_domain" { + value = module.nginx_gateway_fabric.base_domain + description = "The computed base domain" +} + +output "base_subdomain" { + value = module.nginx_gateway_fabric.base_subdomain + description = "The wildcard base subdomain" +} + +output "username" { + value = module.nginx_gateway_fabric.username + description = "Basic auth username (empty if disabled)" +} + +output "password" { + value = module.nginx_gateway_fabric.password + sensitive = true + description = "Basic auth password (empty if disabled)" +} + +output "cloud_provider" { + value = module.nginx_gateway_fabric.cloud_provider + description = "Detected cloud provider (AWS, GCP, AZURE)" +} diff --git a/modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/variables.tf b/modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/variables.tf new file mode 100644 index 00000000..0d889c8c --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_gcp/1.0/variables.tf @@ -0,0 +1,37 @@ +variable "cluster" { + type = any + default = {} +} + +variable "baseinfra" { + type = any + default = {} +} + +variable "cc_metadata" { + type = any + default = { + tenant_base_domain = "tenant.facets.cloud" + } +} + +variable "instance" { + type = any +} + +variable "instance_name" { + type = string + default = "test_instance" +} + +variable "environment" { + type = any + default = { + namespace = "default" + } +} + +variable "inputs" { + type = any + default = {} +} From 3a7060fb58a0fd59135ee7e14022619b266a5a4a Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Mon, 16 Mar 2026 18:46:04 +0530 Subject: [PATCH 07/13] fix: remove leftover migration files from deleted legacy module Co-Authored-By: Claude Opus 4.6 (1M context) --- .../1.0/MIGRATION.md | 277 -------------- .../1.0/convert_nginx_ingress.py | 345 ------------------ 2 files changed, 622 deletions(-) delete mode 100644 modules/ingress/nginx_gateway_fabric_legacy/1.0/MIGRATION.md delete mode 100644 modules/ingress/nginx_gateway_fabric_legacy/1.0/convert_nginx_ingress.py diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/MIGRATION.md b/modules/ingress/nginx_gateway_fabric_legacy/1.0/MIGRATION.md deleted file mode 100644 index 6a3f90da..00000000 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/MIGRATION.md +++ /dev/null @@ -1,277 +0,0 @@ -# Migration Guide: `nginx_ingress_controller` to `nginx_gateway_fabric_legacy_aws` - -## Why migrate? - -The community-maintained **ingress-nginx** controller has reached **end-of-life**. Continued use means no security patches, no bug fixes, and increasing incompatibility with newer Kubernetes versions. The old ingress-nginx setup worked fine, but with the project at EOL we are moving to the **Kubernetes Gateway API**, which is the successor to the Ingress API -- it is now GA, natively supported by the Kubernetes project, and provides a standardised, role-oriented model for traffic routing. NGINX Gateway Fabric is the Gateway API implementation used by this module. - -Because NGINX Gateway Fabric's implementation of the Gateway API is still evolving, regex-based routing showed **unreliable and unpredictable behaviour** during our testing -- path ordering conflicts, capture-group interactions, and inconsistent matching made it difficult to reason about which backend ultimately received a request. For this reason, `nginx_gateway_fabric_legacy_aws` **enforces path-prefix-based routing only**. Regex paths (`~`, `~*`, `^` anchors) are not supported. Only `PathPrefix` and `Exact` path types are allowed. This keeps routing rules simple, auditable, and reproducible across environments. - ---- - -## What changes? - -| Area | Old (`nginx_ingress_controller`) | New (`nginx_gateway_fabric_legacy_aws`) | -|------|----------------------------------|-------------------------------------| -| **Flavor** | `nginx_ingress_controller` | `nginx_gateway_fabric_legacy_aws` | -| **Routing model** | Regex + prefix (annotation-driven) | **Path prefix only** (`PathPrefix` / `Exact`) | -| **Annotations on rules** | Supported (custom NGINX snippets, rewrites, etc.) | **Not supported** -- must be translated to native Gateway API features (header matching, URL rewrite, CORS, etc.) | -| **TLS certificates** | ACM ARNs or manual | ACM ARNs supported natively (via ACK), DNS-01 wildcard certs enabled by default (`use_dns01: true`) | -| **`basicAuth` / `basic_auth`** | Supported | Dropped (not natively supported in NGF) | -| **`equivalent_prefixes`** | Supported on domains | Dropped | -| **`grpc` (boolean)** | Global flag in `spec` | Per-rule `grpc_config` block | -| **`port_name`** | Accepted | Dropped -- use `port` (number) instead | -| **`advanced.nginx_ingress_controller.values`** | Helm overrides | Moved to `spec.helm_values` | -| **`advanced.nginx_ingress_controller.domain_prefix_override`** | Advanced block | Moved to `spec.domain_prefix_override` | -| **`advanced.nginx_ingress_controller.disable_endpoint_validation`** | Advanced block | Moved to `spec.disable_endpoint_validation` | -| **Domain-level `rules`** | Nested inside each domain object | Flattened into global `spec.rules` (keyed as `_`) | -| **`certificate_reference`** | Can be an AWS ACM ARN | ACM ARNs are supported natively; alternatively use a K8s TLS Secret name | -| **Namespace** | Optional | Required on every rule (derived automatically when using `${service..out.interfaces.main.name}` templates) | - ---- - -## Conversion script - -A Python helper (`convert_nginx_ingress.py`) is included in this module to automate the mechanical parts of the migration: - -```bash -python3 convert_nginx_ingress.py [-o output.json] [--default-namespace default] -``` - -### What the script does automatically - -- Updates `flavor` to `nginx_gateway_fabric_legacy_aws`, `kind`, and `version`. -- Enables `use_dns01: true` and `dns01_cluster_issuer: "gts-production"` for DNS-01 wildcard certificates. -- Moves `advanced.nginx_ingress_controller.*` fields into `spec`. -- Flattens domain-level rules into `spec.rules`. -- Adds `path_type: "PathPrefix"` to every rule. -- Strips `^` anchors from paths. -- Converts `grpc: true` to `grpc_config: { "enabled": true, "match_all_methods": true }`. -- Derives `namespace` from `${service..out.interfaces.main.name}` templates, or falls back to `--default-namespace`. -- Preserves ACM ARN `certificate_reference` values (the AWS module handles them natively via ACK). -- Adds `ack_acm_controller_details` input (required for ACM ARN domains). -- Drops unsupported fields (`basicAuth`, `equivalent_prefixes`, `annotations`, `disable_auth`, `port_name`, `allow_wildcard`, etc.) and prints warnings to stderr. - -### What requires manual effort from your side - -1. **Custom annotations** -- The old format allowed arbitrary NGINX annotations on each rule (snippets, rewrite targets, rate limits, proxy settings, etc.). These have **no automatic translation**. You must review each dropped annotation and re-implement the behaviour using native Gateway API features (header matching, URL rewriting, CORS, request/response header modifiers, timeouts, etc.). Refer to the [README](README.md) for available options. - -2. **Regex-based paths** -- Any rule whose `path` contained regex syntax (`~`, `~*`, capture groups, character classes) must be rewritten as one or more `PathPrefix` or `Exact` rules. There is no regex path support in this module. - -3. **Certificate references** -- With `use_dns01: true` (enabled by default), certificates are issued automatically as wildcard certs via the `gts-production` ClusterIssuer using DNS-01 validation. ACM ARN `certificate_reference` values are preserved and handled natively by the AWS module via the ACK ACM controller. Ensure the `ack_acm_controller` module is deployed if you have ACM ARN domains. - -4. **`port_name` without `port`** -- If any rule only specified `port_name` (no numeric `port`), you must look up the correct port number and add it. - ---- - -## Migration steps - -1. **Run the converter:** - ```bash - python3 convert_nginx_ingress.py my-instance.json -o my-instance-converted.json - ``` - -2. **Review warnings** printed to stderr. Each warning identifies a field that was dropped or needs attention. - -3. **Handle annotations manually.** For each rule that had custom annotations, decide how to express the same behaviour using the examples below. - -### Proxy timeouts - -Old (annotation): -```json -{ - "annotations": { - "nginx.ingress.kubernetes.io/proxy-read-timeout": "60", - "nginx.ingress.kubernetes.io/proxy-send-timeout": "30" - } -} -``` - -New (`timeouts` block on the rule): -```json -{ - "rules": { - "api": { - "service_name": "api-service", - "namespace": "default", - "port": 8080, - "path": "/api", - "path_type": "PathPrefix", - "timeouts": { - "request": "60s", - "backend_request": "30s" - } - } - } -} -``` - -### Rewrite target - -Old (annotation): -```json -{ - "annotations": { - "nginx.ingress.kubernetes.io/rewrite-target": "/v2/$1" - } -} -``` - -New (`url_rewrite` block on the rule): -```json -{ - "rules": { - "legacy_api": { - "service_name": "new-api-service", - "namespace": "default", - "port": 8080, - "path": "/old-api", - "path_type": "PathPrefix", - "url_rewrite": { - "rewrite_rule": { - "path_type": "ReplacePrefixMatch", - "replace_path": "/v2/api" - } - } - } - } -} -``` - -> **Note:** Regex capture groups (`$1`, `$2`) are not supported. You must express rewrites as prefix replacements (`ReplacePrefixMatch`) or full path replacements (`ReplaceFullPath`). - -### CORS headers - -Old (annotations): -```json -{ - "annotations": { - "nginx.ingress.kubernetes.io/enable-cors": "true", - "nginx.ingress.kubernetes.io/cors-allow-origin": "https://example.com,https://app.example.com", - "nginx.ingress.kubernetes.io/cors-allow-methods": "GET,POST,PUT", - "nginx.ingress.kubernetes.io/cors-allow-headers": "Content-Type,Authorization", - "nginx.ingress.kubernetes.io/cors-allow-credentials": "true", - "nginx.ingress.kubernetes.io/cors-max-age": "86400" - } -} -``` - -New (`cors` block on the rule): -```json -{ - "rules": { - "api": { - "service_name": "api-service", - "namespace": "default", - "port": 8080, - "path": "/", - "path_type": "PathPrefix", - "cors": { - "enabled": true, - "allow_origins": { - "origin1": { "origin": "https://example.com" }, - "origin2": { "origin": "https://app.example.com" } - }, - "allow_methods": { - "get": { "method": "GET" }, - "post": { "method": "POST" }, - "put": { "method": "PUT" } - }, - "allow_headers": { - "content_type": { "header": "Content-Type" }, - "auth": { "header": "Authorization" } - }, - "allow_credentials": true, - "max_age": 86400 - } - } - } -} -``` - -### Custom request/response headers - -Old (annotations): -```json -{ - "annotations": { - "nginx.ingress.kubernetes.io/configuration-snippet": "proxy_set_header X-Custom-Header custom-value;", - "nginx.ingress.kubernetes.io/server-snippet": "add_header X-Response-ID unique-id always;" - } -} -``` - -New (`request_header_modifier` / `response_header_modifier` on the rule): -```json -{ - "rules": { - "api": { - "service_name": "api-service", - "namespace": "default", - "port": 8080, - "path": "/", - "path_type": "PathPrefix", - "request_header_modifier": { - "add": { - "custom_header": { - "name": "X-Custom-Header", - "value": "custom-value" - } - }, - "set": { - "source_header": { - "name": "X-Request-Source", - "value": "gateway" - } - }, - "remove": { - "sensitive_header": { - "name": "X-Sensitive-Header" - } - } - }, - "response_header_modifier": { - "add": { - "response_id": { - "name": "X-Response-ID", - "value": "unique-id" - } - }, - "remove": { - "server_header": { - "name": "Server" - } - } - } - } - } -} -``` - -### No native equivalent -- use `spec.helm_values` - -If an annotation has no direct Gateway API equivalent (e.g., custom NGINX snippets, rate limiting, IP whitelisting), you can apply global NGINX configuration via `spec.helm_values`: - -```json -{ - "spec": { - "helm_values": { - "nginx": { - "config": { - "entries": { - "proxy-buffer-size": "16k", - "client-max-body-size": "50m" - } - } - } - } - } -} -``` - -> **Note:** `helm_values` applies globally, not per-rule. Features like per-rule rate limiting and IP whitelisting are not natively supported in NGINX Gateway Fabric. - -4. **Replace regex paths.** Split complex regex rules into multiple prefix rules if needed. - -5. **Verify ACM setup.** If you have domains with ACM ARN `certificate_reference`, ensure the `ack_acm_controller` module is deployed and the `ack_acm_controller_details` input is configured. - -6. **Validate** the output JSON and deploy to a staging environment first. diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/convert_nginx_ingress.py b/modules/ingress/nginx_gateway_fabric_legacy/1.0/convert_nginx_ingress.py deleted file mode 100644 index fc7dd1ce..00000000 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/convert_nginx_ingress.py +++ /dev/null @@ -1,345 +0,0 @@ -#!/usr/bin/env python3 -""" -Converts a var.instance JSON from the old nginx_k8s_native (ingress controller) -format to the new nginx_gateway_fabric_legacy_aws/1.0 format. - -Enables use_dns01 by default with gts-production ClusterIssuer for wildcard -certificate issuance via DNS-01 validation. ACM ARN certificate_references -are preserved (the AWS module handles them natively via ACK). - -Usage: - python3 convert_nginx_ingress.py [-o output.json] [--default-namespace default] -""" - -import argparse -import json -import re -import sys - - -CERT_GUIDE_URL = ( - "https://docs.facets.cloud/docs/nginx-gateway-fabric-legacy#custom-certificates" -) - -SERVICE_TEMPLATE_RE = re.compile( - r"^\$\{service\.(?P[^.]+)\.out\.interfaces\.[^.]+\.name\}$" -) - -SUPPORTED_DOMAIN_FIELDS = {"domain", "alias", "certificate_reference", "rules"} -SUPPORTED_SPEC_FIELDS = { - "private", - "force_ssl_redirection", - "domains", - "rules", - "subdomains", - "grpc", - "basicAuth", - "basic_auth", - "allow_wildcard", - "ingress_chart_version", - "annotations_risk_level", - "body_size", - "data_plane", - "control_plane", -} - -DEFAULT_INPUTS = { - "kubernetes_details": { - "resource_name": "default", - "resource_type": "kubernetes_cluster", - }, - "gateway_api_crd_details": { - "resource_name": "", - "resource_type": "gateway_api_crd", - }, - "prometheus_details": { - "resource_name": "prometheus", - "resource_type": "configuration", - }, - "ack_acm_controller_details": { - "resource_name": "", - "resource_type": "ack_acm_controller", - }, -} - -DEFAULT_SPEC_FIELDS = { - "body_size": "150m", - "data_plane": { - "scaling": { - "min_replicas": 2, - "max_replicas": 5, - "target_cpu_utilization_percentage": 70, - "target_memory_utilization_percentage": 80, - }, - "resources": { - "requests": {"cpu": "250m", "memory": "256Mi"}, - "limits": {"cpu": "1", "memory": "512Mi"}, - }, - }, - "control_plane": { - "scaling": { - "min_replicas": 2, - "max_replicas": 3, - "target_cpu_utilization_percentage": 70, - "target_memory_utilization_percentage": 80, - }, - "resources": { - "requests": {"cpu": "200m", "memory": "256Mi"}, - "limits": {"cpu": "500m", "memory": "512Mi"}, - }, - }, -} -DROP_SPEC_FIELDS_WARN = {"basicAuth", "basic_auth", "allow_wildcard"} -DROP_SPEC_FIELDS_SILENT = {"ingress_chart_version", "annotations_risk_level", "grpc"} - - -def warn(msg): - print(f" [!] {msg}", file=sys.stderr) - - -def section(title): - print(f"\n{'=' * 60}", file=sys.stderr) - print(f" {title}", file=sys.stderr) - print(f"{'=' * 60}", file=sys.stderr) - - -def derive_namespace(service_name, existing_namespace, default_namespace): - if existing_namespace: - return existing_namespace - m = SERVICE_TEMPLATE_RE.match(service_name or "") - if m: - name = m.group("name") - return f"${{service.{name}.out.attributes.namespace}}" - return default_namespace - - -def convert_rule(key, rule, global_grpc, default_namespace): - new_rule = {} - - new_rule["service_name"] = rule.get("service_name", "") - - if "port" in rule: - new_rule["port"] = rule["port"] - elif "port_name" in rule: - warn(f"Rule '{key}': DROPPED 'port_name' (no numeric 'port' found).") - warn(f" ACTION REQUIRED: Look up the correct port number and add it.") - - path = rule.get("path", "/") - if path.startswith("^"): - path = path[1:] - new_rule["path"] = path - new_rule["path_type"] = "PathPrefix" - - ns = derive_namespace( - rule.get("service_name"), rule.get("namespace"), default_namespace - ) - new_rule["namespace"] = ns - - if rule.get("domain_prefix"): - new_rule["domain_prefix"] = rule["domain_prefix"] - - rule_grpc = rule.get("grpc", global_grpc) - if rule_grpc: - new_rule["grpc_config"] = {"enabled": True, "match_all_methods": True} - - if "annotations" in rule: - annotations = rule["annotations"] - if isinstance(annotations, dict) and annotations: - warn(f"Rule '{key}': DROPPED annotations (not supported in gateway fabric):") - for akey, aval in annotations.items(): - warn(f" - {akey}: {aval}") - else: - warn(f"Rule '{key}': DROPPED annotations (not supported in gateway fabric).") - if "domains" in rule: - warn(f"Rule '{key}': DROPPED 'domains' from rule.") - warn(f" Domains must be defined at spec.domains level and apply to all rules.") - warn(f" ACTION REQUIRED: Add required domains under spec.domains.") - if "disable_auth" in rule: - pass # silently drop - if "port_name" in rule and "port" in rule: - pass # port_name is redundant when port exists - - return new_rule - - -def convert(input_data, default_namespace): - output = {} - - # Top-level fields - output["flavor"] = "nginx_gateway_fabric_legacy_aws" - if "kind" in input_data: - output["kind"] = "ingress" - if "version" in input_data: - output["version"] = "1.0" - if "metadata" in input_data: - metadata = dict(input_data["metadata"]) - annotations = metadata.get("annotations", {}) - if annotations: - nginx_annotations = {k: v for k, v in annotations.items() if "nginx" in k.lower()} - if nginx_annotations: - section("Metadata Annotations") - for akey in nginx_annotations: - warn(f"DROPPED: '{akey}' -- nginx annotations are not supported in gateway fabric.") - filtered = {k: v for k, v in annotations.items() if "nginx" not in k.lower()} - metadata["annotations"] = filtered - output["metadata"] = metadata - - # Set default inputs, adding only missing keys - inputs = dict(input_data.get("inputs", {})) - missing_inputs = [k for k in DEFAULT_INPUTS if k not in inputs] - if missing_inputs: - section("Inputs") - for key, default_value in DEFAULT_INPUTS.items(): - if key not in inputs: - inputs[key] = default_value - if key in ("gateway_api_crd_details", "ack_acm_controller_details"): - warn(f"ADDED: '{key}' with placeholder resource_name ''.") - warn(f" ACTION REQUIRED: Update resource_name with the actual resource name.") - else: - warn(f"ADDED: '{key}' with default value.") - output["inputs"] = inputs - - spec = input_data.get("spec", {}) - advanced = input_data.get("advanced", {}) - adv_nic = advanced.get("nginx_ingress_controller", {}) - - out_spec = {} - - # Carry over supported simple fields - for field in ("private", "force_ssl_redirection", "subdomains"): - if field in spec: - out_spec[field] = spec[field] - - # Enable DNS-01 wildcard certs by default for AWS - out_spec["use_dns01"] = True - out_spec["dns01_cluster_issuer"] = "gts-production" - - # Move advanced fields into spec - if "domain_prefix_override" in adv_nic: - out_spec["domain_prefix_override"] = adv_nic["domain_prefix_override"] - if "disable_endpoint_validation" in adv_nic: - out_spec["disable_endpoint_validation"] = adv_nic["disable_endpoint_validation"] - - # Warn about helm values, dropped fields, unknown fields - has_adv = bool(adv_nic) - dropped_fields = [f for f in DROP_SPEC_FIELDS_WARN if spec.get(f)] - unknown_fields = [f for f in spec if f not in SUPPORTED_SPEC_FIELDS] - if has_adv or dropped_fields or unknown_fields: - section("Spec & Advanced") - if has_adv: - warn("advanced.nginx_ingress_controller contains helm values.") - warn(" These are NOT automatically migrated -- the helm charts are different.") - warn(" ACTION REQUIRED: Review and configure equivalent settings under spec.helm_values.") - for field in dropped_fields: - warn(f"DROPPED: spec.{field} -- not supported in gateway fabric.") - for field in unknown_fields: - warn(f"DROPPED: spec.{field} -- unknown field.") - - global_grpc = spec.get("grpc", False) - - # Process domains - domains_in = spec.get("domains", {}) - out_domains = {} - extracted_rules = {} - - if domains_in: - section("Domains") - - for dkey, dval in domains_in.items(): - out_domain = {} - for field in ("domain", "alias", "certificate_reference"): - if field in dval: - out_domain[field] = dval[field] - - cert = dval.get("certificate_reference", "") - if cert and "arn:aws:acm" in cert: - warn(f"Domain '{dkey}': KEPT ACM ARN certificate_reference.") - warn(f" The AWS module handles ACM ARNs natively via ACK ACM controller.") - warn(f" Ensure ack_acm_controller is deployed (see inputs).") - - if "equivalent_prefixes" in dval: - warn(f"Domain '{dkey}': DROPPED 'equivalent_prefixes' (not supported).") - - # Extract domain-level rules - if "rules" in dval: - for rkey, rval in dval["rules"].items(): - composed_key = f"{dkey}_{rkey}" - extracted_rules[composed_key] = rval - - # Warn about unsupported domain fields - for field in dval: - if field not in SUPPORTED_DOMAIN_FIELDS: - warn(f"Domain '{dkey}': DROPPED field '{field}' (not supported).") - - out_domains[dkey] = out_domain - - if out_domains: - out_spec["domains"] = out_domains - - # Process rules: global + domain-extracted - all_rules_in = {} - all_rules_in.update(spec.get("rules", {})) - all_rules_in.update(extracted_rules) - - # Check if any rules have annotations or domains that will be dropped - rules_with_issues = any( - "annotations" in r or "domains" in r or ("port_name" in r and "port" not in r) - for r in all_rules_in.values() - ) - if rules_with_issues: - section("Rules") - - out_rules = {} - for rkey, rval in all_rules_in.items(): - out_rules[rkey] = convert_rule(rkey, rval, global_grpc, default_namespace) - - if out_rules: - out_spec["rules"] = out_rules - - # Set defaults for fields that are new in gateway fabric - for field, default_value in DEFAULT_SPEC_FIELDS.items(): - if field in spec: - out_spec[field] = spec[field] - else: - out_spec[field] = default_value - - output["spec"] = out_spec - - # Summary - section("Summary") - print(f" Converted {len(out_rules)} rules, {len(out_domains)} domains.", file=sys.stderr) - print(f" Review all [!] warnings above before deploying.\n", file=sys.stderr) - - return output - - -def main(): - parser = argparse.ArgumentParser( - description="Convert nginx_k8s_native instance JSON to nginx_gateway_fabric_legacy_aws/1.0" - ) - parser.add_argument("input", help="Path to input JSON file") - parser.add_argument("-o", "--output", help="Path to output JSON file (default: stdout)") - parser.add_argument( - "--default-namespace", - default="default", - help="Default namespace for rules without a derivable namespace (default: 'default')", - ) - args = parser.parse_args() - - with open(args.input, "r") as f: - input_data = json.load(f) - - result = convert(input_data, args.default_namespace) - - output_json = json.dumps(result, indent=2) + "\n" - - if args.output: - with open(args.output, "w") as f: - f.write(output_json) - print(f"Output written to {args.output}", file=sys.stderr) - else: - print(output_json) - - -if __name__ == "__main__": - main() From f8eb0a33311f292118f651052465d80dc0345054 Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Tue, 17 Mar 2026 23:54:42 +0530 Subject: [PATCH 08/13] Restrict ACK ACM controller IAM policy to read-only access Remove permissions that allow creating, importing, deleting, renewing, or modifying ACM certificates. The controller now only needs to adopt and sync existing certificates via AdoptedResource. Co-Authored-By: Claude Opus 4.6 (1M context) --- modules/ack_acm_controller/legacy/1.0/main.tf | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/modules/ack_acm_controller/legacy/1.0/main.tf b/modules/ack_acm_controller/legacy/1.0/main.tf index 27b08beb..d4496fd4 100644 --- a/modules/ack_acm_controller/legacy/1.0/main.tf +++ b/modules/ack_acm_controller/legacy/1.0/main.tf @@ -46,18 +46,12 @@ resource "aws_iam_policy" "ack_acm" { { Effect = "Allow" Action = [ - "acm:RequestCertificate", "acm:DescribeCertificate", - "acm:ListCertificates", - "acm:DeleteCertificate", - "acm:ExportCertificate", "acm:GetCertificate", + "acm:ListCertificates", "acm:ListTagsForCertificate", "acm:AddTagsToCertificate", - "acm:RemoveTagsFromCertificate", - "acm:UpdateCertificateOptions", - "acm:ImportCertificate", - "acm:RenewCertificate" + "acm:RemoveTagsFromCertificate" ] Resource = "*" } From 0fb5c7086055cb650b4f1706c81e68e99eed4032 Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Wed, 18 Mar 2026 00:53:33 +0530 Subject: [PATCH 09/13] Add acm:ExportCertificate to IAM policy for adopted cert export Required for exportTo to work with adopted certificates. This permission only reads existing cert data (cert body + private key), does not create new certificates. Co-Authored-By: Claude Opus 4.6 (1M context) --- modules/ack_acm_controller/legacy/1.0/main.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/ack_acm_controller/legacy/1.0/main.tf b/modules/ack_acm_controller/legacy/1.0/main.tf index d4496fd4..222976e6 100644 --- a/modules/ack_acm_controller/legacy/1.0/main.tf +++ b/modules/ack_acm_controller/legacy/1.0/main.tf @@ -47,6 +47,7 @@ resource "aws_iam_policy" "ack_acm" { Effect = "Allow" Action = [ "acm:DescribeCertificate", + "acm:ExportCertificate", "acm:GetCertificate", "acm:ListCertificates", "acm:ListTagsForCertificate", From 0ec41b6a92f0c736f895f92290da2ee2929d29a5 Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Wed, 18 Mar 2026 12:44:16 +0530 Subject: [PATCH 10/13] feat: dual-mode TLS for NGINX Gateway Fabric AWS module Adds ACM mode detection: when domains have ACM ARNs as certificate_reference and no ACK controller is available, TLS terminates at the NLB instead of the Gateway pod. - Detects ACM mode via certificate_reference + ACK controller presence - Adds NLB ssl-cert/ssl-ports annotations in ACM mode - Passes var.instance as-is in ACM mode (no domain rewriting) - Gates ACK Certificate CRD resources on ACK controller availability - Passes external_tls_termination=true to base utility module Co-Authored-By: Claude Opus 4.6 (1M context) --- .../1.0/main.tf | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf index acd9ca8f..12ca6e9b 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf @@ -19,17 +19,32 @@ locals { if can(domain.certificate_reference) && length(regexall("arn:aws:acm:", lookup(domain, "certificate_reference", ""))) > 0 } + # Detect ACK ACM controller availability + use_ack_acm = try(var.inputs.ack_acm_controller_details, null) != null + + # ACM mode: when ACM domains exist but no ACK controller, + # TLS terminates at the NLB instead of at the Gateway pod + acm_mode = !local.use_ack_acm && length(local.acm_cert_domains) > 0 + + # ACM ARNs to attach to NLB for TLS termination + acm_cert_arns = local.acm_mode ? [ + for domain_key, domain in local.acm_cert_domains : domain.certificate_reference + ] : [] + # K8s secret name for ACM cert domains — the ACK Certificate CRD exports cert to this secret + # Only relevant when ACK controller is available acm_cert_secret_names = { for domain_key, domain in local.acm_cert_domains : domain_key => "${local.name}-${domain_key}-acm-tls" } - # Rewrite instance domains: replace ACM ARN certificate_reference with K8s secret name - # The base module only sees K8s secret names — never ACM ARNs + # Rewrite instance domains based on TLS termination mode: + # - ACK path: replace ACM ARN with K8s secret name (TLS at Gateway) + # - ACM mode: no rewriting needed (base module ignores certificate_reference with external_tls_termination) + # - Otherwise: pass through unchanged acm_modified_domains = { for domain_key, domain in lookup(var.instance.spec, "domains", {}) : - domain_key => contains(keys(local.acm_cert_secret_names), domain_key) ? merge(domain, { + domain_key => local.use_ack_acm && contains(keys(local.acm_cert_secret_names), domain_key) ? merge(domain, { certificate_reference = local.acm_cert_secret_names[domain_key] }) : domain } @@ -80,8 +95,9 @@ locals { ) # Build modified instance with rewritten domains - # When DNS-01 is active: disable_base_domain=true (we re-add it ourselves with certificate_reference) - modified_instance = merge(var.instance, { + # ACM mode: pass var.instance as-is — base module ignores certificate_reference when external_tls_termination=true + # cert-manager/ACK mode: apply domain rewrites and DNS-01 overrides + modified_instance = local.acm_mode ? var.instance : merge(var.instance, { spec = merge(var.instance.spec, { domains = local.modified_domains disable_base_domain = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? true : lookup(var.instance.spec, "disable_base_domain", false) @@ -119,7 +135,12 @@ locals { "service.beta.kubernetes.io/aws-load-balancer-nlb-target-type" = "ip" "service.beta.kubernetes.io/aws-load-balancer-backend-protocol" = "tcp" "service.beta.kubernetes.io/aws-load-balancer-target-group-attributes" = lookup(var.instance.spec, "private", false) ? "proxy_protocol_v2.enabled=true,preserve_client_ip.enabled=false" : "proxy_protocol_v2.enabled=true,preserve_client_ip.enabled=true" - } + }, + # ACM mode: attach ACM certs to NLB for TLS termination + local.acm_mode ? { + "service.beta.kubernetes.io/aws-load-balancer-ssl-cert" = join(",", local.acm_cert_arns) + "service.beta.kubernetes.io/aws-load-balancer-ssl-ports" = "443" + } : {} ) } @@ -132,7 +153,8 @@ module "nginx_gateway_fabric" { environment = var.environment inputs = local.merged_inputs - service_annotations = local.aws_annotations + service_annotations = local.aws_annotations + external_tls_termination = local.acm_mode load_balancer_class = "service.k8s.aws/nlb" @@ -149,10 +171,10 @@ module "nginx_gateway_fabric" { # ACK ACM Certificate CRD resources — creates ACM certificates via ACK controller # and exports them to K8s TLS secrets for Gateway listener consumption. -# Only created for domains whose certificate_reference is an ACM ARN. -# Requires the ack_acm_controller to be deployed (optional input). +# Only created when ACK controller is available and domains have ACM ARNs. +# When ACK is not available, ACM certs are attached directly to the NLB instead. module "ack_acm_certificate" { - for_each = local.acm_cert_domains + for_each = local.use_ack_acm ? local.acm_cert_domains : {} source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resource" name = "${local.name}-acm-cert-${each.key}" @@ -187,8 +209,9 @@ module "ack_acm_certificate" { # Pre-create empty TLS secrets for ACK ACM certificate export # ACK ACM controller requires the target secret to exist before it can export +# Only created when ACK controller is available resource "kubernetes_secret_v1" "acm_cert" { - for_each = local.acm_cert_domains + for_each = local.use_ack_acm ? local.acm_cert_domains : {} metadata { name = local.acm_cert_secret_names[each.key] From b8ea7d16fd3ec14b0a2e613c7475092778cb6b3a Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Wed, 18 Mar 2026 18:37:38 +0530 Subject: [PATCH 11/13] feat: dual-mode TLS, DNS-01/ACM guard, remove branch ref, update README - Remove temporary branch ref from utility module source - Disable DNS-01 when ACM mode is active (incompatible) - Fix inconsistent conditional types in modified_instance - Update README with dual-mode TLS architecture and examples Co-Authored-By: Claude Opus 4.6 (1M context) --- .../1.0/README.md | 176 ++++++++++++++---- .../1.0/main.tf | 11 +- 2 files changed, 149 insertions(+), 38 deletions(-) diff --git a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/README.md b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/README.md index e5b529ef..ea0c80be 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/README.md +++ b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/README.md @@ -7,7 +7,8 @@ Kubernetes Gateway API implementation for AWS with NLB, Proxy Protocol v2, ACM, This module is an **AWS-specific wrapper** around the base `nginx_gateway_fabric` utility module. It adds: - **AWS NLB**: Network Load Balancer with Proxy Protocol v2 and IP target type -- **ACM Integration**: Use AWS Certificate Manager ARNs as `certificate_reference` — the module creates ACK Certificate CRDs and manages TLS secret lifecycle +- **Dual-Mode TLS**: Automatically selects TLS termination point based on configuration +- **ACM Integration**: Use AWS Certificate Manager ARNs as `certificate_reference` - **DNS-01 Wildcard Certificates**: Optional DNS-01 validation via a pre-existing ClusterIssuer (e.g., `gts-production`) for wildcard listeners This is the **legacy** flavor that uses `cc_metadata` and legacy input conventions. @@ -20,14 +21,16 @@ This is the **legacy** flavor that uses `cc_metadata` and legacy input conventio ┌──────────────────────────────────────────────┐ │ AWS Wrapper (this module) │ │ │ - │ 1. ACM ARN → K8s secret rewrite │ - │ 2. DNS-01 → certificate_reference rewrite │ - │ 3. AWS NLB annotations │ + │ 1. Dual-mode TLS detection │ + │ 2. ACM ARN → K8s secret (ACK path) │ + │ or ACM ARN → NLB ssl-cert (ACM mode) │ + │ 3. DNS-01 → certificate_reference rewrite │ + │ 4. AWS NLB annotations │ │ │ │ ┌──────────────────────────────────┐ │ │ │ Base Utility Module │ │ - Internet ────────► │ │ - Gateway + Listeners │ │ - (NLB) │ │ - HTTPRoute / GRPCRoute │ │ + │ │ - Gateway + Listeners │ │ + │ │ - HTTPRoute / GRPCRoute │ │ │ │ - Helm chart deployment │ │ │ │ - HTTP-01 certs (if needed) │ │ │ └──────────────────────────────────┘ │ @@ -36,16 +39,76 @@ This is the **legacy** flavor that uses `cc_metadata` and legacy input conventio --- -## TLS Certificate Flows +## Dual-Mode TLS + +The module automatically selects between two TLS termination modes based on the presence of ACM ARNs and the ACK ACM controller input. + +### Mode Detection + +``` +For each domain in spec.domains: + if certificate_reference matches "arn:aws:acm:" → ACM domain + +If ACK controller input provided → ACK path (Gateway terminates TLS) +If ACM domains exist + no ACK controller → ACM mode (NLB terminates TLS) +If no ACM domains → cert-manager mode (Gateway terminates TLS) +``` + +### Path 1: cert-manager Mode (No ACM ARNs) + +``` +Client (TLS) → NLB:443 (TCP passthrough) + → [PP2 header][TLS encrypted data] + → Gateway:443 (HTTPS listener + ProxyProtocol) + → Gateway terminates TLS with cert-manager K8s secret + → HTTPRoute matching, proxies to upstream +``` + +NLB annotations: no `ssl-cert`, no `ssl-ports`. Gateway has per-domain HTTPS listeners with TLS termination. + +### Path 2: ACK Path (ACM ARNs + ACK Controller) + +Same as cert-manager mode, but the ACK ACM controller creates ACM certificates and exports them to K8s TLS secrets. The Gateway terminates TLS using those secrets. + +``` +Client (TLS) → NLB:443 (TCP passthrough) + → [PP2 header][TLS encrypted data] + → Gateway:443 (HTTPS listener + ProxyProtocol) + → Gateway terminates TLS with ACK-exported K8s secret + → HTTPRoute matching, proxies to upstream +``` + +### Path 3: ACM Mode (ACM ARNs + No ACK Controller) -Three mutually exclusive certificate strategies per domain: +NLB terminates TLS using free, non-exportable ACM public certificates. Gateway receives plaintext HTTP on port 443. + +``` +Client (TLS) → NLB:443 (TLS listener, ACM terminates) + → [PP2 header][plain HTTP] + → Gateway:443 (HTTP listener + ProxyProtocol) + → Gateway reads PP2, matches HTTPRoute by Host header + → Proxies to upstream +``` + +NLB annotations include `ssl-cert` (ACM ARNs) and `ssl-ports: 443`. Gateway has a single HTTP listener on port 443 with no hostname restriction — routing is handled entirely by HTTPRoute `hostnames` fields. + +**Key differences in ACM mode:** +- No TLS certificates created or managed (no bootstrap, no cert-manager, no ACK CRDs) +- DNS-01 is automatically disabled (incompatible with NLB TLS termination) +- Single Gateway listener instead of per-domain listeners +- All HTTPRoutes reference the single `"https"` listener + +--- + +## TLS Certificate Flows | Domain has | Flow | Listener | Managed by | |---|---|---|---| -| ACM ARN in `certificate_reference` | ACK ACM Certificate CRD | Wildcard (`*.domain`) | AWS wrapper | -| No cert ref + `use_dns01: true` | cert-manager DNS-01 via ClusterIssuer | Wildcard (`*.domain`) | AWS wrapper | -| No cert ref + `use_dns01: false` | cert-manager HTTP-01 (default) | Exact hostname | Utility module | -| K8s secret in `certificate_reference` | User-managed | Wildcard (`*.domain`) | User | +| ACM ARN + ACK controller | ACK ACM Certificate CRD | HTTPS, wildcard (`*.domain`) | AWS wrapper | +| ACM ARN + no ACK controller | NLB TLS termination | HTTP on 443 (single listener) | NLB/ACM | +| No cert ref + `use_dns01: true` | cert-manager DNS-01 via ClusterIssuer | HTTPS, wildcard (`*.domain`) | AWS wrapper | +| No cert ref + `use_dns01: false` | cert-manager HTTP-01 (default) | HTTPS, exact hostname | Utility module | +| K8s secret in `certificate_reference` | User-managed | HTTPS, wildcard (`*.domain`) | User | ### HTTP-01 (Default) @@ -60,6 +123,8 @@ When `use_dns01: true`, the module: 3. Sets `certificate_reference` on all domains, causing the utility module to create wildcard listeners (`*.domain`) 4. Disables the utility module's HTTP-01 ClusterIssuer and bootstrap cert creation (no domains left without `certificate_reference`) +**Note**: DNS-01 is automatically disabled when ACM mode is active (incompatible with NLB TLS termination). + **Prerequisite**: The ClusterIssuer (default: `gts-production`) must already exist in the cluster with a DNS-01 solver configured. ```json @@ -77,14 +142,9 @@ When `use_dns01: true`, the module: } ``` -This creates: -- Wildcard listener: `*.api.example.com` -- Certificate: `*.api.example.com` + `api.example.com` via `gts-production` -- Base domain also gets a wildcard listener and DNS-01 cert - -### ACM Certificates +### ACM Certificates (with ACK Controller) -Use an ACM ARN as `certificate_reference`. The module creates an ACK Certificate CRD that provisions the cert and exports it to a K8s TLS secret. +Use an ACM ARN as `certificate_reference` with the ACK ACM controller deployed. The module creates an ACK Certificate CRD that provisions the cert and exports it to a K8s TLS secret. ```json { @@ -102,29 +162,28 @@ Use an ACM ARN as `certificate_reference`. The module creates an ACK Certificate **Prerequisite**: The `ack_acm_controller` must be deployed (optional input). -### Mixed Mode +### ACM Certificates (NLB Termination — No ACK Controller) -ACM and DNS-01 can coexist. ACM domains keep their ACM flow; domains without `certificate_reference` use DNS-01 when `use_dns01: true`. +Use an ACM ARN as `certificate_reference` without the ACK controller. The NLB terminates TLS using the free ACM certificate directly. ```json { "spec": { - "use_dns01": true, "domains": { - "acm_domain": { - "domain": "acm.example.com", - "alias": "acm", + "production": { + "domain": "api.example.com", + "alias": "prod", "certificate_reference": "arn:aws:acm:us-east-1:123456789:certificate/abc-123" - }, - "dns01_domain": { - "domain": "dns.example.com", - "alias": "dns" } } } } ``` +**No ACK controller needed.** The ACM cert is attached directly to the NLB via `aws-load-balancer-ssl-cert` annotation. The Gateway receives plaintext HTTP on port 443. + +**Limitation**: When any domain uses ACM without ACK, ALL traffic goes through NLB TLS termination. No mixing of ACM and cert-manager on the same instance. + --- ## Configuration @@ -182,6 +241,37 @@ ACM and DNS-01 can coexist. ACM domains keep their ACM flow; domains without `ce } ``` +### ACM (NLB Termination) Example + +```json +{ + "kind": "ingress", + "flavor": "nginx_gateway_fabric_legacy_aws", + "version": "1.0", + "spec": { + "private": false, + "force_ssl_redirection": true, + "domains": { + "production": { + "domain": "api.example.com", + "alias": "prod", + "certificate_reference": "arn:aws:acm:us-east-1:123456789:certificate/abc-123" + } + }, + "rules": { + "api": { + "service_name": "api-service", + "namespace": "default", + "port": "8080", + "path": "/api" + } + } + } +} +``` + +No `ack_acm_controller_details` input needed. The NLB terminates TLS with the ACM certificate. + --- ## Spec Options @@ -192,7 +282,7 @@ ACM and DNS-01 can coexist. ACM domains keep their ACM flow; domains without `ce | `force_ssl_redirection` | boolean | `true` | Redirect HTTP to HTTPS | | `disable_base_domain` | boolean | `false` | Disable auto-generated base domain | | `domain_prefix_override` | string | - | Override auto-generated domain prefix | -| `use_dns01` | boolean | `false` | Enable DNS-01 wildcard certificates for all domains | +| `use_dns01` | boolean | `false` | Enable DNS-01 wildcard certificates (disabled automatically in ACM mode) | | `dns01_cluster_issuer` | string | `gts-production` | ClusterIssuer name for DNS-01 validation | | `disable_endpoint_validation` | boolean | `false` | Disable HTTP endpoint validation | | `basic_auth` | boolean | `false` | Enable basic authentication | @@ -209,7 +299,7 @@ ACM and DNS-01 can coexist. ACM domains keep their ACM flow; domains without `ce | `kubernetes_details` | `@outputs/kubernetes` | Yes | Kubernetes cluster connection | | `gateway_api_crd_details` | `@outputs/gateway_api_crd` | Yes | Gateway API CRD installation | | `prometheus_details` | `@outputs/prometheus` | No | Prometheus for PodMonitor | -| `ack_acm_controller_details` | `@outputs/ack_acm_controller` | No | ACK ACM controller (required for ACM ARN domains) | +| `ack_acm_controller_details` | `@outputs/ack_acm_controller` | No | ACK ACM controller — when provided, ACM certs are exported to K8s secrets via ACK CRDs (Gateway terminates TLS). When absent and ACM ARNs are used, NLB terminates TLS directly. | --- @@ -222,9 +312,20 @@ ACM and DNS-01 can coexist. ACM domains keep their ACM flow; domains without `ce - Target type: IP (for direct pod routing) - Load balancer class: `service.k8s.aws/nlb` +### NLB Annotations by Mode + +| Annotation | cert-manager / ACK | ACM mode | +|---|---|---| +| `aws-load-balancer-backend-protocol` | `tcp` | `tcp` | +| `aws-load-balancer-type` | `external` | `external` | +| `aws-load-balancer-nlb-target-type` | `ip` | `ip` | +| `aws-load-balancer-target-group-attributes` | `proxy_protocol_v2.enabled=true,...` | `proxy_protocol_v2.enabled=true,...` | +| `aws-load-balancer-ssl-cert` | _(not set)_ | `` | +| `aws-load-balancer-ssl-ports` | _(not set)_ | `443` | + ### Proxy Protocol v2 -Always enabled. The module configures `NginxProxy` CRD with `rewriteClientIP` in ProxyProtocol mode to correctly extract client IPs from NLB. +Always enabled in all modes. The module configures `NginxProxy` CRD with `rewriteClientIP` in ProxyProtocol mode to correctly extract client IPs from NLB. --- @@ -238,7 +339,7 @@ kubectl describe certificate -dns01-cert- -n kubectl get clusterissuer gts-production -o yaml ``` -### Check ACM Certificate Status +### Check ACM Certificate Status (ACK path) ```bash kubectl get certificate.acm.services.k8s.aws -n @@ -248,9 +349,18 @@ kubectl describe certificate.acm.services.k8s.aws -acm-cert- - ### Check Gateway Listeners ```bash +# cert-manager/ACK mode: expect per-domain HTTPS listeners +# ACM mode: expect single HTTP listener named "https" on port 443 kubectl get gateway -n -o yaml | grep -A 20 listeners ``` +### Verify NLB TLS Mode + +```bash +# Check if ssl-cert annotation is present (ACM mode) or absent (cert-manager mode) +kubectl get svc -n -l app.kubernetes.io/name=nginx-gateway-fabric -o yaml | grep ssl-cert +``` + ### NLB Issues ```bash diff --git a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf index 12ca6e9b..5e68c88c 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf @@ -3,7 +3,8 @@ locals { name = lower(var.environment.namespace == "default" ? var.instance_name : "${var.environment.namespace}-${var.instance_name}") # --- DNS-01 configuration --- - use_dns01 = lookup(var.instance.spec, "use_dns01", false) + # DNS-01 is incompatible with ACM mode (NLB terminates TLS, no cert-manager involvement) + use_dns01 = !local.acm_mode && lookup(var.instance.spec, "use_dns01", false) dns01_cluster_issuer = lookup(var.instance.spec, "dns01_cluster_issuer", "gts-production") # Compute base domain (mirrors utility module logic — needed to take over base domain for DNS-01) @@ -95,12 +96,12 @@ locals { ) # Build modified instance with rewritten domains - # ACM mode: pass var.instance as-is — base module ignores certificate_reference when external_tls_termination=true + # ACM mode: pass original domains/spec — base module ignores certificate_reference when external_tls_termination=true # cert-manager/ACK mode: apply domain rewrites and DNS-01 overrides - modified_instance = local.acm_mode ? var.instance : merge(var.instance, { + modified_instance = merge(var.instance, { spec = merge(var.instance.spec, { - domains = local.modified_domains - disable_base_domain = local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? true : lookup(var.instance.spec, "disable_base_domain", false) + domains = local.acm_mode ? lookup(var.instance.spec, "domains", {}) : local.modified_domains + disable_base_domain = local.acm_mode ? lookup(var.instance.spec, "disable_base_domain", false) : (local.use_dns01 && !lookup(var.instance.spec, "disable_base_domain", false) ? true : lookup(var.instance.spec, "disable_base_domain", false)) }) }) From 1775c73858afe5a1844a9d5dc7ba7ce4e9c9ce14 Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Thu, 19 Mar 2026 12:14:07 +0530 Subject: [PATCH 12/13] fix: deduplicate ACM ARNs in NLB ssl-cert annotation Use distinct() to avoid duplicate ARNs when multiple domains share the same ACM wildcard certificate. Co-Authored-By: Claude Opus 4.6 (1M context) --- modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf index 5e68c88c..43a3ac79 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf @@ -28,9 +28,9 @@ locals { acm_mode = !local.use_ack_acm && length(local.acm_cert_domains) > 0 # ACM ARNs to attach to NLB for TLS termination - acm_cert_arns = local.acm_mode ? [ + acm_cert_arns = local.acm_mode ? distinct([ for domain_key, domain in local.acm_cert_domains : domain.certificate_reference - ] : [] + ]) : [] # K8s secret name for ACM cert domains — the ACK Certificate CRD exports cert to this secret # Only relevant when ACK controller is available From a537c8b4ae73b034a583c7e0d35ea949d6cd831b Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Thu, 19 Mar 2026 12:21:07 +0530 Subject: [PATCH 13/13] remove export permission --- modules/ack_acm_controller/legacy/1.0/main.tf | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/ack_acm_controller/legacy/1.0/main.tf b/modules/ack_acm_controller/legacy/1.0/main.tf index 222976e6..d4496fd4 100644 --- a/modules/ack_acm_controller/legacy/1.0/main.tf +++ b/modules/ack_acm_controller/legacy/1.0/main.tf @@ -47,7 +47,6 @@ resource "aws_iam_policy" "ack_acm" { Effect = "Allow" Action = [ "acm:DescribeCertificate", - "acm:ExportCertificate", "acm:GetCertificate", "acm:ListCertificates", "acm:ListTagsForCertificate",