Compliance for cloud engineers · Lesson 4 of 7 · about 25 minutes

Evidence without the pain: Git, Terraform and GitOps

Why the tools engineers already use produce the change-management evidence auditors want, almost for free: Git history, pull request reviews, Terraform plans and drift checks, and GitOps keeping production matched to the code. Plus how to collect evidence automatically, every day.

By the end you'll know

  • The questions auditors ask about every change, and where the answers already live
  • How to read Git history as evidence of who changed what, and when
  • What separation of duties means, and how branch protection enforces it
  • How Terraform plans and drift checks prove production matches the agreed baseline
  • How GitOps and scheduled jobs turn evidence into something that collects itself

Where you are

Lesson 3 named change management, CM-2 and CM-3, as the family where engineers have the biggest advantage. This lesson shows why. Teams that work through Git, pull requests, Terraform and GitOps already produce most of the evidence an auditor wants for change control, as a side effect of working normally. The trick is knowing where it is, and closing the few gaps that tools don't close for you.

A bank statement, not a shoebox of receipts

Imagine proving where your money went last year. One way is a shoebox of paper receipts, some missing, some faded, sorted the night before. The other is your bank statement: every transaction, automatically recorded, with the date, the amount and who it went to, and nobody could quietly edit it afterwards.

Manual change evidence is the shoebox: screenshots, spreadsheets updated by hand, emails that say "approved", gathered in a rush before the audit. Git is the bank statement. Every change is recorded as it happens, with who made it, when, exactly what changed, and, with pull requests, who reviewed and approved it. You don't collect this evidence; you just stop throwing it away.

What auditors ask about a change

For change control, an auditor picks a sample of changes to production, and for each one asks roughly the same six questions. Here's where each answer already lives:

The auditor asksWhere the answer lives
Who made the change?The commit's author
What exactly changed?The commit's diff, line by line
When?The commit and merge timestamps
Was it reviewed and approved by someone else?The pull request's reviews
Was it tested before it went live?The checks that ran on the pull request
Does production actually match what was approved?Terraform's plan, and GitOps sync status

The rest of this lesson takes those in order.

Git: who, what and when

Any Git repository is already a change log. Here's the repository for this website, the five most recent changes:

BASH
git log --format='%h %ad %an %s' --date=short -5

--format picks what to show for each commit: %h is its short ID, %ad the date, %an the author's name and %s the summary line. Run it in any repository you work in. Here's this site's:

OUTPUT
f9cdd96 2026-10-25 Mayowa Babatola Compliance Lesson 3: controls in engineering terms, with the control sorter game (#71)
c4ffe69 2026-10-25 Mayowa Babatola Compliance Lesson 2: the frameworks in job descriptions, and rename the scenario sections (#70)
5a0a7cd 2026-10-25 Mayowa Babatola Compliance track: new topic, and Lesson 1: what compliance is (#69)
407f15b 2026-10-25 Mayowa Babatola Networking Lesson 12: troubleshooting checklist, with the error decoder widget (#68)
d15547b 2026-10-25 Mayowa Babatola Networking Lesson 11: networking in Kubernetes, and fix the CivicGrid callouts in 7 and 10 (#67)

Who, when, and what in a sentence, with a pull request number to follow. An auditor usually asks about one file or system, so you can narrow it down. Add -- and a path to see the history of just one file:

BASH
git log --format='%h %ad %an %s' --date=short -3 -- content/resources.yaml
OUTPUT
5a0a7cd 2026-10-25 Mayowa Babatola Compliance track: new topic, and Lesson 1: what compliance is (#69)
2fae0cb 2026-10-24 Mayowa Babatola Networking track: 12 lessons, and publish Lesson 1 (#57)
7b82721 2026-10-24 Mayowa Babatola Publish Terraform Lesson 2 and add the loops and conditionals lesson to the track (#49)

Every change ever made to that file, by whom and when. And for any one change, git show --stat says exactly which files it touched:

BASH
git show --stat --format='%h %an %ad%n%s' --date=iso c4ffe69
OUTPUT
c4ffe69 Mayowa Babatola 2026-10-25 00:55:20 -0400
Compliance Lesson 2: the frameworks in job descriptions, and rename the scenario sections (#70)

 app/blog/mdx-components.js                        |   2 +
 app/components/FrameworkFinder.js                 |  89 ++++++++
 app/globals.css                                   |  18 ++
 content/lessons/compliance/the-frameworks.mdx     | 247 ++++++++++++++++++++++
 content/lessons/compliance/what-compliance-is.mdx |   2 +-
 5 files changed, 357 insertions(+), 1 deletion(-)

Drop --stat and you get every changed line. That's CM-3's "what exactly changed" answered to the character, and it's the same for code, Terraform and Kubernetes files, as long as they live in Git. Anything that changes outside Git is a gap in this record, which is the first case in Spot the problem below.

Pull requests: reviewed and approved

Git records who changed something. The pull request records the part auditors care about most: that someone else looked at it and agreed before it went live. With the GitHub command-line tool, you can pull a pull request's record as data:

BASH
gh pr view 71 --json number,author,mergedBy,mergedAt,reviews

It returns the author, who merged it, when, and the list of reviews: who approved, and when. Saved alongside the commit, that's a complete change record, straight from the source.

The principle behind it has a name: separation of duties, sometimes called the four-eyes principle. The person who writes a change shouldn't be the only person who approves it, so that no single person, by mistake or on purpose, can push something into production alone. Frameworks ask for it directly; in NIST 800-53 it's AC-5.

Git hosts can enforce it, instead of just hoping. Branch protection rules on the main branch can:

  • Require a pull request, so nobody pushes to the main branch directly.
  • Require at least one approval from someone other than the author before merging.
  • Require checks to pass, such as tests or a Terraform plan, which is the "was it tested?" answer.
  • Apply the rules to admins too, so the most powerful accounts can't quietly skip them.

The setting itself becomes evidence: a screenshot, or better, the branch protection rules exported as data, show the control is enforced, not just promised.

Terraform: the baseline, in writing

Terraform answers the questions Git can't on its own: what should production look like, and does it?

The Terraform files are your baseline configuration, CM-2 from Lesson 3: the agreed, approved state of the infrastructure, in writing. And terraform plan is a preview of a change before it happens, which makes it ideal evidence: many teams post the plan's output on the pull request, so the reviewer approves the actual change, not just the code.

Plan has one more trick that's worth knowing for compliance. With -detailed-exitcode, its exit code says whether production matches the code:

BASH
terraform plan -detailed-exitcode
echo $?

echo $? prints the exit code of the command before it. There are three possible answers:

Exit codeMeans
0Production matches the code. No changes.
1The plan itself failed, like a typo or missing credentials.
2Production and the code differ: something changed that the code doesn't know about, or a code change hasn't been applied.

A difference between the code and what's really running is called drift. Run that check on a schedule, alert on exit code 2, and you have a detective control for unapproved changes: anyone who changes production by hand in the cloud console shows up the next morning. Keep the daily results, and you have evidence that the baseline held, every day of the audit period.

GitOps: production that can't wander off

Argo CD, from the Argo CD tile, takes this one step further for Kubernetes. It constantly compares the cluster with Git, shows whether each app is Synced or OutOfSync, and keeps a history of every sync: which commit was deployed, and when.

With self-heal turned on, it goes further still: if someone changes something in the cluster by hand, Argo CD notices it no longer matches Git, and puts it back. That turns CM-3 from a rule people follow into a rule the system enforces. The only way to change production is to change Git, and the only way to change Git is a reviewed pull request.

Every step leaves a recordA change moves left to right: commit, pull request, checks, merge, deploy. Each step leaves evidence. The commit records who, what and when. The pull request records the review and approval. The checks record tests and the Terraform plan. The merge records who merged it and when. The deploy, through Argo CD or Terraform apply, records which commit is live, and a drift check confirms production still matches.CommitPull requestChecksMergeDeploywho, what,and whengit logreviewed andapproved by anotherreviewstested, and thechange previewedterraform planwho merged it,and whenmergedBywhich commit is live,and no drift sincesync historyWork through the pipeline, and the evidence collects itself.
A change through Git, pull requests, checks and GitOps leaves evidence at every step. A change made by hand in the console leaves none.

Evidence that collects itself

The best evidence isn't gathered before an audit; it's gathered every day, by a machine, whether anyone remembers or not. Remember the SC-8 check from Lesson 1? Here's the same idea as a scheduled job in GitHub Actions, to read rather than run:

YAML
name: daily-evidence
on:
  schedule:
    - cron: "17 6 * * *"
jobs:
  sc8:
    runs-on: ubuntu-latest
    steps:
      - name: Check encryption in transit
        run: |
          {
            echo "Control: SC-8"
            echo "Checked: $(date -u)"
            echo "Plain HTTP: $(curl -s -o /dev/null -w '%{http_code}' http://api.example.com/)"
            echo "HTTPS: $(curl -s -o /dev/null -w '%{http_code}' https://api.example.com/)"
          } > evidence-sc8.txt
      - uses: actions/upload-artifact@v4
        with:
          name: evidence-sc8
          path: evidence-sc8.txt

Every morning at 06:17 UTC, it runs the same check and keeps the result as a stored file, from the cron schedule in Python Lesson 8. Add the Terraform drift check as a second job, and a few more like it, and an audit becomes "here are the daily results for the whole period" instead of a week of screenshots. It's also where the industry is heading: FedRAMP 20x, from Lesson 2, is built around exactly this kind of automated, continuous evidence.

In my real setup

Every change to this website and to CivicGrid's cluster goes through Git and a pull request, and Argo CD deploys the cluster from Git, so most of the change record already exists. But look at the Git output above honestly: every author is me, and I merge my own pull requests. There's no second reviewer, because I work alone. An auditor would mark that as a separation-of-duties gap, AC-5, and they'd be right. Solo projects can't fully close it; what they can do is add compensating controls, like required automated checks before merging and a drift check, and say plainly where the gap is. Lesson 7 does exactly that.

Spot the problem

Three changes that leave the record weaker than it looks. Work out the gap before you read the explanation.

The quick fix in production. At 2am an app is failing, and an engineer fixes it with kubectl edit, changing a setting directly in the cluster. It works. Nobody updates Git. What's wrong? The change has no pull request, no review and no record, so CM-3 wasn't followed, and the next audit sample might land on it. Worse, with Argo CD self-heal, the fix is quietly undone at the next sync, and the outage comes back. Emergencies happen, so most teams allow an emergency change process: fix it by hand if you must, then put the same change through Git straight afterwards, with the incident linked.

Approved in 30 seconds. Every pull request in a team's repository has an approval. An auditor looks closer: most were approved by the same person, within a minute of being opened, on pull requests hundreds of lines long. What's the problem? The approvals are there, but they don't show a real review: this is "rubber-stamping". Separation of duties only works if the second person actually looks. Signs that a reviewer did, like comments, questions and requested changes, are evidence too.

The console click. A team manages its cloud network with Terraform, and every change goes through a pull request. One evening, someone opens a firewall port in the cloud console to test something, and forgets to close it. A month later, the daily drift check is still returning exit code 2, and nobody's watching it. What failed? The baseline did its job, and the drift check found the change. But a detective control only works if someone acts on what it detects. An alert that nobody reads is the same as no alert at all. Drift should page someone, or open a ticket, every time.

Where you are now

You know the six questions auditors ask about a change, and where Git, pull requests, Terraform and GitOps already answer them. You know separation of duties, and how branch protection enforces it. You can use Terraform's exit codes to check for drift, and you've seen how a scheduled job collects evidence every day. Lesson 5 goes one step further: instead of only recording what happened, it checks the rules automatically before anything reaches production, with policy as code.

Check yourself

  1. Why is Git history good evidence for change management?

  2. What does separation of duties mean for a code change?

  3. terraform plan -detailed-exitcode returns 2. What does that mean?

  4. With Argo CD self-heal on, what happens to a change made by hand in the cluster?

  5. An engineer fixes production at 2am with kubectl edit. What should happen next?

  6. A drift check has returned exit code 2 every day for a month. What's the real failure?

Key terms

  • Change record: the evidence for one change: who, what, when, reviewed by whom, tested how.
  • Separation of duties (AC-5): no one person can make and approve a change alone. Also called the four-eyes principle.
  • Branch protection: rules on a Git branch that require pull requests, approvals and passing checks before merging.
  • Baseline configuration (CM-2): the agreed state of the infrastructure, written as code.
  • Drift: a difference between what's running and what the code says should be running.
  • Self-heal: Argo CD putting the cluster back to match Git when someone changes it by hand.
  • Emergency change: a fix made outside the normal process, then recorded through it straight afterwards.
  • Continuous evidence: evidence collected automatically, on a schedule, instead of gathered before an audit.

Go deeper