Self-Service CI/CD for AWS

Self-service AWS CodePipeline platform — developers ship compliant CI/CD pipelines in minutes via dashboard, CLI, CDK, or AI prompt, while platform teams enforce policy-as-code guardrails, governance, and per-team isolation.

Service Mesh (Istio Ambient)

Pipeline Builder runs an Istio ambient (sidecar-less) service mesh on every deploy target — deploy/local/minikube, deploy/aws/ec2, and deploy/aws/eks. It provides STRICT mutual TLS and identity-based L4 authorization between every service, closing the gap left by the Kubernetes NetworkPolicy / CiliumNetworkPolicy files (which are unenforced on all three targets — no Cilium controller is installed and the minikube substrates use the default bridge CNI).

TL;DR — All east-west traffic inside the pipeline-builder namespace is mTLS-encrypted and authorized by SPIFFE identity. The only plaintext hops are the intentional ingress edge (the ALB / nginx TLS listener) and two PERMISSIVE carve-outs. The mesh is the real enforcement layer; the NetworkPolicy files are best-effort defense-in-depth for the day a CNI enforces them.

Why ambient (not sidecars)

Ambient adds only node-level components (ztunnel DaemonSet + istio-cni), living in istio-systemno per-pod proxy and no changes to any container or securityContext. That matters here because:

AWS recommends EKS Auto Mode + Istio ambient, so eks uses the same data plane as local/ec2 — the design is uniform across all three targets.

Architecture

[ALB/ACM (aws)  or  nginx TLS (local)]
        │  (plaintext to nginx:8080 on aws; TLS to nginx:8443 on local — PERMISSIVE carve-out)
        ▼
      nginx  ──ztunnel HBONE mTLS──▶  platform / pipeline / plugin / frontend / ...   [STRICT]
                                          │
                       ztunnel HBONE mTLS ▼
                    postgres / pgbouncer / mongodb / redis(+sentinel) / registry / minio   [STRICT, TCP]

mTLS posture

PeerAuthentication/default sets STRICT namespace-wide. Two PERMISSIVE carve-outs, both port-scoped, keep non-mesh clients working:

Carve-out Where Why
nginx 8080 (local also 8443) PeerAuthentication/nginx-ingress The ALB (aws) / browser (local) is not a mesh member; nginx terminates the external leg. nginx→upstream stays STRICT.
prometheus 9090 PeerAuthentication/prometheus-keda KEDA’s metrics-adapter (in the keda namespace, non-mesh) scrapes it for the plugin ScaledObject.

Everything else — including app→datastore TCP (postgres/mongo/redis/registry/ minio) — is STRICT mTLS. Kubelet health probes are auto-exempted by istio-cni; Prometheus→app /metrics is in-mesh (Prometheus is in the same namespace).

Authorization (identity-based L4)

Each workload runs under its own ServiceAccount (created in istio.yaml), so its SPIFFE identity is cluster.local/ns/pipeline-builder/sa/<name>. Sensitive workloads (datastores + app APIs) have an AuthorizationPolicy (action: ALLOW) listing exactly the caller identities real traffic needs.

ALLOW = default-deny once selected. Every scraped app service lists prometheus (metrics share port 3000); every app API lists nginx (the single ingress principal); registry/minio list default (bootstrap Jobs). Observability infra (loki/alertmanager/thanos/jaeger) has no policy → STRICT-mTLS-only (any mesh peer), to bound the enumeration surface.

Allow-lists were derived from real dependencies, not the (stale) NetworkPolicy files — e.g. Redis is used by ~every service (not just plugin), and reporting connects to postgres. The refreshed NetworkPolicy files now agree with the mesh policies. See the appendix in the implementation plan for the full table.

aws specifics

External egress

The mesh keeps Istio’s default outboundTrafficPolicy: ALLOW_ANY — do not set REGISTRY_ONLY, or you break billing→payment providers, plugin/buildkit→ pypi/ghcr base-image pulls, message→SES/SMTP, and platform→GitHub/Bitbucket OAuth. External destinations are plaintext-passthrough (protected by the remote’s own TLS); the plugin/billing egress NetworkPolicies still bound them.

Queues, KEDA & buildkit

Verify

# control plane
kubectl get pods -n istio-system                 # istiod, ztunnel, istio-cni Ready
istioctl analyze -n pipeline-builder             # no errors

# every pod enrolled in ambient (HBONE)
istioctl ztunnel-config workloads                # PROTOCOL=HBONE for each pod

# mTLS is enforced: a pod OUTSIDE the mesh is denied
kubectl run probe --rm -it --image=curlimages/curl -- \
  curl -m5 http://platform.pipeline-builder:3000/health   # expect connection reset/denied

# ingress still works
curl -sk https://localhost:8443/health           # local
curl -s  https://<alb-dns>/health                # aws

eks adds: confirm capture across all Auto Mode nodes, node SecurityGroups allow node↔node :15008 (HBONE), and a Karpenter scale-up captures pods on fresh nodes (ztunnel/istio-cni Ready first).

LEAN mode (trimming the footprint)

The mesh adds ~1 CPU (istiod + ztunnel + istio-cni) on top of the app stack. On an ~8-core node the full stack + mesh exceeds 8 vCPU and pods sit Pending (and can starve the apiserver). LEAN=1 trims the deploy so the core stack + mesh fits — supported on both minikube and ec2 (they share the minikube substrate):

# minikube (~8-core laptop)
LEAN=1 ./deploy/local/minikube/bin/setup.sh
# clean restart: minikube delete --profile=pipeline-builder, then re-run

# ec2 (fits a t3.xlarge / 4 vCPU instead of requiring t3.2xlarge)
LEAN=1 sudo -E bash deploy/aws/ec2/bin/startup.sh   # -E preserves LEAN through sudo
# ...or at launch: set the CFN `Lean` param — LEAN=1 deploy/aws/ec2/bin/setup.sh
#    (or: pipeline-manager infra provision --target ec2 --lean --instance-type t3.xlarge)

LEAN omits the optional observability/admin services (prometheus, thanos, loki, promtail, jaeger, alertmanager, mongo-express, pgadmin) and collapses every workload to a single replica, leaving the core stack + mesh room to schedule. Both targets drive the same lean_filter over the kustomize stream. The full stack is the default (LEAN=0) for larger machines; eks is unaffected (Karpenter provisions more nodes instead).

Troubleshooting

Symptom Likely cause
A service 403s another at L4 Caller identity missing from the callee’s AuthorizationPolicy — add its sa/<name> and re-apply istio.yaml.
mongodb ReplicaSetNoPrimary / stateful peer stuck The workload’s own SA must be in its AuthorizationPolicy — replica-set/gossip traffic is self-directed (e.g. sa/mongodb on mongodb-allow).
All calls to a service denied An ALLOW policy selected it but omitted a real caller (often prometheus or nginx).
External ingress broken after enabling STRICT nginx external port not carved out (8080 on aws, 8080+8443 on local).
Builds can’t pull base images outboundTrafficPolicy was set to REGISTRY_ONLY — revert to ALLOW_ANY.
Pods not captured (no HBONE) istio-cni not Ready before the pod started; on a non-standard node, set values.cni.cniConfDir/cniBinDir.
Periodic Redis reconnects ztunnel idle-timeout reaping idle pub/sub connections — benign; tune the client keepAlive if noisy.

Optional: Kiali

Kiali (mesh visualization) is not installed to save resources. Add it per the upstream Istio docs if you want a topology/health dashboard.

Cross-target parity

The three k8s/ trees are parallel copies. The policy model is identical across them (same SAs, PeerAuthentications, AuthorizationPolicies); only these differ: nginx carve-out port (local adds 8443), Redis Sentinel (aws), per-org + db-backup (aws/eks), and the install mechanics (istiod HA + Auto Mode notes on eks). Keep them in sync when editing.