2 Aug 2026 · 9 min read

Infrastructure as code for SCADA: one repo, twenty sites, zero drift

scada · ignition · architecture · automation

You manage twenty remote sites — pump stations, cold stores, sub-plants, whatever they are — and each one runs the same gateway doing the same job. Someone in the front office asks for a small change: a new alarm on the outlet pressure, across the fleet. On most sites that request turns into three weeks of remoting into gateways one at a time, hand-editing each in the Designer, and signing it off. Then an audit six months later finds that site 14 never got the change, site 7 got a slightly different version, and nobody can say who touched the setpoint at site 11 or when.

None of that is a people problem. It's an architecture problem, and the software world solved it fifteen years ago with an idea called infrastructure as code. It has finally become possible on the plant floor, and this is the piece I wish someone had handed me the first time I ran a multi-site SCADA fleet.

What "infrastructure as code" actually means, in plant terms

Strip the buzzword and it's simple: your configuration lives in files, and those files are the single source of truth that builds the system — not a memory in someone's head, not a state trapped inside a gateway. You don't click twenty gateways into shape by hand. You write down what the site should be, once, and a pipeline makes every site match it.

CLICK-OPS · today

engineer

20 sites, 20 hand-configs. Drift by month three.

INFRASTRUCTURE AS CODE

one repo= truthapply →

One source of truth → 20 identical sites. No drift possible.

Same twenty sites, two philosophies. One is a stack of snowflakes; the other is a build you can reproduce.

If that sounds familiar, it should. It's the same instinct as a UDT: define the pump once, stamp out a hundred instances, change the definition and every instance follows. Infrastructure as code is that instinct applied to the whole gateway — the screens, the tags, the scripts, the connections — instead of just one data type. The two words that matter are declarative (you describe the end state, not the click-by-click steps) and reproducible (you can rebuild any site from the files alone, on a fresh gateway, and get exactly what you had).

Why it's suddenly high time

This idea isn't new. What's new is that the excuses ran out.

  • The crew change. The engineers who hold twenty sites' worth of undocumented config in their heads are retiring. When that knowledge is in a repository instead, it doesn't walk out the door.
  • Sites are multiplying. One plant you can run by hand. Twenty you cannot, and "we'll just be careful" is not a strategy that survives site number twenty-one.
  • Auditors and cyber teams now ask. "Show me who changed this alarm, when, and who approved it" is a normal question in 2026. With files under version control the answer is a commit. Without it, the answer is a shrug.
  • The drift tax is real money. Every site that quietly diverges from the others is a site that behaves differently in an incident, trains operators differently, and fails an audit differently. You are already paying for drift; you're just paying in surprises instead of in discipline.

What changed: your config became files you can actually manage

For thirty years the reason you couldn't do this was that SCADA config lived inside the gateway's own database, reachable only through the vendor's tool. There was nothing to put under version control.

Ignition 8.3 changed that by storing projects as structured files on disk — Perspective views, tags, named queries all as JSON, scripts as .py. I've written about what that shift means and why Ignition got there first, so I won't relitigate it. The point for us here is narrow and large at the same time: once the configuration is files, every technique the software world built for managing files becomes available to the plant floor. Infrastructure as code is the most valuable of them.

A case study: twenty edge sites and one central gateway

Here's the shape almost every multi-site operator has. Each remote site runs an Ignition Edge gateway with the same HMI and the same tag model, differing only in a handful of site-specific values. A central gateway aggregates them for dashboards and history. Let's build that as code.

One repository is the whole fleet

Everything about the fleet lives in one git repository. Roughly:

ignition-fleet/
├── projects/
│   └── site-hmi/                     # the shared project — written ONCE
│       ├── com.inductiveautomation.perspective/
│       │   └── views/Overview/
│       │       ├── view.json         # the HMI screen (components, bindings)
│       │       └── resource.json
│       └── ignition/script-python/
│           └── site/pumps.py         # gateway scripts (Jython 2.7)
├── tags/
│   └── PumpStation.json              # UDT definition — the data model
├── config/                           # gateway config: DB + OPC connections
├── sites/                            # the ONLY per-site difference
│   ├── edge-riyadh-01.yaml
│   ├── edge-jeddah-02.yaml
│   └── central.yaml
├── .gitignore
└── .github/workflows/deploy.yml      # the pipeline that ships it

The important line is projects/site-hmi/ — the HMI and logic exist exactly once. Twenty sites, one screen definition. Change it, and you've changed it everywhere, on purpose.

The template, and the small file that makes a site itself

The shared model — say a pump-station UDT — is a versioned JSON file. This is a trimmed shape of what Ignition writes to disk:

{
  "name": "PumpStation",
  "tagType": "UdtType",
  "tags": [
    { "name": "OutletPressure", "dataType": "Float8",
      "opcItemPath": "{DeviceAddress}/pressure/outlet", "historyEnabled": true },
    { "name": "HighPressureAlarm", "valueSource": "expr",
      "expression": "{OutletPressure} > {HighPressureSetpoint}" }
  ]
}

Notice {DeviceAddress} and {HighPressureSetpoint} — they aren't hard-coded. They're the parameters each site fills in. And a site is nothing more than its parameter file:

# sites/edge-riyadh-01.yaml
site_name: "Riyadh Pump Station 01"
gateway: edge-riyadh-01
device_address: "opc.tcp://10.20.1.5:4840"
tags:
  HighPressureSetpoint: 9.5     # bar — this site runs hotter
history_provider: central-historian
shared templateHMI · tags · logic(one git repo)+edge-riyadh-01.yaml=Riyadh gateway+edge-jeddah-02.yaml=Jeddah gateway+central.yaml=Central gateway
The template is written once. Each site is just its own small parameter file: an address, a name, a setpoint. Same idea as a UDT — one definition, many instances — but for the whole gateway.

That's the whole trick, and it's one an OT engineer already understands: the template is the UDT definition, and each sites/*.yaml is an instance. One definition, many sites — but now for the entire gateway, not just a tag folder. Onboarding site twenty-one is a new eight-line file, not a three-month project.

Shipping it: push once, deploy everywhere

You never touch a production gateway by hand again. You change the files, open a pull request, a colleague reviews the diff — the exact lines that changed to the alarm logic — and on merge a pipeline promotes it to every gateway over the Gateway Network.

# .github/workflows/deploy.yml  (shape, not gospel)
on:
  push: { branches: [main] }
jobs:
  deploy:
    runs-on: [self-hosted, ot-zone]     # runner lives INSIDE the OT network
    steps:
      - uses: actions/checkout@v4
      - name: Lint tags & views
        run: ./ci/validate.sh           # catch a broken binding before it ships
      - name: Deploy to every site
        run: |
          for site in sites/edge-*.yaml; do
            ./ci/render.sh "$site" | ./ci/push-to-gateway.sh
          done
          ./ci/render.sh sites/central.yaml | ./ci/push-to-gateway.sh
# the day-to-day, for the whole fleet:
git switch -c alarm/outlet-pressure
# ...edit the UDT in the Designer, which writes the JSON...
git commit -am "Add outlet high-pressure alarm to pump stations"
git push          # open a PR → review the diff → merge → all 20 sites update
git pushCI / CDbuild · testdeploygatewaynetworkedge · site 1edge · site 2edge · site …edge · site 20central gateway
One git push, one pipeline, every site updated the same way — edges and central together. The change that used to take three weeks of site visits is one review and one merge.

Three things just died, quietly and permanently:

  • Drift is now impossible. The gateways are rebuilt from the repo, so "site 14 is different" can't happen — there's one definition and it's the same one everywhere.
  • The missed site can't be missed. The loop covers the fleet; you don't remember twenty gateways, the pipeline does.
  • The audit answers itself. Every change is a commit with an author, a reviewer, a timestamp, and a deploy record. No extra paperwork — the paperwork is the workflow.

How Ignition actually helps here

To be concrete about which parts of Ignition carry this, because it's not magic and it's not complete:

  • Tags, Perspective views, and named queries are JSON; scripts are .py. That's what makes the whole thing diffable and reviewable in the first place.
  • The Gateway Network is the delivery road. It's how the pipeline pushes a built project to a remote edge gateway without someone driving to site.
  • Ignition Edge is the per-site runtime — a light gateway that runs the same project the central one does, which is exactly why one template can serve both.
  • config/ (database and OPC connections, security) is largely file-based too, so the connections travel with the project — while secrets do not, and shouldn't. You inject those at deploy time from a vault. More on that below.

The contrarian view

Here's the part that'll annoy the right people. Infrastructure as code is not IT wisdom the plant floor needs to humbly import. It's a discipline OT invented, perfected, and then threw in a skip.

For forty years a control system shipped with a drawing package: every I/O point, every interlock, revision-controlled, signed off, one source of truth that the panel was physically built to match. Change control was rigorous because getting it wrong hurt. Then HMI software got "easy." Drag a pump onto a screen, bind a tag, done — no drawing, no review, no record. It felt like progress, and in one sense it was. But we traded a disciplined source of truth for thousands of undocumented clicks, and the gap between what's drawn and what's actually running is the tax every plant now quietly pays.

Infrastructure as code isn't a new idea arriving from Silicon Valley. It's the drawing package coming home — except this version builds the plant instead of merely describing it, and it never gets out of sync because the plant is built from it.

And the reason it hasn't already swept the industry isn't technical. Ignition 8.3 shipped the capability; the tooling exists; the case studies exist. The honest blocker is that hand-configuration is billable by the hour and a one-command twenty-site deploy is not. A lot of integrator revenue is the three weeks of clicking this approach deletes. The technology has been ready longer than the business models that sell against it. Notice who's quiet about this, and ask why.

Where it bleeds — because I'm not selling you a pipeline

If I only told you the good parts, I'd be doing the vendor-slide thing. The real edges:

  • Visual diffs are still JSON, not pictures. A small change to a Perspective view reviews cleanly as a text diff. A big graphic refactor still needs someone to open the Designer and check it looks right. A proper visual-diff tool for views is the obvious missing piece.
  • Merge conflicts on shared views are painful. Two people editing the same screen produces JSON that doesn't merge gracefully. The fix is discipline — small changes, short-lived branches — not a tool.
  • Not everything is file-based yet. Some legacy resources still serialize to binary .bin (transaction groups, some reports); you .gitignore those and handle them the old way for now. 8.3 is a direction, not a finished migration.
  • Secrets need a grown-up story. Config travels with the project; the encryption keys must not. That means a vault and deploy-time injection, which is real work you have to get right before you scale this.
  • The pipeline glue is yours to build. Ignition ships the file format and the deploy road. The lint/test/promote/rollback pipeline you assemble yourself. Most teams underspend here, and it's the part that separates a demo from a fleet you trust. Good news: it all runs air-gapped — self-hosted git, an in-zone runner, an internal vault — so this is deployable even in a network with no internet.

The verdict

If you run a single plant with one gateway, infrastructure as code is a bigger hammer than your problem needs, and I won't pretend otherwise. Learn it when you have a reason to.

But if you run many sites — a fleet of edges reporting to a central gateway, or the same stack rolled out client after client — this stops being optional the moment you're honest about what hand-configuration is already costing you in drift, missed changes, and audit exposure. The capability is finally here: your configuration can be a file you review, version, and rebuild from, instead of a state trapped in twenty gateways and one retiring engineer's memory.

That's the whole pitch, and it's an old one. The plant floor wrote everything down and built to the drawing long before software did. Infrastructure as code is just the drawing package that builds itself — and for a control room, "the same thing, everywhere, on the record" was always the point.

Keep reading