<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jan Pokorný</title>
    <description>The latest articles on DEV Community by Jan Pokorný (@jan_pokorn_cd874da1d88ff).</description>
    <link>https://dev.to/jan_pokorn_cd874da1d88ff</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4063723%2F1fc9633b-c039-4ef4-bea4-f6d5eff1db55.jpg</url>
      <title>DEV Community: Jan Pokorný</title>
      <link>https://dev.to/jan_pokorn_cd874da1d88ff</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jan_pokorn_cd874da1d88ff"/>
    <language>en</language>
    <item>
      <title>Terraform finally met switches: Cisco Network as Code with ACI just works</title>
      <dc:creator>Jan Pokorný</dc:creator>
      <pubDate>Thu, 29 Oct 2026 08:45:50 +0000</pubDate>
      <link>https://dev.to/jan_pokorn_cd874da1d88ff/terraform-finally-met-switches-cisco-network-as-code-with-aci-just-works-22al</link>
      <guid>https://dev.to/jan_pokorn_cd874da1d88ff/terraform-finally-met-switches-cisco-network-as-code-with-aci-just-works-22al</guid>
      <description>&lt;p&gt;Every network engineer wants to automate their network. Automation is our pride. Working automation is proof of good architecture — and in large setups, an operational necessity. Yet I've always felt that switch vendors aren't exactly helping us.&lt;/p&gt;

&lt;p&gt;Networking is a conservative world. Every time I dove into automation, I ended up frustrated by how far behind it is. First, there are the archaic protocols that nobody truly understands and that — at least everywhere I've worked — never delivered in practice: SNMP in write mode, NETCONF, RESTCONF, screen-scraping over SSH. (Yes, OpenConfig and gNMI exist; if your vendor mix supports them well, I envy you.) Right next to them stands a long parade of revolutionary proprietary management systems from the vendors themselves. The trouble with those is always the same: they're heavyweight, they integrate poorly with anything else, and above all they have the lifespan of a mayfly. Just for HP switches, I'm running out of fingers on one hand counting how many times their primary management tool has changed.&lt;/p&gt;

&lt;p&gt;Ansible and the REST APIs appearing on switches looked like a small ray of hope. But even there, disappointment came with time. Ansible modules typically covered only &lt;em&gt;some&lt;/em&gt; parts of the configuration — full coverage was never on the table. Partly because modules were missing, partly because Ansible simply isn't the right tool for this domain: there's no state, no plan/diff before you push, and deleting things is entirely on you — which is exactly what makes it unmaintainable at scale. The final nail in the coffin is opening GitHub and seeing that the last commit to the relevant collection was five years ago.&lt;/p&gt;

&lt;p&gt;So in the end, we all land back at SSH and pushing CLI commands. Hand on heart — isn't that so? Thanks to netmiko we can more or less reliably shove config &lt;em&gt;onto&lt;/em&gt; switches and call it automation. But when someone asks whether we can also &lt;em&gt;change&lt;/em&gt; a config, or — heaven forbid — safely &lt;em&gt;delete&lt;/em&gt; one, we change the subject.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Ansible fan to Terraform convert
&lt;/h2&gt;

&lt;p&gt;I used to be a big Ansible fan. I built some genuinely nice, complex projects with it. I even wrote my own modules. Then I got pulled into a cloud project that required Terraform. The moment I tried it, I understood: for infrastructure, Terraform is &lt;em&gt;the hammer&lt;/em&gt;. And from that moment on, every problem I saw was a nail. From day one I kept thinking: I wish I could manage switches like this.&lt;/p&gt;

&lt;p&gt;Then the opportunity came. We decided to upgrade the network in our datacenters, and Cisco ACI was one of the contenders. I noticed Cisco offers Terraform-based management, so I tested it during the PoC. It worked well, and the conclusion was that it could cover our day-to-day operations and unlock automation.&lt;/p&gt;

&lt;p&gt;Long story short: today, &lt;strong&gt;99% of our entire ACI configuration is driven by Terraform&lt;/strong&gt;. From NTP to L3Outs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Cisco Network as Code actually is
&lt;/h2&gt;

&lt;p&gt;Cisco NaC is essentially &lt;em&gt;ready-to-eat Terraform&lt;/em&gt;, prepared directly by Cisco. And credit where it's due — Cisco did a phenomenal job here (at least for ACI). We run it on ACI 6.1(5e) with NaC module version 2.0.0.&lt;/p&gt;

&lt;p&gt;Why does it work so well? In my view, two cornerstones:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. APIC.&lt;/strong&gt; Managing one central controller with a high-quality API is orders of magnitude easier than managing dozens or hundreds of individual switches. Terraform talks to a single API with one coherent view of the fabric, not to a fleet of boxes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Separation of data from code.&lt;/strong&gt; As a user, you practically don't need to know Terraform at all. You don't write Terraform — you write a data structure (YAML), which the Terraform module reads and assembles itself into whatever is needed. This isn't a Cisco invention — Google does something similar in their Fabric FAST framework for landing zones — but Cisco executed it really well. A complete beginner can have a working project within a few hours.&lt;/p&gt;

&lt;p&gt;Look how simple the Terraform part is. This is essentially the &lt;em&gt;whole&lt;/em&gt; thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aci&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CiscoDevNet/aci"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aci"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aci_user&lt;/span&gt;
  &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aci_password&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aci_url&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"aci"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"netascode/nac-aci/aci"&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2.0.0"&lt;/span&gt;

  &lt;span class="nx"&gt;yaml_directories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"data/**"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;manage_access_policies&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;manage_fabric_policies&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;manage_pod_policies&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;manage_node_policies&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;manage_interface_policies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;manage_tenants&lt;/span&gt;            &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It really just says how to connect to the APIC, which configuration sections Terraform should manage, and where to load the data model from. You flip on only the sections you want Terraform to own — we leave pod policies at their day-0 defaults, which is pretty much the missing 1 % from the claim above.&lt;/p&gt;

&lt;p&gt;The data model itself is plain YAML. A bridge domain looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tenants&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;company&lt;/span&gt;
      &lt;span class="na"&gt;bridge_domains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;d2-dev-lan-1_1201&lt;/span&gt;
          &lt;span class="na"&gt;vrf&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dc2-l3-fix&lt;/span&gt;
          &lt;span class="na"&gt;unicast_routing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;General LAN network&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the best part: you can split the data model into as many small files as you like and build any directory structure that suits your team. Terraform merges everything into one big YAML document and processes that. Our structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── docs
├── iac
│   └── data
│       ├── access_policies
│       │   ├── interface_policies
│       │   └── switch_policies
│       ├── fabric_policies
│       ├── interface_policies
│       │   ├── 101_102
│       │   ├── 103_104
│       │   ├── 105_106
│       │   └── ...
│       ├── node_policies
│       └── tenants
│           ├── mgmt
│           ├── company
│           │   ├── application_profiles
│           │   │   └── endpoint_groups
│           │   ├── bridge_domains
│           │   └── vrfs
│           └── lab
└── scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cisco's model covers, I dare say, 100% of the configuration — I haven't yet hit anything I couldn't find — and the &lt;a href="https://netascode.cisco.com/docs/data_models/apic/overview/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; is perfectly readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform is not the automation. It's the key to it.
&lt;/h2&gt;

&lt;p&gt;Managing APIC with Terraform is not automation by itself. It's the key that unlocks it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It unlocks GitOps.&lt;/strong&gt; Your complete configuration is now text, living in git. You have every change and the complete history — from the day the fabric was born to the last update. You know who made each change and based on what. (We had the luxury of a greenfield deployment — our fabric was born straight into git. Adopting NaC on an existing brownfield fabric is possible, but it's a story of its own.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It unlocks CI/CD and real change management.&lt;/strong&gt; Suddenly, changes aren't made by a specific engineer clicking in a GUI — they're made by a pipeline. Every change is a pull request that someone has to approve. In the PR you see exactly what is being proposed, and exactly what Terraform plans to do on the APIC side (&lt;code&gt;terraform plan&lt;/code&gt; is posted right into the PR as a comment). Review rules are explicit and enforced. And before anything gets applied, the pipeline takes an APIC configuration snapshot. No snapshot, no apply. Period.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg6jkd8ar52h1fw0890do.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg6jkd8ar52h1fw0890do.png" alt="Pull request with Terraform plan posted as a comment" width="800" height="740"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fny7uhdpko3an65dq4owj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fny7uhdpko3an65dq4owj.png" alt="Plan detail: Plan: 11 to add, 0 to change, 0 to destroy" width="800" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ah1xo1s2r22un2wxw5r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ah1xo1s2r22un2wxw5r.png" alt="Pipeline run: plan → snapshot → apply" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  NetBox as the source of truth
&lt;/h3&gt;

&lt;p&gt;Now for the actual automation. We record absolutely everything in NetBox — it is our source of truth. Devices, every single cable, VLANs, prefixes, IP addresses, VRFs, virtualization clusters… everything is diligently documented there.&lt;/p&gt;

&lt;p&gt;Since the ACI side is now just YAML files, nothing was easier than adding a few custom fields to NetBox and — with a handful of prompts to Claude — building a script that pulls data from the NetBox API and updates the YAML files. New network segment? Add a record to the central documentation, and within minutes there's a pull request waiting for approval. Approve, and it's deployed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69b1pc7ee8xjkb6d66wz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F69b1pc7ee8xjkb6d66wz.png" alt="VLAN object in NetBox with ACI custom fields" width="800" height="701"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Or skip the scripts — bring an AI agent
&lt;/h3&gt;

&lt;p&gt;Maybe writing Python scripts isn't trendy anymore. Maybe it isn't even necessary. The next step is an AI agent. If there's one thing AI agents are unbeatable at, it's working with text — and what better task could one get than generating YAML from a work order?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I've added server company-server-01 to NetBox. Prepare the configuration to connect it to ACI."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent knows NetBox exists. It can look up which rack the server is in and which leaf switches live in that rack. It can check what cable type is planned. It knows our port-allocation rules and the ACI object naming convention. This is a trivial task — even a small model handles it comfortably. And there's nothing to be afraid of: it does exactly the same thing a human would. It opens a pull request for approval. There's still a human in the loop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F969qog3qwuepg6dbxs0w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F969qog3qwuepg6dbxs0w.png" alt="AI agent session preparing ACI YAML from NetBox data" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The risks (and how to handle them)
&lt;/h2&gt;

&lt;p&gt;It wouldn't be fair to end without the downsides. Cisco NaC has a few, and they're manageable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The growing state file
&lt;/h3&gt;

&lt;p&gt;Every new section of configuration means a bit more time for Terraform to compute its plan. In the beginning, Terraform was snappy; as the project grows, the full cycle takes longer and longer. This isn't an ACI or NaC problem — it's a classic Terraform problem, and the solution is classic too: split the project into sub-projects per configuration section. If I'm working on access policies, there's no need to recompute state for tenants, and vice versa — and when both change, they can run in parallel. Cisco NaC already divides the configuration tree into six sections (you can see them as the &lt;code&gt;manage_*&lt;/code&gt; flags in &lt;code&gt;main.tf&lt;/code&gt; above), which is the natural place to cut. Splitting below that level feels risky to me. We're not that big and I'm confident we'll manage all-in-one — but for huge setups, this would be a real topic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drift — and the blast radius of an apply
&lt;/h3&gt;

&lt;p&gt;The elephant in the room: what happens when someone changes something in the GUI? Terraform doesn't know about it — until the next plan, which flags it as drift, and the next apply, which reverts it. That's either your best friend or a production incident, depending on whether the GUI change was a mistake or an emergency fix someone forgot to backport.&lt;/p&gt;

&lt;p&gt;We handle it with two rules. First, configuration changes go through pull requests only — the GUI is for reading and troubleshooting. Second, the pipeline runs &lt;code&gt;terraform plan&lt;/code&gt; on a schedule, so any drift shows up as an alert within hours, not months later during an unrelated change.&lt;/p&gt;

&lt;p&gt;The same mechanism cuts the other way, too: a bad PR can produce a plan that &lt;em&gt;destroys&lt;/em&gt; objects — and on a production fabric, &lt;code&gt;X to destroy&lt;/code&gt; can mean EPGs going away along with the traffic in them. This is exactly why the plan is posted into the PR: the reviewer sees the add/change/destroy counts before approving, and the pre-apply APIC snapshot is the last resort. An automated guard that blocks plans with unexpected destroy counts would be a sensible next hardening step — reviewers are human.&lt;/p&gt;

&lt;h3&gt;
  
  
  No referential integrity between objects
&lt;/h3&gt;

&lt;p&gt;Thanks to the data/code separation, the links between objects are gone. Nothing stops you from referencing a policy that doesn't exist — typically a typo in a name. With hand-written Terraform and its resource references, this couldn't happen. It's a small thing, but worth knowing. There's a possible fix: &lt;a href="https://github.com/netascode/nac-validate" rel="noopener noreferrer"&gt;nac-validate&lt;/a&gt;, a CLI that validates the YAML against a schema and runs semantic rules (written in Python) that should catch dangling references before the plan even runs. I'll be honest — I only gave it a quick try, it didn't really grab me, and we don't use it for now. Your mileage may vary, so it's worth a look. Until then, an AI agent doing a pre-push textual consistency check finds this kind of problem effortlessly, and worst case, the APIC will raise a fault and tell you anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if it all falls apart?
&lt;/h3&gt;

&lt;p&gt;What if, for whatever reason, you can't continue with Terraform? Then nothing stops you from simply throwing it away and continuing another way. The objects Terraform created are ordinary ACI objects — you just start managing them differently. There's no lock-in in the data. Support-wise, one nuance worth knowing: the underlying &lt;a href="https://netascode.cisco.com/resources/supported_products/" rel="noopener noreferrer"&gt;ACI Terraform provider is fully TAC-supported&lt;/a&gt;, while the NaC modules themselves are open source maintained by Cisco, with Cisco Professional Services behind them for customization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;For years, "network automation" meant heroically wrapping duct tape around SSH. With ACI and Cisco Network as Code, for the first time I feel like we're doing infrastructure-as-code on the network with the same comfort the cloud folks have had for a decade: declarative data model, git history, PR-based change management, CI/CD, and a source of truth feeding it all.&lt;/p&gt;

&lt;p&gt;If you're running ACI and still clicking in the GUI — try the PoC. It took us a few hours to get the first working project, and we never looked back.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Happy to answer questions about our setup — pipelines, NetBox integration, or the AI agent workflow. Drop them in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>networking</category>
      <category>terraform</category>
      <category>cisco</category>
      <category>devops</category>
    </item>
    <item>
      <title>No SNMP OIDs for DOM stats on Cisco ACI switches? Use the API and Zabbix.</title>
      <dc:creator>Jan Pokorný</dc:creator>
      <pubDate>Wed, 05 Aug 2026 07:57:00 +0000</pubDate>
      <link>https://dev.to/jan_pokorn_cd874da1d88ff/no-snmp-oids-for-dom-stats-on-cisco-aci-switches-use-the-api-and-zabbix-593d</link>
      <guid>https://dev.to/jan_pokorn_cd874da1d88ff/no-snmp-oids-for-dom-stats-on-cisco-aci-switches-use-the-api-and-zabbix-593d</guid>
      <description>&lt;p&gt;&lt;em&gt;Tested on Zabbix 7.0.0 and Cisco ACI 6.1(5e).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you run optical links, you probably already know this: the moment you actually &lt;em&gt;need&lt;/em&gt; DOM stats (Digital Optical Monitoring — the light levels your SFPs see and send) is the moment it's too late to start collecting them. The value is in the history.&lt;/p&gt;

&lt;p&gt;On our previous infrastructure we collected SFP Rx/Tx power over SNMP as a matter of course, and it paid off more than once. Watching the values over time makes troubleshooting flapping links much less of a guessing game — and in one case it did something even better: we had a batch of SFPs from a bad production series, and the slow, steady decline in their optical levels let me predict the failures about a week before they actually died. Replacing optics on your own schedule instead of at 3 AM is a good trade for a few Zabbix items.&lt;/p&gt;

&lt;p&gt;Then we moved to Cisco ACI, and the SNMP approach hit a wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: ACI doesn't expose DOM stats over SNMP
&lt;/h2&gt;

&lt;p&gt;As far as I can tell, Cisco ACI switches (Nexus 9000 series in ACI mode) simply don't have SNMP OIDs for DOM statistics. I tested this myself, walking the usual suspects, and found nothing. I even gave an Anthropic Opus model access to SNMP on the switches and let it dig through the MIBs on its own — same conclusion: it's just not there.&lt;/p&gt;

&lt;p&gt;I'm honestly not sure whether this is a limitation of the switch series or of ACI mode specifically. My money is on ACI mode, because on our Inter-Pod Network — similar but different Cisco Nexus switches running in NX-OS mode — the DOM OIDs exist and work exactly as you'd expect. Same vendor, same optics concept, different operating mode, completely different story.&lt;/p&gt;

&lt;h2&gt;
  
  
  DOM stats &lt;em&gt;are&lt;/em&gt; collected — by the APIC (once you turn it on)
&lt;/h2&gt;

&lt;p&gt;The good news: the APIC controller can collect DOM statistics from the whole fabric. The less good news: for some reason it's &lt;strong&gt;not enabled by default&lt;/strong&gt;. You have to switch it on here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fabric → Fabric Policies → Policies → Monitoring → Fabric Node Controls → Enable DOM&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F419kpa1fzmdosdjxt36k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F419kpa1fzmdosdjxt36k.png" alt="APIC GUI — Enable DOM toggle under Fabric Node Controls" width="598" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why isn't this the default? Genuine question. It's cheap telemetry about the physical layer of every link in your fabric, the hardware is already measuring it, and the failure mode it catches is one of the most common ones in a data center. If anyone from the BU is reading this — I'd love to hear the reasoning.&lt;/p&gt;

&lt;p&gt;Once enabled, the APIC collects the values, and I believe it would raise a fault if a value crossed the configured thresholds. But what the APIC &lt;em&gt;can't&lt;/em&gt; do is draw you a graph of those values over time. And as I said at the start, the history is the whole point — a single snapshot won't tell you that an SFP has been quietly losing 0.2 dB a week. So we want these values in an external monitoring system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring over the API instead
&lt;/h2&gt;

&lt;p&gt;No SNMP, but there's a perfectly good REST API. The catch: classic NMS tools and REST APIs don't get along very well. With Zabbix, though, this is very doable.&lt;/p&gt;

&lt;p&gt;The typical pain point of API-based monitoring is authentication. It's usually a multi-step dance: first a login call to get a token, then the actual query using that temporary token, plus refresh logic when it expires. That's exactly the kind of statefulness you don't want in a monitoring item — ideally, every poll should be &lt;strong&gt;a single self-contained API call&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Cisco APIC has a neat answer to this: &lt;strong&gt;certificate-based (signature) authentication&lt;/strong&gt;. You generate an X.509 certificate, attach it to a local APIC user, and then sign each request with the private key. The signature travels in a &lt;code&gt;Cookie&lt;/code&gt; header alongside the request itself — no login, no token, no session, one HTTP call.&lt;/p&gt;

&lt;p&gt;It's slightly more involved to set up, because you have to generate the certificate and sign every request. But it solves the one-call problem, and there's a nice security bonus: since a signature is only valid for the &lt;em&gt;exact&lt;/em&gt; request it was computed for, your monitoring system effectively gets access to a handful of specific API calls with specific attributes — and nothing else. Even if someone extracts the signed cookie from Zabbix, all they can do with it is read DOM stats. Then you just parse the JSON response and you're done.&lt;/p&gt;

&lt;p&gt;Here's how to set the whole thing up, end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Generate a certificate
&lt;/h2&gt;

&lt;p&gt;A plain self-signed cert is fine. Ten years of validity so you don't have to think about it again any time soon (after 10 years, generate and upload a new one):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl req &lt;span class="nt"&gt;-newkey&lt;/span&gt; rsa:2048 &lt;span class="nt"&gt;-nodes&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-keyout&lt;/span&gt; zabbix-monitoring.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 3650 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; zabbix-monitoring.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=zabbix-monitoring"&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 zabbix-monitoring.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the &lt;code&gt;.key&lt;/code&gt; file private — it never leaves the machine that signs requests. The &lt;code&gt;.crt&lt;/code&gt; is the public part that goes to APIC.&lt;/p&gt;

&lt;p&gt;If you'd rather not do this by hand, there's a &lt;a href="https://github.com/jpokorny-cz/zabbix-cisco-aci-templates/blob/master/scripts/generate-apic-cert.sh" rel="noopener noreferrer"&gt;ready-made script&lt;/a&gt; in my GitHub repo (more on that at the end) that generates the pair and prints everything you'll need for the next steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create the APIC user and attach the certificate
&lt;/h2&gt;

&lt;p&gt;In the APIC GUI:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Admin → AAA → Users → Create Local User&lt;/strong&gt;, e.g. &lt;code&gt;zabbix-monitoring&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;Assign the correct permissions. I used security domain &lt;strong&gt;all&lt;/strong&gt;, role &lt;strong&gt;read-all&lt;/strong&gt;, with &lt;strong&gt;Read privilege&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Open the user, go to &lt;strong&gt;X.509 Certificates&lt;/strong&gt;, click &lt;strong&gt;+&lt;/strong&gt;, name the certificate (I keep it identical to the username, e.g. &lt;code&gt;zabbix-monitoring&lt;/code&gt;), and paste in the contents of the &lt;code&gt;.crt&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Save.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw3nv6619uh3fus2tggud.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw3nv6619uh3fus2tggud.png" alt="APIC GUI — local user with an attached X.509 certificate" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two values from this step matter later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The certificate DN&lt;/strong&gt;, which follows the pattern &lt;code&gt;uni/userext/user-&amp;lt;username&amp;gt;/usercert-&amp;lt;certname&amp;gt;&lt;/code&gt;. With the names above: &lt;code&gt;uni/userext/user-zabbix-monitoring/usercert-zabbix-monitoring&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The certificate's SHA256 fingerprint&lt;/strong&gt;, which you can get locally:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl x509 &lt;span class="nt"&gt;-in&lt;/span&gt; zabbix-monitoring.crt &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-fingerprint&lt;/span&gt; &lt;span class="nt"&gt;-sha256&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: The URL, and how to sign a request
&lt;/h2&gt;

&lt;p&gt;DOM stats live in two APIC object classes — one for the receive side, one for the transmit side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rx: &lt;code&gt;/api/class/ethpmDOMRxPwrStats.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tx: &lt;code&gt;/api/class/ethpmDOMTxPwrStats.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Important detail: &lt;strong&gt;Rx and Tx are two separate API calls, and therefore two separate signed requests.&lt;/strong&gt; The signature covers &lt;code&gt;method + URI + body&lt;/code&gt; of one specific request, so you can't reuse the Rx cookie for the Tx endpoint. You'll go through the signing below twice, once per URI.&lt;/p&gt;

&lt;p&gt;The signing itself is: take the string &lt;code&gt;GET&lt;/code&gt; + the URI (path and query string, no host), sign it with your private key (SHA256 + PKCS#1 v1.5), and base64 the result. In plain shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'/api/class/ethpmDOMRxPwrStats.json'&lt;/span&gt;

&lt;span class="nv"&gt;SIGNATURE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'GET%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URI&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | openssl dgst &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="nt"&gt;-sign&lt;/span&gt; zabbix-monitoring.key &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; 0&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;FINGERPRINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl x509 &lt;span class="nt"&gt;-in&lt;/span&gt; zabbix-monitoring.crt &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-fingerprint&lt;/span&gt; &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^.*Fingerprint=//'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;DN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'uni/userext/user-zabbix-monitoring/usercert-zabbix-monitoring'&lt;/span&gt;

&lt;span class="nv"&gt;COOKIE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"APIC-Certificate-Algorithm=v1.0; APIC-Certificate-Fingerprint=&lt;/span&gt;&lt;span class="nv"&gt;$FINGERPRINT&lt;/span&gt;&lt;span class="s2"&gt;; APIC-Certificate-DN=&lt;/span&gt;&lt;span class="nv"&gt;$DN&lt;/span&gt;&lt;span class="s2"&gt;; APIC-Request-Signature=&lt;/span&gt;&lt;span class="nv"&gt;$SIGNATURE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$COOKIE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the test call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sk&lt;/span&gt; &lt;span class="s2"&gt;"https://&amp;lt;your-apic&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;$URI&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Cookie: &lt;/span&gt;&lt;span class="nv"&gt;$COOKIE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything is wired up correctly, you get back JSON along these lines (trimmed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"214"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imdata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ethpmDOMRxPwrStats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"attributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"dn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"topology/pod-1/node-101/sys/phys-[eth1/1]/phys/domstats/rxpower"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-2.334269"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"hiAlarm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.999912"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"hiWarn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.000000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"loAlarm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-13.695722"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"loWarn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-10.300390"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"alert"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One object per optical interface in the whole fabric, with the current Rx power in dBm plus the module's own alarm/warning thresholds. The &lt;code&gt;dn&lt;/code&gt; tells you exactly which pod/node/port it is.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;Tx&lt;/strong&gt;, repeat the exact same procedure with &lt;code&gt;URI='/api/class/ethpmDOMTxPwrStats.json'&lt;/code&gt; — nothing else changes.&lt;/p&gt;

&lt;p&gt;There's also a &lt;a href="https://github.com/jpokorny-cz/zabbix-cisco-aci-templates/blob/master/scripts/generate-apic-cookie.py" rel="noopener noreferrer"&gt;Python script&lt;/a&gt; in the repo that does the signing for you and prints the finished Cookie header value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./generate-apic-cookie.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key&lt;/span&gt; zabbix-monitoring.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cert&lt;/span&gt; zabbix-monitoring.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dn&lt;/span&gt; &lt;span class="s2"&gt;"uni/userext/user-zabbix-monitoring/usercert-zabbix-monitoring"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--uri&lt;/span&gt; &lt;span class="s2"&gt;"/api/class/ethpmDOMRxPwrStats.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Wiring it into Zabbix
&lt;/h2&gt;

&lt;p&gt;Now the Zabbix side. The pattern is: one HTTP agent item fetches the raw JSON, a low-level discovery rule finds all the interfaces in it, and dependent item prototypes slice out the per-interface dBm values — so the APIC gets polled exactly once per cycle no matter how many optics you have.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Create the host and macros
&lt;/h3&gt;

&lt;p&gt;Create a host for your APIC (&lt;strong&gt;Data collection → Hosts → Create host&lt;/strong&gt;) and give it an interface with the APIC's address. The interface type doesn't matter here — it's just a holder for the APIC's FQDN/IP (used via &lt;code&gt;{HOST.CONN}&lt;/code&gt; below), since the actual polling happens through HTTP agent items, not the interface itself. Then add two host macros holding the full Cookie header values you generated in step 3:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;{$APIC.COOKIE.RX}&lt;/code&gt; = &lt;code&gt;APIC-Certificate-Algorithm=v1.0; APIC-Certificate-Fingerprint=...; APIC-Certificate-DN=...; APIC-Request-Signature=...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{$APIC.COOKIE.TX}&lt;/code&gt; = same idea, generated for the Tx URI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj1p0vfqpl5l6juvo5f1h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj1p0vfqpl5l6juvo5f1h.png" alt="host macros with the two cookie values" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 The raw HTTP agent item
&lt;/h3&gt;

&lt;p&gt;Create an item on the host:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name:&lt;/strong&gt; &lt;code&gt;APIC Rx power raw&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; &lt;code&gt;HTTP agent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key:&lt;/strong&gt; &lt;code&gt;apic.optics.rx.raw&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;code&gt;https://{HOST.CONN}/api/class/ethpmDOMRxPwrStats.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headers:&lt;/strong&gt; &lt;code&gt;Cookie&lt;/code&gt; → &lt;code&gt;{$APIC.COOKIE.RX}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type of information:&lt;/strong&gt; &lt;code&gt;Text&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History / Trends:&lt;/strong&gt; &lt;code&gt;Do not store&lt;/code&gt; (this item only exists to feed the items below — no point storing the huge JSON blob)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gq1xrugxbi2u8p4e4f0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gq1xrugxbi2u8p4e4f0.png" alt="Zabbix — HTTP agent item configuration" width="800" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Test it (the &lt;strong&gt;Test&lt;/strong&gt; button in the item form) — you should see the same JSON as from curl.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 The discovery rule
&lt;/h3&gt;

&lt;p&gt;Create a discovery rule on the host:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name:&lt;/strong&gt; &lt;code&gt;APIC Rx optics discovery&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; &lt;code&gt;Dependent item&lt;/code&gt;, &lt;strong&gt;Master item:&lt;/strong&gt; &lt;code&gt;apic.optics.rx.raw&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key:&lt;/strong&gt; &lt;code&gt;apic.optics.rx.discovery&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preprocessing:&lt;/strong&gt; one step, &lt;code&gt;JSONPath&lt;/code&gt; → &lt;code&gt;$.imdata&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLD macros:&lt;/strong&gt; &lt;code&gt;{#DN}&lt;/code&gt; → &lt;code&gt;$.ethpmDOMRxPwrStats.attributes.dn&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This turns every interface in the JSON into a discovered entity identified by its DN.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh338u8f11rhnnyvie0xa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh338u8f11rhnnyvie0xa.png" alt="Zabbix — discovery rule general configuration" width="799" height="443"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flh9b6b3fp815g4b5ayou.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flh9b6b3fp815g4b5ayou.png" alt="Zabbix — discovery rule JSONPath preprocessing step" width="800" height="201"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhjw69mve0ox8jkzjlfyv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhjw69mve0ox8jkzjlfyv.png" alt="Zabbix — discovery rule LLD macro mapping" width="800" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 The item prototype
&lt;/h3&gt;

&lt;p&gt;Inside the discovery rule, create an item prototype:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name:&lt;/strong&gt; &lt;code&gt;DOM stats - Rx dbm [{#DN}]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; &lt;code&gt;Dependent item&lt;/code&gt;, &lt;strong&gt;Master item:&lt;/strong&gt; &lt;code&gt;apic.optics.rx.raw&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key:&lt;/strong&gt; &lt;code&gt;apic.optics.rx.dbm[{#DN}]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type of information:&lt;/strong&gt; &lt;code&gt;Numeric (float)&lt;/code&gt;, &lt;strong&gt;Units:&lt;/strong&gt; &lt;code&gt;dbm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preprocessing:&lt;/strong&gt; two &lt;code&gt;JSONPath&lt;/code&gt; steps:

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;$.imdata[?(@.ethpmDOMRxPwrStats.attributes.dn=="{#DN}")]&lt;/code&gt; — pick this interface's object out of the array&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$[0].ethpmDOMRxPwrStats.attributes.value&lt;/code&gt; — extract the dBm value&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvirgn903avtiv2jxdywv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvirgn903avtiv2jxdywv.png" alt="Zabbix — item prototype general configuration" width="800" height="488"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0rlq5z1ye08rwhy5rtxb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0rlq5z1ye08rwhy5rtxb.png" alt="Zabbix — item prototype JSONPath preprocessing steps" width="800" height="238"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that's it. After the next discovery run, &lt;strong&gt;Latest data&lt;/strong&gt; fills up with one Rx power item per optical interface in the fabric, and Zabbix starts building the history that started this whole exercise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh16jqspplyq4ijytknn6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh16jqspplyq4ijytknn6.png" alt="Zabbix — Latest data showing per-interface Rx power items" width="800" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;Tx&lt;/strong&gt;, clone the whole chain (raw item → discovery rule → prototype), swap the URL to &lt;code&gt;ethpmDOMTxPwrStats.json&lt;/code&gt;, the cookie macro to &lt;code&gt;{$APIC.COOKIE.TX}&lt;/code&gt;, and &lt;code&gt;RxPwrStats&lt;/code&gt; to &lt;code&gt;TxPwrStats&lt;/code&gt; in the JSONPaths.&lt;/p&gt;

&lt;p&gt;The response also carries each module's own &lt;code&gt;hiAlarm&lt;/code&gt;/&lt;code&gt;hiWarn&lt;/code&gt;/&lt;code&gt;loAlarm&lt;/code&gt;/&lt;code&gt;loWarn&lt;/code&gt; thresholds, so you can build alerting on top of this — evaluating the value against those limits and firing triggers. I've kept that out of this article to keep it focused, but a finished version of exactly that (a status item computed in JavaScript preprocessing plus warning/critical trigger prototypes) is in the GitHub template.&lt;/p&gt;

&lt;h2&gt;
  
  
  The repo
&lt;/h2&gt;

&lt;p&gt;While building this, I went looking for an existing Zabbix template for Cisco APIC — and came up empty. So I made one and put it on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/jpokorny-cz/zabbix-cisco-aci-templates" rel="noopener noreferrer"&gt;github.com/jpokorny-cz/zabbix-cisco-aci-templates&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you'd rather skip the manual setup above entirely, you can just import the template — everything from this article is in there, plus a good deal more: fabric health score, fault counts by severity, controller status, CPU/memory/disk usage, threshold evaluation and triggers for the optics, and a companion template for Nexus switches in the Inter-Pod Network role. The cert and cookie scripts I linked are in there too.&lt;/p&gt;

&lt;p&gt;If you run ACI and Zabbix, give it a try — and if you extend it or find something broken, issues and pull requests are very welcome. And if you've solved ACI DOM monitoring some other way (streaming telemetry? something I missed?), I'd genuinely like to hear about it in the comments.&lt;/p&gt;

</description>
      <category>cisco</category>
      <category>aci</category>
      <category>zabbix</category>
      <category>networking</category>
    </item>
  </channel>
</rss>
