Micro-segmentation with ESGs¶
Problem — the storefront works, but its security still rides on EPGs, which
are welded to bridge domains and subnets. Security now needs to follow the
workload, not the wire: every endpoint tagged tier=web belongs to the web
security zone wherever it lives, and the zones talk to each other over
contracts. That is an endpoint security group (ESG) — classification
decoupled from the network, scoped to a VRF.
This continues the commerce deployment from Stand up a multi-tier application.
The design¶
An ESG lives in a VRF and pulls endpoints in by selector. Here each zone selects on a policy tag, so onboarding a workload is a matter of tagging it — no BD or subnet change. The same contracts govern traffic between the zones, provided and consumed on the ESGs:
from niwaki import Niwaki
from niwaki.design import tenant
config = tenant("commerce")
config.vrf("prod")
app = config.app("storefront")
web = app.esg("esg-web").bind(vrf="prod")
web.tag_selector("tier", "web")
web.consume("web-to-app")
svc = app.esg("esg-app").bind(vrf="prod")
svc.tag_selector("tier", "app")
svc.provide("web-to-app")
svc.consume("app-to-db")
db = app.esg("esg-db").bind(vrf="prod")
db.tag_selector("tier", "db")
db.provide("app-to-db")
config.filter("f-http").entry("http", tcp=8080)
config.filter("f-postgres").entry("pg", tcp=5432)
config.contract("web-to-app").set(scope="vrf").subject("http").bind(filter="f-http")
config.contract("app-to-db").set(scope="vrf").subject("sql").bind(filter="f-postgres")
The bind(vrf="prod") is mandatory — an ESG has no meaning without a VRF scope —
and it sits on the ESG cursor, before the selectors and verbs that are its
children. tag_selector(key, value) matches the policy tag; provide and
consume attach contracts exactly as they do on an EPG.
Plan and push¶
aci = Niwaki.connect("https://apic.example.com", "admin", "secret")
plan = config.push(aci, mode="plan")
print(f"{len(plan.creates)} objects to create")
config.push(aci)
The with form closes the session for you; connect() is used here so the
rest of the page can share one client — see Connection & transport.
Verify¶
esgs = aci.tenant("commerce").query("fvESg").fetch()
assert {e.name for e in esgs} == {"esg-web", "esg-app", "esg-db"}
selectors = aci.tenant("commerce").query("fvTagSelector").fetch()
assert {s.match_value for s in selectors} == {"web", "app", "db"}
assert config.push(aci, mode="plan").has_changes is False
Variations & pitfalls¶
Selectors are additive — an ESG can combine
tag_selector,ep_selector(an IP or MAC match expression likeep_selector("ip=='10.30.10.0/24'")) andepg_selector(pull in a whole EPG by DN). A workload matched by any selector joins the zone.Tags are unique per VRF — the same
key=valuetag classifies one way in a VRF; do not reuse a tag pair for two zones in the same VRF.ESG contracts, not taboo — ESGs use
provide/consume/intra_epg; they do not support taboo contracts (the APIC rejects the attachment). Model deny-intent with a tighter contract set instead.Migrating off EPGs — you can run EPG and ESG classification side by side during a cutover, then retire the EPG contracts once every endpoint is tagged and matched.