Skip to content

feat(foundry): add sentinel to on-prem k3s cluster via Cloudflare Tunnel#119

Merged
BK1031 merged 3 commits into
mainfrom
bk1031/foundry-migration
Jul 6, 2026
Merged

feat(foundry): add sentinel to on-prem k3s cluster via Cloudflare Tunnel#119
BK1031 merged 3 commits into
mainfrom
bk1031/foundry-migration

Conversation

@BK1031

@BK1031 BK1031 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

First slice of the EKS → foundry k3s migration. Ports sentinel (only) to the on-prem cluster via a Cloudflare Tunnel; mapache, vault, argocd stay on EKS until this bakes. Structured so a second on-prem cluster can drop in with the same pattern later.

Architecture

  • Cloudflare Tunnel (gr-foundry) with a single catch-all ingress rule → foundry's Traefik service. No hostname list in the tunnel config.
  • external-dns on foundry (txtOwnerId: gr-foundry) writes Cloudflare records for each foundry Ingress it sees. --default-targets forces every record to CNAME the tunnel.
  • cloudflared Deployment (2 replicas) as the tunnel connector.
  • Sentinel manifest set copied from manifests/sentinel/ with only two file changes: Ingress switches to Traefik + adds cloudflare-proxied annotation; postgres ExternalName points at the public gr-postgres.gauchoracing.com hostname.

Adding another hostname (mapache/vault/argocd next slices) is one file — the Ingress in that service's manifest set. No terraform edit, no CF dashboard click.

Adding another cluster is the same shape: its own tunnel + its own external-dns with a distinct txtOwnerId.

What was intentionally skipped

  • cert-manager — TLS terminates at CF edge, tunnel to origin is plaintext HTTP.
  • metrics-server — k3s bundles it.
  • mapache, vault, argocd on foundry — follow-up PRs.

Cutover runbook

  1. terraform apply — creates the tunnel. No DNS records created here, so no 409 with EKS external-dns.
  2. Bootstrap secrets + config on foundry:
    kubectl -n cloudflared create secret generic cloudflared-secrets \
      --from-literal=TUNNEL_TOKEN="$(terraform output -raw foundry_tunnel_token)"
    kubectl -n external-dns create secret generic cloudflare-api-token \
      --from-literal=api-token="$CLOUDFLARE_API_TOKEN"
    kubectl -n external-dns create configmap foundry-tunnel-target \
      --from-literal=target="$(terraform output -raw foundry_tunnel_id).cfargotunnel.com"
    
  3. kubectl apply -f kubernetes/bootstrap/root-foundry.yaml on the foundry ArgoCD.
  4. Populate sentinel-secrets (command in apps-foundry/sentinel.yaml).
  5. Wait for pods Healthy against gr-postgres.
  6. Delete the existing sentinel-v5.gauchoracing.com record via CF dashboard (or scale EKS external-dns to 0). Foundry external-dns writes the new CNAME within its --interval (1m default). Traffic flips within CF TTL.

Test plan

  • terraform plan shows only the tunnel + config as adds, no DNS records, no drift on other resources.
  • After apply: terraform output -raw foundry_tunnel_id returns a UUID.
  • Manually apply root-foundry.yaml on foundry; ArgoCD reports Applications Missing/OutOfSync until secrets are populated, then Healthy.
  • cloudflared pods Ready; logs show "Registered tunnel connection" against 4 CF edge locations.
  • external-dns pod logs show it discovered the sentinel Ingress and would write a CNAME (won't succeed until the old record is gone).
  • From on-prem sentinel namespace: kubectl exec deploy/core -- nc -vz gr-postgres.gauchoracing.com 5432 succeeds.
  • kubectl port-forward -n sentinel svc/kerbecs 10310:10310 + curl -H 'Host: sentinel-v5.gauchoracing.com' http://localhost:10310/api/core/keys returns JWKS before flipping DNS.
  • After DNS flip: browser test of https://sentinel-v5.gauchoracing.com login flow (Discord + Google), token issuance, iss claim.

Parallel apps-foundry/ + manifests-foundry/ tree for cutover to the
foundry k3s cluster. Existing EKS tree (apps/, manifests/) stays put
until the on-prem stack is verified in production.

- Terraform: cloudflare_zero_trust_tunnel_cloudflared (gr-foundry) +
  remote-managed ingress rules + 4 proxied CNAMEs for sentinel-v5,
  mapache, vault, argocd. random provider added.
- cloudflared Deployment (2 replicas) as the tunnel connector; token
  bootstrapped manually from terraform output.
- Sentinel/mapache/vault manifests copied verbatim from the EKS tree,
  diverging only in ingress.yaml (Traefik, no ALB annotations, no TLS
  block) and postgres.yaml (public gr-postgres.gauchoracing.com
  hostname instead of the internal EC2 hostname).
- argocd-server-public Ingress adds argocd.gauchoracing.com to the
  foundry ArgoCD alongside foundry-infra's internal Ingress.
- Cutover runbook embedded as comments in foundry.tf + apps-foundry/*.
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Terraform plan: prod

step result
fmt success
init success
validate success
plan success
plan output
module.postgres.random_password.postgres: Refreshing state... [id=none]
module.clickhouse.random_password.admin: Refreshing state... [id=none]
module.mqtt.random_password.mqtt_tcm26: Refreshing state... [id=none]
module.mqtt.random_password.mqtt_mapache: Refreshing state... [id=none]
module.origin_cert.tls_private_key.this: Refreshing state... [id=4c5b5e0a4a4f13723ba2aebe888a7cb50529fcc0]
data.cloudflare_zone.gauchoracing: Reading...
data.cloudflare_accounts.current: Reading...
module.mqtt.random_password.mqtt: Refreshing state... [id=none]
module.origin_cert.tls_cert_request.this: Refreshing state... [id=9398eb1d26e54eb59b18d476ced10810e80172e5]
module.origin_cert.cloudflare_origin_ca_certificate.this: Refreshing state... [id=307819530070461722629184968406649377122389744032]
data.cloudflare_zone.gauchoracing: Read complete after 0s [id=5ac5ae9c6086e4b55c5e1b21ca963d94]
module.eks.module.eks.module.kms.data.aws_partition.current[0]: Reading...
module.eks.module.eks.data.aws_iam_policy_document.node_assume_role_policy[0]: Reading...
module.postgres.data.aws_ami.al2023_arm64: Reading...
module.clickhouse.data.aws_ami.al2023_arm64: Reading...
module.mqtt.data.aws_ami.al2023_arm64: Reading...
module.eks.module.eks.data.aws_iam_policy_document.assume_role_policy[0]: Reading...
module.clickhouse.aws_ebs_volume.data: Refreshing state... [id=vol-0e312e8d71875ec89]
module.postgres.aws_ebs_volume.data: Refreshing state... [id=vol-02b203218b57629b4]
module.eks.module.eks.data.aws_iam_policy_document.assume_role_policy[0]: Read complete after 0s [id=2830595799]
module.eks.module.eks.data.aws_iam_policy_document.node_assume_role_policy[0]: Read complete after 0s [id=3518401652]
module.eks.module.eks.module.kms.data.aws_partition.current[0]: Read complete after 0s [id=aws]
module.eks.module.eks.data.aws_partition.current[0]: Reading...
module.eks.module.eks.data.aws_partition.current[0]: Read complete after 0s [id=aws]
module.eks.module.eks.data.aws_caller_identity.current[0]: Reading...
module.eks.module.eks.module.kms.data.aws_caller_identity.current[0]: Reading...
module.eks.module.eks.aws_cloudwatch_log_group.this[0]: Refreshing state... [id=/aws/eks/gr-prod/cluster]
module.eks.module.eks.module.kms.data.aws_caller_identity.current[0]: Read complete after 0s [id=211125506628]
module.vpc.module.vpc.aws_vpc.this[0]: Refreshing state... [id=vpc-06e13a97395396a3b]
cloudflare_ruleset.ssl_overrides: Refreshing state... [id=5a5ed8237d6f48418c172979ffe5da81]
module.eks.module.eks.data.aws_caller_identity.current[0]: Read complete after 0s [id=211125506628]
module.eks.module.eks.aws_iam_role.eks_auto[0]: Refreshing state... [id=gr-prod-eks-auto-20260601094833482500000004]
module.eks.module.eks.aws_iam_role.this[0]: Refreshing state... [id=gr-prod-cluster-20260601094833481300000002]
module.eks.module.eks.data.aws_iam_session_context.current[0]: Reading...
module.origin_cert.aws_acm_certificate.this: Refreshing state... [id=arn:aws:acm:us-west-2:211125506628:certificate/d10d5205-6d4b-4798-a152-293c69174660]
module.eks.module.eks.data.aws_iam_policy_document.custom[0]: Reading...
module.eks.module.eks.data.aws_iam_policy_document.custom[0]: Read complete after 0s [id=513122117]
module.eks.module.eks.aws_iam_role_policy_attachment.eks_auto["AmazonEC2ContainerRegistryPullOnly"]: Refreshing state... [id=gr-prod-eks-auto-20260601094833482500000004/arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPullOnly]
module.eks.module.eks.aws_iam_role_policy_attachment.eks_auto["AmazonEKSWorkerNodeMinimalPolicy"]: Refreshing state... [id=gr-prod-eks-auto-20260601094833482500000004/arn:aws:iam::aws:policy/AmazonEKSWorkerNodeMinimalPolicy]
module.eks.module.eks.data.aws_iam_session_context.current[0]: Read complete after 0s [id=arn:aws:sts::211125506628:assumed-role/github-actions-terraform/GitHubActions]
module.eks.module.eks.aws_iam_policy.custom[0]: Refreshing state... [id=arn:aws:iam::211125506628:policy/gr-prod-cluster-20260601094833480900000001]
module.eks.module.eks.aws_iam_role_policy_attachment.this["AmazonEKSClusterPolicy"]: Refreshing state... [id=gr-prod-cluster-20260601094833481300000002/arn:aws:iam::aws:policy/AmazonEKSClusterPolicy]
module.eks.module.eks.aws_iam_role_policy_attachment.this["AmazonEKSComputePolicy"]: Refreshing state... [id=gr-prod-cluster-20260601094833481300000002/arn:aws:iam::aws:policy/AmazonEKSComputePolicy]
module.eks.module.eks.aws_iam_role_policy_attachment.this["AmazonEKSLoadBalancingPolicy"]: Refreshing state... [id=gr-prod-cluster-20260601094833481300000002/arn:aws:iam::aws:policy/AmazonEKSLoadBalancingPolicy]
module.eks.module.eks.aws_iam_role_policy_attachment.this["AmazonEKSNetworkingPolicy"]: Refreshing state... [id=gr-prod-cluster-20260601094833481300000002/arn:aws:iam::aws:policy/AmazonEKSNetworkingPolicy]
module.eks.module.eks.aws_iam_role_policy_attachment.this["AmazonEKSBlockStoragePolicy"]: Refreshing state... [id=gr-prod-cluster-20260601094833481300000002/arn:aws:iam::aws:policy/AmazonEKSBlockStoragePolicy]
module.eks.module.eks.aws_iam_role_policy_attachment.custom[0]: Refreshing state... [id=gr-prod-cluster-20260601094833481300000002/arn:aws:iam::211125506628:policy/gr-prod-cluster-20260601094833480900000001]
module.eks.module.eks.module.kms.data.aws_iam_policy_document.this[0]: Reading...
module.eks.module.eks.module.kms.data.aws_iam_policy_document.this[0]: Read complete after 0s [id=922405470]
module.eks.module.eks.module.kms.aws_kms_key.this[0]: Refreshing state... [id=7768801a-b38a-4c26-8bc3-7bf6fe2aac86]
module.mqtt.data.aws_ami.al2023_arm64: Read complete after 1s [id=ami-0c0d650b2def3db6e]
module.clickhouse.data.aws_ami.al2023_arm64: Read complete after 1s [id=ami-0c0d650b2def3db6e]
module.postgres.data.aws_ami.al2023_arm64: Read complete after 1s [id=ami-0c0d650b2def3db6e]
module.eks.module.eks.module.kms.aws_kms_alias.this["cluster"]: Refreshing state... [id=alias/eks/gr-prod]
module.eks.module.eks.aws_iam_policy.cluster_encryption[0]: Refreshing state... [id=arn:aws:iam::211125506628:policy/gr-prod-cluster-ClusterEncryption20260601094855072000000006]
module.eks.module.eks.aws_iam_role_policy_attachment.cluster_encryption[0]: Refreshing state... [id=gr-prod-cluster-20260601094833481300000002/arn:aws:iam::211125506628:policy/gr-prod-cluster-ClusterEncryption20260601094855072000000006]
module.vpc.module.vpc.aws_default_route_table.default[0]: Refreshing state... [id=rtb-08f817bde5f65eb92]
module.vpc.module.vpc.aws_default_security_group.this[0]: Refreshing state... [id=sg-0a592b2169fd42df8]
module.vpc.module.vpc.aws_default_network_acl.this[0]: Refreshing state... [id=acl-0fd92b5b8eb95b2f9]
module.eks.module.eks.aws_security_group.cluster[0]: Refreshing state... [id=sg-0cac44db03a686436]
module.mqtt.aws_security_group.this: Refreshing state... [id=sg-0f5a2dc492283dafe]
module.clickhouse.aws_security_group.this: Refreshing state... [id=sg-0dee964416e7aaeb5]
module.postgres.aws_security_group.this: Refreshing state... [id=sg-08a5b2e02e0540520]
module.vpc.module.vpc.aws_route_table.public[0]: Refreshing state... [id=rtb-0815845194166b58b]
module.eks.module.eks.aws_security_group.node[0]: Refreshing state... [id=sg-0b19db83dbe18cbf1]
module.vpc.module.vpc.aws_subnet.public[0]: Refreshing state... [id=subnet-0264aaaa19faa70f5]
module.vpc.module.vpc.aws_subnet.public[1]: Refreshing state... [id=subnet-0182a0562244a6fac]
module.vpc.module.vpc.aws_subnet.public[2]: Refreshing state... [id=subnet-0a5a8299f6da9bc59]
module.vpc.module.vpc.aws_route_table.private[0]: Refreshing state... [id=rtb-0c18db918f54ad033]
module.vpc.module.vpc.aws_internet_gateway.this[0]: Refreshing state... [id=igw-0ace83040de106603]
module.vpc.module.vpc.aws_subnet.private[0]: Refreshing state... [id=subnet-09fbaccd0b3aaab85]
module.vpc.module.vpc.aws_subnet.private[1]: Refreshing state... [id=subnet-022e58c410c24d794]
module.vpc.module.vpc.aws_subnet.private[2]: Refreshing state... [id=subnet-06540460bfd7d06a2]
module.mqtt.aws_security_group_rule.ingress_cidr[0]: Refreshing state... [id=sgrule-1294033340]
module.clickhouse.aws_security_group_rule.ingress_cidr["8123"]: Refreshing state... [id=sgrule-467233960]
module.clickhouse.aws_security_group_rule.ingress_cidr["9000"]: Refreshing state... [id=sgrule-2026118668]
module.vpc.module.vpc.aws_eip.nat[0]: Refreshing state... [id=eipalloc-015b9b6ae09534761]
module.vpc.module.vpc.aws_route.public_internet_gateway[0]: Refreshing state... [id=r-rtb-0815845194166b58b1080289494]
module.eks.module.eks.aws_security_group_rule.node["ingress_self_coredns_tcp"]: Refreshing state... [id=sgrule-1577514230]
module.eks.module.eks.aws_security_group_rule.node["ingress_cluster_10251_webhook"]: Refreshing state... [id=sgrule-1337801498]
module.eks.module.eks.aws_security_group_rule.node["ingress_cluster_9443_webhook"]: Refreshing state... [id=sgrule-3115694346]
module.eks.module.eks.aws_security_group_rule.node["ingress_cluster_kubelet"]: Refreshing state... [id=sgrule-2079615841]
module.eks.module.eks.aws_security_group_rule.node["ingress_self_coredns_udp"]: Refreshing state... [id=sgrule-4200159001]
module.eks.module.eks.aws_security_group_rule.node["ingress_cluster_6443_webhook"]: Refreshing state... [id=sgrule-3460871904]
module.eks.module.eks.aws_security_group_rule.node["ingress_cluster_443"]: Refreshing state... [id=sgrule-500562133]
module.eks.module.eks.aws_security_group_rule.node["ingress_cluster_4443_webhook"]: Refreshing state... [id=sgrule-118149494]
module.eks.module.eks.aws_security_group_rule.node["ingress_cluster_8443_webhook"]: Refreshing state... [id=sgrule-3709110977]
module.eks.module.eks.aws_security_group_rule.node["egress_all"]: Refreshing state... [id=sgrule-4004824215]
module.eks.module.eks.aws_security_group_rule.node["ingress_nodes_ephemeral"]: Refreshing state... [id=sgrule-504933240]
module.postgres.aws_security_group_rule.ingress_cidr[0]: Refreshing state... [id=sgrule-3721507580]
module.eks.module.eks.aws_security_group_rule.cluster["ingress_nodes_443"]: Refreshing state... [id=sgrule-535925259]
module.vpc.module.vpc.aws_route_table_association.public[0]: Refreshing state... [id=rtbassoc-09913d53f1ff40442]
module.vpc.module.vpc.aws_route_table_association.public[2]: Refreshing state... [id=rtbassoc-01d455080d37c3108]
module.vpc.module.vpc.aws_route_table_association.public[1]: Refreshing state... [id=rtbassoc-041ffe3137ec2ff7a]
module.vpc.module.vpc.aws_route_table_association.private[2]: Refreshing state... [id=rtbassoc-0b736817e30c38444]
module.vpc.module.vpc.aws_route_table_association.private[0]: Refreshing state... [id=rtbassoc-0c836864d0eccc7fc]
module.vpc.module.vpc.aws_route_table_association.private[1]: Refreshing state... [id=rtbassoc-07ea61af9805b07cb]
module.vpc.module.vpc.aws_nat_gateway.this[0]: Refreshing state... [id=nat-019992cce709b8681]
module.clickhouse.aws_security_group_rule.ingress_sg["sg-0b19db83dbe18cbf1-8123"]: Refreshing state... [id=sgrule-4177740833]
module.clickhouse.aws_security_group_rule.ingress_sg["sg-0b19db83dbe18cbf1-9000"]: Refreshing state... [id=sgrule-2466486093]
module.mqtt.aws_security_group_rule.ingress_sg["sg-0b19db83dbe18cbf1"]: Refreshing state... [id=sgrule-1062873908]
module.postgres.aws_security_group_rule.ingress_sg["sg-0b19db83dbe18cbf1"]: Refreshing state... [id=sgrule-1130224857]
module.clickhouse.aws_instance.this: Refreshing state... [id=i-0f862cc6460b5d98a]
module.mqtt.aws_instance.this: Refreshing state... [id=i-0bf98528bc8e9dab0]
module.postgres.aws_instance.this: Refreshing state... [id=i-013aab40e28a0b6b7]
module.vpc.module.vpc.aws_route.private_nat_gateway[0]: Refreshing state... [id=r-rtb-0c18db918f54ad0331080289494]
module.eks.module.eks.aws_eks_cluster.this[0]: Refreshing state... [id=gr-prod]
module.eks.module.eks.time_sleep.this[0]: Refreshing state... [id=2026-06-01T10:46:10Z]
module.eks.module.eks.data.tls_certificate.this[0]: Reading...
module.eks.module.eks.aws_eks_access_entry.this["arn-aws-iam--211125506628-user-admin-cli"]: Refreshing state... [id=gr-prod:arn:aws:iam::211125506628:user/admin-cli]
module.eks.module.eks.aws_eks_access_entry.this["arn-aws-iam--211125506628-role-github-actions-terraform"]: Refreshing state... [id=gr-prod:arn:aws:iam::211125506628:role/github-actions-terraform]
module.eks.module.eks.aws_eks_access_policy_association.this["arn-aws-iam--211125506628-user-admin-cli_admin"]: Refreshing state... [id=gr-prod#arn:aws:iam::211125506628:user/admin-cli#arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy]
module.eks.module.eks.aws_eks_access_policy_association.this["arn-aws-iam--211125506628-role-github-actions-terraform_admin"]: Refreshing state... [id=gr-prod#arn:aws:iam::211125506628:role/github-actions-terraform#arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy]
module.eks.module.eks.data.tls_certificate.this[0]: Read complete after 1s [id=f97f646c2cd14cc0db0f757f0fccc96abbbe2af5]
module.eks.module.eks.aws_iam_openid_connect_provider.oidc_provider[0]: Refreshing state... [id=arn:aws:iam::211125506628:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/21512EE80634956C7C9D0B9647C70224]
module.argocd.helm_release.argocd: Refreshing state... [id=argocd]
module.mqtt.aws_eip.this[0]: Refreshing state... [id=eipalloc-000796d397533c6d8]
cloudflare_dns_record.gr_mqtt: Refreshing state... [id=53badd7d1ab83280dc57c671ee486f90]
module.clickhouse.aws_volume_attachment.data: Refreshing state... [id=vai-843739595]
module.clickhouse.aws_eip.this[0]: Refreshing state... [id=eipalloc-0970fced638b7d8d9]
module.postgres.aws_volume_attachment.data: Refreshing state... [id=vai-3584522434]
module.postgres.aws_eip.this[0]: Refreshing state... [id=eipalloc-06d6c59b1e0a49482]
cloudflare_dns_record.gr_clickhouse: Refreshing state... [id=d6f3d3c09db5c56703a7b107d6ac3f29]
cloudflare_dns_record.gr_postgres: Refreshing state... [id=a69d68353c27a536e84d7448af48e3f0]
data.cloudflare_accounts.current: Still reading... [00m10s elapsed]
data.cloudflare_accounts.current: Still reading... [00m20s elapsed]
data.cloudflare_accounts.current: Still reading... [00m30s elapsed]
data.cloudflare_accounts.current: Still reading... [00m40s elapsed]
data.cloudflare_accounts.current: Still reading... [00m50s elapsed]
data.cloudflare_accounts.current: Still reading... [01m00s elapsed]
data.cloudflare_accounts.current: Still reading... [01m10s elapsed]
data.cloudflare_accounts.current: Still reading... [01m20s elapsed]
data.cloudflare_accounts.current: Still reading... [01m30s elapsed]
data.cloudflare_accounts.current: Still reading... [01m40s elapsed]
data.cloudflare_accounts.current: Still reading... [01m50s elapsed]
data.cloudflare_accounts.current: Still reading... [02m00s elapsed]
data.cloudflare_accounts.current: Still reading... [02m10s elapsed]
data.cloudflare_accounts.current: Still reading... [02m20s elapsed]
data.cloudflare_accounts.current: Still reading... [02m30s elapsed]
data.cloudflare_accounts.current: Still reading... [02m40s elapsed]
data.cloudflare_accounts.current: Still reading... [02m50s elapsed]
data.cloudflare_accounts.current: Still reading... [03m00s elapsed]
data.cloudflare_accounts.current: Still reading... [03m10s elapsed]
data.cloudflare_accounts.current: Still reading... [03m20s elapsed]
data.cloudflare_accounts.current: Read complete after 3m28s

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
 <= read (data resources)

Terraform will perform the following actions:

  # data.cloudflare_zero_trust_tunnel_cloudflared_token.foundry will be read during apply
  # (config refers to values not yet known)
 <= data "cloudflare_zero_trust_tunnel_cloudflared_token" "foundry" {
      + account_id = "b5a9f67e75f4f8aba5be9500f6a098a5"
      + token      = (sensitive value)
      + tunnel_id  = (known after apply)
    }

  # cloudflare_zero_trust_tunnel_cloudflared.foundry will be created
  + resource "cloudflare_zero_trust_tunnel_cloudflared" "foundry" {
      + account_id        = "b5a9f67e75f4f8aba5be9500f6a098a5"
      + account_tag       = (known after apply)
      + config_src        = "cloudflare"
      + connections       = (known after apply)
      + conns_active_at   = (known after apply)
      + conns_inactive_at = (known after apply)
      + created_at        = (known after apply)
      + deleted_at        = (known after apply)
      + id                = (known after apply)
      + metadata          = (known after apply)
      + name              = "gr-foundry"
      + remote_config     = (known after apply)
      + status            = (known after apply)
      + tun_type          = (known after apply)
      + tunnel_secret     = (sensitive value)
    }

  # cloudflare_zero_trust_tunnel_cloudflared_config.foundry will be created
  + resource "cloudflare_zero_trust_tunnel_cloudflared_config" "foundry" {
      + account_id = "b5a9f67e75f4f8aba5be9500f6a098a5"
      + config     = {
          + ingress = [
              + {
                  + service = "http://traefik.kube-system.svc.cluster.local:80"
                },
            ]
        }
      + created_at = (known after apply)
      + id         = (known after apply)
      + source     = (known after apply)
      + tunnel_id  = (known after apply)
      + version    = (known after apply)
    }

  # random_bytes.foundry_tunnel_secret will be created
  + resource "random_bytes" "foundry_tunnel_secret" {
      + base64 = (sensitive value)
      + hex    = (sensitive value)
      + length = 32
    }

Plan: 3 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + foundry_tunnel_id         = (known after apply)
  + foundry_tunnel_token      = (sensitive value)

Warning: Resource Destruction Considerations

  with cloudflare_zero_trust_tunnel_cloudflared_config.foundry,
  on foundry.tf line 81, in resource "cloudflare_zero_trust_tunnel_cloudflared_config" "foundry":
  81: resource "cloudflare_zero_trust_tunnel_cloudflared_config" "foundry" {

This resource cannot be destroyed from Terraform. If you create this
resource, it will be present in the API until manually deleted.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.
Releasing state lock. This may take a few moments...

Drop mapache, vault, vault-k8s-operator, and argocd-server-ingress from
apps-foundry + manifests-foundry. Remove them from the tunnel ingress
rules and DNS records in foundry.tf. Only sentinel-v5.gauchoracing.com
routes through the gr-foundry tunnel in this cutover; the other three
hostnames stay on EKS until sentinel bakes.

Adding a hostname later is one entry in the tunnel ingress list + one
entry in local.foundry_hostnames + copying its manifest set from
manifests/ to manifests-foundry/ with the same ingress + postgres
adjustments used here.

The initial commit's history is noisier this way (46 files added, 26
files removed) but avoids force-pushing during review. Squash-merge on
merge if a clean history matters.
@BK1031 BK1031 changed the title feat(foundry): add on-prem k3s cluster tree + Cloudflare Tunnel feat(foundry): add sentinel to on-prem k3s cluster via Cloudflare Tunnel Jul 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 699c3ef745

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread kubernetes/apps-foundry/mapache.yaml Outdated
#
# kubectl -n mapache create secret generic mapache-secrets \
# --from-literal=DATABASE_PASSWORD="$(cd ../../infra/environments/prod && terraform output -raw postgres_password)" \
# --from-literal=MQTT_PASSWORD="$(cd ../../infra/environments/prod && terraform output -raw mqtt_password)" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Populate MQTT_PASSWORD from mapache credential

When following this foundry bootstrap command, MQTT_PASSWORD gets the mqtt_password output, but the foundry mapache pods set MQTT_USER=mapache (for example in manifests-foundry/mapache/live.yaml and gr26.yaml). The NanoMQ module pairs the mapache user with mqtt_password_mapache, so these clients will fail MQTT auth after cutover unless this literal uses terraform output -raw mqtt_password_mapache or the manifests switch back to the gr26 user.

Useful? React with 👍 / 👎.

#
# kubectl --context eks -n sentinel get secret sentinel-secrets -o yaml \
# | grep -v '^\s*resourceVersion:\|^\s*uid:\|^\s*creationTimestamp:\|^\s*namespace:' \
# | kubectl --context foundry apply -f -

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the copied secret into sentinel namespace

When using the documented EKS-to-foundry copy path, the previous pipe strips the source namespace: and this kubectl apply does not set -n sentinel, so the secret will be created in the foundry context's current namespace (usually default) instead of where the Sentinel deployments read sentinel-secrets. Add -n sentinel here (or preserve/set the namespace) so the cutover does not leave Sentinel pods without their required secret.

Useful? React with 👍 / 👎.

…scaling

Replaces the explicit-hostname tunnel config + terraform-managed DNS
records with a single catch-all tunnel rule that forwards everything to
Traefik, and delegates DNS reconciliation to external-dns on the foundry
cluster. Adding a new hostname is now one edit: the Ingress in the
service's manifest set. No terraform change, no CF dashboard step.

Same pattern scales to N clusters: each cluster runs its own tunnel + its
own external-dns with a distinct txtOwnerId, managing its own disjoint
hostname set in the shared gauchoracing.com zone.

- foundry.tf: tunnel config -> single catch-all rule; drop
  cloudflare_dns_record.foundry + local.foundry_hostnames; update
  runbook comment for the new bootstrap sequence.
- apps-foundry/external-dns.yaml: Helm-chart Application with
  txtOwnerId=gr-foundry, txtPrefix=fdry-. Reads the tunnel UUID from
  a manually-created ConfigMap (populated from `terraform output`) so
  the tunnel ID never has to live in git. --default-targets forces
  every record to CNAME the tunnel regardless of the Ingress's LB
  status field.
- manifests-foundry/sentinel/ingress.yaml: add
  external-dns.alpha.kubernetes.io/cloudflare-proxied annotation so
  external-dns writes the record orange-cloud.
@BK1031 BK1031 merged commit ca42196 into main Jul 6, 2026
1 check passed
@BK1031 BK1031 deleted the bk1031/foundry-migration branch July 6, 2026 04:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant