Policy as Code: why manual checks do not hold infrastructure
Manual infrastructure checks rest on an agreement: change review works as long as the team has someone who remembers that resources must not be exposed externally and that service accounts do not need administrator rights.
As soon as the flow of changes grows or part of the work goes to a contractor, the agreement stops holding — the plan is applied faster than anyone can read the diff.
Failure points look predictable. On large changes, review becomes a formality: no one parses four hundred lines in a network description line by line, and that is exactly where open access appears.
Environments drift apart — what was approved for the test environment is applied in production with different rights and parameters.
Configuration drift accumulates unnoticed: console edits do not make it into the repository, and the next apply returns the infrastructure not to where it was left.
Policy as Code moves these rules from chat into code that checks the plan before apply. Sentinel and OPA describe prohibitions declaratively: mandatory labels, allowed regions, a ban on unrestricted network rules, access rights requirements.
The check is built into the pipeline, so rejected changes do not reach the environment — with a specific reason and a reference to the violated rule, not with a “redo it” message.
Next, we look at how this works in practice and what exactly keeps infrastructure within defined boundaries.
What changes after implementing automated checks
Policies on Open Policy Agent or Sentinel are described declaratively and connected to the pipeline between infrastructure description and its apply.
Therefore, a prohibition triggers at the change planning stage, not at deployment time: the engineer sees the reason for rejection in the output of an ordinary command and fixes the configuration before it touches the production environment.
- Change review time. Manual analysis, which took a reviewer from half an hour to several hours, is replaced by running policies against the plan — the verdict arrives together with the build result, usually within minutes.
- Share of rejected changes. About a third of requests do not pass the check automatically: ports exposed externally, public storage, missing labels, overly broad rights. These edits simply do not reach the environment.
- Classes of failures that go away. Non-compliance with security policy, discrepancies between environments, changes made manually bypassing the configuration, and gradual privilege creep.
- Share of incidents. Incidents caused by configuration stop being the main cause of failures, and analyzing each case takes less time — it is clear which policy and which change allowed it.
- Release acceleration and downtime. The recovery window during rollback shrinks from hours to minutes: the configuration is reproducible, and the previous state is deployed in the same way as the current one.
Checks do not replace review but remove the mechanical part from it: a person looks at architectural decisions, not at a list of open ports. The policy set expands gradually — starting with prohibitions on critical items and adding rules as failures accumulate.
Approaches to infrastructure control: comparing options
The choice of option determines at which step a violation will be caught.
A rejected plan is fixed in minutes, while rolling back an already applied change affects live resources, so we compare options by interception point and maintenance burden, not by the “power” of the rules language.
| Option | When it fits | Limitations |
|---|---|---|
| Platform built-in policy engine (Sentinel) | The entire landscape is on one platform, and checks are needed between plan and apply | Rules are tied to the platform: when migrating, they must be described anew, and the language’s expressiveness is limited by the platform’s tasks |
| External policy engine (OPA) | Multiple platforms and environments, one rule must work everywhere, including clusters and build pipelines | Requires its own policy delivery and versioning layer, and plan state is passed in a separate format |
| Repository checks | Feedback is needed before contacting the platform: naming conventions, mandatory tags, module structure | They see source code but not the computed plan values — some violations surface later |
| Manual review | Rare changes and an early stage, when rules have not yet settled | Does not scale: as the number of teams grows, approval time and the share of missed violations grow |
A working setup is usually assembled from a combination: fast checks in the repository cut off the obvious, the built-in engine checks the plan before apply, and the external one holds common rules for several environments. Next, we look at how to assemble such a combination without duplicating rules between layers.
Stages of Policy as Code implementation: from audit to handover
The order of stages here is not a formality: if you enable blocking immediately, the build pipeline will stop at the first merge, and the team will return to manual edits.
Therefore, we first describe the current state, then formulate rules in warning mode, and only after testing on a staging environment do we switch them to blocking.
- Audit of repositories and states. We compile a list of root modules where state is stored, who makes changes and how often, and which resources are created outside the common template. The output is a map of risk zones: open ports, public storage, resources without tags and an owner.
- Policy schema. We classify rules into three groups: mandatory (security and access), organizational (naming, tags, regions), and hygiene (ban on outdated resource types). For each rule, we record its scope and level — blocking or warning.
- Binding rules to Terraform stages. Some checks run against the plan before apply — they catch an error before resources are created, some after apply, and some during review. This way the developer gets feedback in seconds, not after deployment.
- Mode configuration. We enable blocking mode only for rules with zero false positives; the rest are logged. We formalize exceptions explicitly — with justification, an owner, and a review deadline, so that permanent workarounds do not accumulate.
- Testing on staging. We run the policies against a copy of real configurations: count the share of triggers, analyze disputed cases, and measure how much check time increased. After that, the rules are moved to the common environment based on an individual calculation. Handover. We hand over the repository with policies, a schema of their structure, the procedure for changing and adding rules, and instructions for developers. Next, the customer's team maintains the rules itself — edits go through the same review as code.
Implementation artifacts: policy library, schemas, regulations
Policy as Code implementation is tested not on launch day, but at the moment when a rule needs to be adjusted and its author is no longer on the project.
Therefore, we hand over not a configured tool, but a working system — a set of artifacts that your team can use to change rules on its own, without us.
- Policy library — Sentinel and Rego rules organized by domain: network, encryption, tags, resource limits. Each rule has a description, an example trigger, and its own test.
- Dependency schema — a map of modules and outputs: what reads others’ values, where shared state is, which edits affect neighboring components and require simultaneous changes.
- Access matrix — who initiates a change, who approves it, for which resources manual approval is required, and what passes automatically based on check results.
- Change procedure — the rollout order for a new rule: first warning mode, then soft prohibition, then hard, with a run against historical configurations before enabling.
- Developer guide — how to run policies locally before sending code, how to read a rejection output, and what to do about a false positive.
- Exception catalog — formalized deviations with an owner and expiration date, so that a rule is not disabled by a code edit in a hurry.
- Documentation — the entry point: where rules are stored, how to add your own, what test coverage threshold is considered normal, and whom to ask with a question.
Separately, we hand over the log of the first runs: you can see which rules triggered most often and where the team adjusted configurations for them — this is the best training material for new engineers.
Case study: from manual checks to automatic change control
The environment was several hundred repositories with infrastructure descriptions, maintained by more than ten teams.
Every change went through manual review: the reviewer compared the configuration against an internal regulation using a checklist that lagged behind the regulation by months.
Some violations — externally exposed storage, lack of encryption, edits in protected environments — went into work and surfaced only after apply.
The rules were described as code on Open Policy Agent and moved to a separate repository with versioning and tests: each rule includes configurations that it must pass and must block.
The check was built into the pipeline at the plan stage — before apply, so the author of a change immediately sees which rule and which line stopped it.
A new rule is not written “for the future”: first an incident or security requirement is recorded, then the rule is run against historical configurations, and only then is it enabled in blocking mode.
At first, some rules ran in warning mode — this is how false positives were cleaned up without stopping teams.
Blocks before apply grew from isolated cases to 95% of all detected violations. The share of change rollbacks caused by a configuration error fell from 12% to 3%.
The review time for one change decreased from 40 minutes of manual review to 3 minutes of machine review, while the number of rules grew from 15 to 60, and the number of manual checklist items fell by a factor of three.
How does a built-in policy engine differ from an external one?
A built-in engine comes with the tool: rules are written in its own language and checked directly at the change planning stage.
An external engine is a separate component that receives an infrastructure description in machine-readable form and issues a verdict based on a common set of rules. They differ along four axes: rules language, policy portability, check speed, and maintenance.
| Criterion | Built-in engine | External engine |
|---|---|---|
| Rules language | Proprietary, knows the objects of one tool | General-purpose declarative, unified for all checks |
| Policy portability | Work only inside their own tool | One set covers infrastructure and application manifests |
| Check speed | Verdict in the same process, without network calls | Separate service: a network call in exchange for independent scaling |
| Rule maintenance | At the pace of tool updates | Its own schedule, but you maintain the rule registry and tests yourself |
Both options are Policy as Code: rules live in a repository, go through review, and are checked by automated tests.
A built-in engine covers fast risks of one environment — rules about environments, tags, and allowed resource types are written in hours and do not require a separate service.
The external one wins when checks are needed across several tools at once. We usually start with the built-in one, and move common rules to the external engine — with a registry, versioning, and a test for each rule. The check remains fast, and policies do not have to be rewritten when the tool changes.
Technical concerns: limitations and common implementation mistakes
Implementing the rules themselves is rarely difficult — the difficulty is in how a policy behaves when a team is working under a deadline and in several environments at once. Below are the questions that most often come up when handing the solution over to operations.
What if a rule blocks a normal change?
We divide rules into warning and blocking, and we enable the latter only after running them against the change history in the repository — you can immediately see what share of triggers is false.
A rule is formulated around an invariant (an open port, disabled encryption), not around code formatting: a style check breaks on any refactoring and gets disabled by the team itself within a month.
What to do during an incident if a rule prevents an urgent fix?
The bypass path is designed in advance, not invented at the moment of failure: a separate role with a short lifetime, a mandatory reason, a trail in the log, and an automatic task to bring the configuration back to standard.
A policy without a standard bypass gets disabled entirely — that is the worst outcome, because no control remains at all.
Can rules be reused between development and operations?
The common library covers what is the same everywhere: resource tags, encryption, network segmentation. Differences are moved into per-environment overrides — in development, a soft mode and relaxed limits; in operations, blocking.
Inheritance in Terraform is built through separate root modules per environment: otherwise, a relaxation added for debugging quietly makes its way into the production environment.
Let's discuss the task: where to start with implementing checks
So that implementing policy checks does not remain a demo on a test repository, we start by analyzing the current process.
It is important for us to see how an infrastructure change is born today: who prepares resource descriptions in Terraform, who approves, and at which step an error usually surfaces — during review or already in the cloud after apply.
To start, we need the following from you:
- a link to the repository with infrastructure code and examples of typical changes over recent cycles;
- a list of requirements that are currently checked manually: naming, tags, network access, encryption, regions;
- roles and approval rules: who initiates a change, who approves, who has the right to make an exception;
- a description of environments and differences in apply modes;
- access to the version control system and to the environment where checks will run;
- contact details of the engineer responsible for infrastructure who confirms disputed rules.
In response, you receive an assessment and work plan: a list of rules with priorities, a scheme for connecting them to the change process, the scope of the pilot environment, and timelines.
We keep the pilot environment narrow — one storage and 5-8 rules in warning mode, so the team gets used to signals without blocking releases. We enable blocking mode after the rules stop producing false positives.
Handing over documentation is part of the work, not the finale: a description of each rule, the reason it appeared, and the procedure for analyzing a trigger remain with your team. Describe the task — we will come back with an assessment and a plan.







