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..d4496fd4 --- /dev/null +++ b/modules/ack_acm_controller/legacy/1.0/main.tf @@ -0,0 +1,118 @@ +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:DescribeCertificate", + "acm:GetCertificate", + "acm:ListCertificates", + "acm:ListTagsForCertificate", + "acm:AddTagsToCertificate", + "acm:RemoveTagsFromCertificate" + ] + 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/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 5523f5a4..00000000 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md +++ /dev/null @@ -1,663 +0,0 @@ -# NGINX Gateway Fabric - -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. - -### 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 -- **Multi-Domain Support**: Routes work across all configured domains -- **TLS Management**: Automatic SSL certificates via cert-manager (HTTP-01 and DNS-01) -- **Multi-Cloud**: AWS (NLB), Azure (LB), GCP (GCLB) -- **gRPC Support**: Native GRPCRoute resources -- **CORS**: Cross-origin resource sharing configuration -- **Observability**: Prometheus metrics via ServiceMonitor - ---- - -## 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) | - -### Path Type Options - -| Type | Default | Description | -|------|---------|-------------| -| `PathPrefix` | Yes | Matches paths starting with the specified prefix | -| `Exact` | No | Matches the exact path only | - ---- - -## Routing Options - -### Header-Based Routing - -Route traffic based on HTTP headers: - -```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" - }, - "client_header": { - "name": "X-Client-Type", - "value": "mobile.*", - "type": "RegularExpression" - } - } - } - } -} -``` - -### Query Parameter Matching - -Route traffic based on query parameters: - -```json -{ - "rules": { - "api_beta": { - "service_name": "api-beta", - "namespace": "default", - "port": "8080", - "path": "/api", - "path_type": "PathPrefix", - "query_param_matches": { - "version_param": { - "name": "version", - "value": "beta", - "type": "Exact" - } - } - } - } -} -``` - -### HTTP Method Matching - -Route traffic based on HTTP method: - -```json -{ - "rules": { - "api_readonly": { - "service_name": "api-readonly", - "namespace": "default", - "port": "8080", - "path": "/api", - "path_type": "PathPrefix", - "method": "GET" - } - } -} -``` - -Options: `ALL` (default), `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` - ---- - -## URL Rewriting - -Rewrite request URLs before forwarding to backend: - -```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" - } - } - } - } -} -``` - -For full path replacement: - -```json -{ - "url_rewrite": { - "rewrite_rule": { - "path_type": "ReplaceFullPath", - "replace_path": "/v2/api" - } - } -} -``` - ---- - -## Header Modification - -### 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" - } - } - } - } - } -} -``` - -### Response Headers - -Modify headers sent to client: - -```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" - } - } - } -} -``` - -> **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" - } - } - } -} -``` - ---- - -## 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 - } - } - } -} -``` - ---- - -## 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 -{ - "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" - } - } - } - } - } -} -``` - ---- - -## Canary Deployments - -Split traffic between service versions: - -```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 - } - } - } -} -``` - -This sends 20% of traffic to `api-v2` and 80% to `api-v1`. - ---- - -## Request Mirroring - -Mirror traffic to a secondary service for testing: - -```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" - } - } - } -} -``` - ---- - -## Multi-Domain Configuration - -### Custom Domains - -Configure custom domains inside spec: - -```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", - "alias": "prod" - }, - "staging": { - "domain": "staging-api.example.com", - "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) | - ---- - -## TLS Certificate Management - -### HTTP-01 Validation (Default) - -Used when `disable_endpoint_validation: false` (default): - -- Creates bootstrap self-signed certificates for Gateway startup -- cert-manager replaces them with valid Let's Encrypt certificates -- Requires port 80 accessible from internet - -### DNS-01 Validation - -Used when `disable_endpoint_validation: true` or `private: true`: - -- Uses DNS challenges instead of HTTP -- Required for private/internal load balancers -- Requires cert-manager DNS provider configuration - -### Custom Certificates - -Use existing TLS certificates: - -```json -{ - "spec": { - "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" - } - } - } -} -``` - ---- - -## Custom Helm Values - -Override default Helm configuration: - -```json -{ - "spec": { - "helm_values": { - "nginxGateway": { - "replicaCount": 3 - }, - "nginx": { - "config": { - "logging": { - "errorLevel": "debug" - } - } - } - } - } -} -``` - -See available values: https://github.com/nginxinc/nginx-gateway-fabric/blob/main/charts/nginx-gateway-fabric/values.yaml - ---- - -## 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 | -| `domain_prefix_override` | string | - | Override auto-generated domain prefix | -| `renew_cert_before` | string | `720h` | Renew certificate before expiry | -| `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 | -| 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 (if not 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 | - ---- - -## Troubleshooting - -### Check Gateway Status - -```bash -kubectl get gateway -n -kubectl describe gateway -n -``` - -### Check HTTPRoute Status - -```bash -kubectl get httproute -n -kubectl describe httproute -n -``` - -### Controller Logs - -```bash -kubectl logs -n -l app.kubernetes.io/name=nginx-gateway-fabric -c nginx-gateway -``` - -### Certificate Issues - -```bash -kubectl get certificate -n -kubectl describe certificate -n -``` - ---- - -## 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 dbdf7d2c..00000000 Binary files a/modules/ingress/nginx_gateway_fabric_legacy/1.0/charts/nginx-gateway-fabric-2.3.0.tgz and /dev/null differ diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/charts/nginx-gateway-fabric-2.4.1.tgz b/modules/ingress/nginx_gateway_fabric_legacy/1.0/charts/nginx-gateway-fabric-2.4.1.tgz deleted file mode 100644 index 4c127304..00000000 Binary files a/modules/ingress/nginx_gateway_fabric_legacy/1.0/charts/nginx-gateway-fabric-2.4.1.tgz and /dev/null differ 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 e7931088..00000000 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf +++ /dev/null @@ -1,1243 +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) - ] - - # Map of additional hostnames to their config for listeners and certs - 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" - } - } - - # 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 = lookup(var.instance.spec, "private", false) ? { - "service.beta.kubernetes.io/azure-load-balancer-internal" = "true" - } : {} - - 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" - } : {} - - 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 : {} - ) - - # 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 - # If route has domain_prefix, reference the additional hostname listeners - # If route has no domain_prefix, reference the base domain listeners - # 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 - use additional hostname listeners - 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")}" - } - ], - # 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 - # If route has domain_prefix, reference the additional hostname listeners - # If route has no domain_prefix, reference the base domain listeners - # 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 - use additional hostname listeners - 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")}" - } - ], - # 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 k, v in local.rulesFiltered : v.namespace => v.namespace - if v.namespace != var.environment.namespace - } - - # 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" "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 created for HTTP-01 validation (when disable_endpoint_validation is false) -resource "tls_private_key" "bootstrap_additional" { - for_each = local.disable_endpoint_validation ? {} : 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 - 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" "bootstrap_tls_additional" { - for_each = local.disable_endpoint_validation ? {} : 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.disable_endpoint_validation ? 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.disable_endpoint_validation ? 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 - [for domain_key, domain in local.domains : { - name = "https-${domain_key}" - protocol = "HTTPS" - port = 443 - hostname = domain.domain - tls = { - mode = "Terminate" - certificateRefs = [{ - kind = "Secret" - # If certificate_reference is provided, use it (custom cert) - # 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 from rules (domain_prefix + domain) - [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 = local.disable_endpoint_validation ? local.dns_validation_secret_name : config.secret_name - }] - } - allowedRoutes = { - namespaces = { - from = "All" - } - } - }] - ) - } - }] - }), - yamlencode(local.base_helm_values) - ] - - depends_on = [ - kubernetes_secret.bootstrap_tls, - kubernetes_secret.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 - 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.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" "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 a613527e..00000000 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/outputs.tf +++ /dev/null @@ -1,100 +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 - } - ) - - 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"] : [] - } - } -} - -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 = local.dns_validation_secret_name - description = "TLS certificate secret name" -} - -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)" -} 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..ea0c80be --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/README.md @@ -0,0 +1,369 @@ +# 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 +- **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. + +--- + +## Architecture + +``` + ┌──────────────────────────────────────────────┐ + │ AWS Wrapper (this module) │ + │ │ + │ 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 │ │ + │ │ - Gateway + Listeners │ │ + │ │ - HTTPRoute / GRPCRoute │ │ + │ │ - Helm chart deployment │ │ + │ │ - HTTP-01 certs (if needed) │ │ + │ └──────────────────────────────────┘ │ + └──────────────────────────────────────────────┘ +``` + +--- + +## 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) + +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 + 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) + +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`) + +**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 +{ + "spec": { + "use_dns01": true, + "dns01_cluster_issuer": "gts-production", + "domains": { + "production": { + "domain": "api.example.com", + "alias": "prod" + } + } + } +} +``` + +### ACM Certificates (with ACK Controller) + +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 +{ + "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). + +### ACM Certificates (NLB Termination — No ACK Controller) + +Use an ACM ARN as `certificate_reference` without the ACK controller. The NLB terminates TLS using the free ACM certificate directly. + +```json +{ + "spec": { + "domains": { + "production": { + "domain": "api.example.com", + "alias": "prod", + "certificate_reference": "arn:aws:acm:us-east-1:123456789:certificate/abc-123" + } + } + } +} +``` + +**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 + +### 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" + } + } + } +} +``` + +### 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 + +| 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 (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 | +| `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 — 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. | + +--- + +## 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` + +### 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 in all modes. 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 (ACK path) + +```bash +kubectl get certificate.acm.services.k8s.aws -n +kubectl describe certificate.acm.services.k8s.aws -acm-cert- -n +``` + +### 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 +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..43a3ac79 --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_aws/1.0/main.tf @@ -0,0 +1,351 @@ +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 --- + # 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) + 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 + } + + # 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 ? 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 + acm_cert_secret_names = { + for domain_key, domain in local.acm_cert_domains : + domain_key => "${local.name}-${domain_key}-acm-tls" + } + + # 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 => 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 + } + + # --- 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 + # 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 = merge(var.instance, { + spec = merge(var.instance.spec, { + 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)) + }) + }) + + + # 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) ? { + "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" + }, + # 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" + } : {} + ) +} + +# 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.aws_annotations + external_tls_termination = local.acm_mode + + 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 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.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}" + 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 +# Only created when ACK controller is available +resource "kubernetes_secret_v1" "acm_cert" { + for_each = local.use_ack_acm ? 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] + } +} + +# --- 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_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/1.0/variables.tf b/modules/ingress/nginx_gateway_fabric_legacy_aws/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_aws/1.0/variables.tf 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_azure/1.0/variables.tf b/modules/ingress/nginx_gateway_fabric_legacy_azure/1.0/variables.tf new file mode 100644 index 00000000..0d889c8c --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy_azure/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 = {} +} 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 = {} +} 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: []