@@ -153,18 +153,80 @@ locals {
153153 # COORDINATOR_VIPS — comma-separated for serf retry-join.
154154 coordinator_vips = join (" ," , [for p in local . peers : tostring (p. vip ) if p . role == " coordinator" ])
155155
156- # Service VIPs: 127.10.0.<vip>/32 — one per Connect upstream a
157- # worker consumes. Three services in this template (extend by
158- # adding entries here + Connect upstreams in the sidecar config):
159- # webdemo for the cross-peer fan-out demo
160- # postgres-master the Patroni leader, leader-aware via subset filter
161- # postgres-replica any Patroni replica
162- service_vips = [
163- { name = " webdemo" , vip = 10 , port = 8080 },
164- { name = " postgres-master" , vip = 20 , port = 5432 },
165- { name = " postgres-replica" , vip = 21 , port = 5432 },
156+ # ---------- Service declarations ----------
157+ #
158+ # local.services is the single source of truth for every microservice
159+ # on the mesh. Adding a service is one entry here; the platform
160+ # sidecar reads SERVICES_JSON at startup and generates everything
161+ # downstream (loopback aliases, /etc/hosts, Consul registrations,
162+ # Envoy supervise loops, mesh-conn allowlist).
163+ #
164+ # name Consul service name (also the /etc/hosts alias).
165+ # port Canonical app port. App binds 127.0.0.1:port; the local
166+ # Envoy upstream listener binds 127.10.0.<vip>:port.
167+ # subset Optional. When set, this entry is a subset filter on a
168+ # shared backend (the postgres-master/-replica split is the
169+ # only case today). Entries sharing the same `port` collapse
170+ # into one producer-side sidecar — same Envoy public
171+ # listener, same Connect-mTLS endpoint, just different
172+ # service-resolver names with different subset filters.
173+ services_raw = [
174+ { name = " webdemo" , port = 8080 , subset = null },
175+ { name = " postgres-master" , port = 5432 , subset = " master" },
176+ { name = " postgres-replica" , port = 5432 , subset = " replica" },
166177 ]
167- upstreams_json = jsonencode (local. service_vips )
178+
179+ # Backends = unique canonical ports in declaration order. Each
180+ # backend gets ONE producer-side sidecar at sidecar_port=21000+idx.
181+ # Logical names sharing a backend (e.g. postgres-master + -replica)
182+ # ride the same Envoy public listener; their distinct names live
183+ # purely in service-resolver redirects on the Consul side.
184+ service_ports = distinct ([for s in local . services_raw : s . port ])
185+
186+ port_sidecar = {
187+ for idx , p in local . service_ports : tostring (p) => 21000 + idx
188+ }
189+
190+ # Parent service name per backend. If at least one entry at this
191+ # port has subset=null, that entry's `name` is the parent (the
192+ # platform sidecar registers a Consul service with Connect.SidecarService
193+ # inline — pattern A, webdemo case). If every entry has a subset,
194+ # the parent is var.cluster_name (pattern B — Patroni-style: the
195+ # parent service is auto-registered by the workload itself under
196+ # its `scope`, so the platform only registers a standalone
197+ # connect-proxy pointing at it).
198+ port_parent = {
199+ for p in local . service_ports : tostring (p) => try (
200+ [for s in local . services_raw : s . name if s . port == p && s . subset == null ][0 ],
201+ var.cluster_name,
202+ )
203+ }
204+
205+ # Whether the platform registers the parent service itself.
206+ # False = the workload (e.g. Patroni) auto-registers its parent.
207+ port_registers_parent = {
208+ for p in local . service_ports : tostring (p) =>
209+ length ([for s in local . services_raw : s if s . port == p && s . subset == null ]) > 0
210+ }
211+
212+ # Final per-service descriptors. VIP octets allocated 10+ordinal so
213+ # subset entries each get their own 127.10.0.<vip>/32 loopback alias
214+ # + /etc/hosts entry — that's what makes `postgres-master:5432` and
215+ # `postgres-replica:5432` resolve to different consumer-side Envoy
216+ # listeners even when they share a producer-side sidecar.
217+ services = [
218+ for idx , s in local . services_raw : {
219+ name = s . name
220+ port = s . port
221+ subset = s . subset
222+ vip = 10 + idx
223+ sidecar_port = local . port_sidecar [tostring (s. port )]
224+ parent = local . port_parent [tostring (s. port )]
225+ registers_parent = local . port_registers_parent [tostring (s. port )]
226+ }
227+ ]
228+
229+ services_json = jsonencode (local. services )
168230}
169231
170232# ---------- Coordinator ----------
@@ -228,7 +290,7 @@ resource "phala_app" "worker" {
228290 env = {
229291 CLUSTER_NAME = var.cluster_name
230292 PEERS_JSON = local.peers_json
231- UPSTREAMS_JSON = local.upstreams_json
293+ SERVICES_JSON = local.services_json
232294 WORKER_ORDINAL = tostring (each. value )
233295 EXPECTED_REPLICAS = var.worker_replicas + var.coordinator_replicas
234296 COORDINATOR_VIPS = local.coordinator_vips
@@ -253,6 +315,20 @@ resource "phala_app" "worker" {
253315 wait_for_ready = true
254316 wait_timeout_seconds = 600
255317
318+ # Plan-time check: every (name, subset) tuple in local.services must
319+ # be unique. Catches typos like declaring two `webdemo` entries, or
320+ # two `postgres-master` entries with the same subset. Backends
321+ # (services sharing a canonical port) intentionally collapse onto
322+ # one sidecar_port; that's not flagged.
323+ lifecycle {
324+ precondition {
325+ condition = length (local. services ) == length (distinct ([
326+ for s in local . services : " ${ s . name } /${ s . subset == null ? " " : s . subset } "
327+ ]))
328+ error_message = " local.services has duplicate (name, subset) entries — each logical service name (with its subset) must be unique."
329+ }
330+ }
331+
256332 depends_on = [phala_app . coordinator ]
257333}
258334
0 commit comments