Product

CloudThinker × Terraform: Day-2 Operations for the Full IaC Lifecycle

Terraform plans only run when someone opens an MR, so drift from autoscalers and console hot-patches builds up unseen. This post walks a real drift run (Aurora ACU ceiling, EFS throughput and archive, EKS desired_size), shows how CloudThinker reads state, live API and telemetry before deciding who owns each field, and ends with one reviewed reconciliation MR.

12 min read
Cover Image for CloudThinker × Terraform: Day-2 Operations for the Full IaC Lifecycle

CloudThinker × Terraform: Day-2 Operations for the Full IaC Lifecycle

Most Terraform programs don't fail at plan. They fail in the months after.

The first apply is easy. A platform engineer writes a clean module, the pipeline runs, the resources come up green. Then time passes. The cluster autoscaler edits node-group sizes. A database engineer lowers an Aurora capacity ceiling in the console during an incident because the on-call can't wait for a Merge Request. A file system provisioned in one throughput mode turns out wrong for the workload and gets switched by hand. The state file stops describing production, and the next plan either overwrites someone's hot-patch or quietly carries the drift forward.

This is the Day-2 gap. This post walks through why it opens, and how CloudThinker treats Terraform as a continuous lifecycle rather than a one-shot deploy, carried in the team's existing chat, code review and ticketing tools.


Why the state file drifts away from production

None of this is a Terraform flaw. Terraform does exactly what it says: it compares code, state and the cloud when someone runs it. The gap is everything that happens between runs.

Plans only run when someone opens an MR. If a workspace hasn't been touched in six weeks, nobody has asked Terraform what changed in six weeks. Drift accumulates silently and surfaces all at once, usually in the middle of an unrelated change.

The console is faster than the pipeline. During an incident, the shortest path is the AWS console. That's often the right call at 2 a.m. The problem is that nobody writes the change back to HCL afterward, so the next apply reverts the fix.

Some fields were never Terraform's to own. Autoscalers, managed upgrades and AWS-managed defaults change fields legitimately. Terraform will fight them on every plan unless someone tells it not to.

Reviewing a drifted plan takes domain knowledge. "Aurora max_capacity 8 to 4" is a one-line diff. Whether to accept it depends on utilization history, cost and who changed it and why. A plan doesn't carry any of that.

Knowledge stays in two heads. Which fields to ignore, which drift is expected after an EKS upgrade, which module the platform team prefers: that usually lives with two senior engineers.


Where CloudThinker meets Terraform

01Code and cloud

IaC estate

  • GitLabGitLab or GitHubHCL, modules, Merge Requests
  • AWSAWS OrganizationsLive resource state and metrics
  • KubernetesAmazon EKSNode groups, addons, autoscaler
  • Terraform stateThe last recorded intent

Nothing installed in the cluster

02Lifecycle

CloudThinkerCloudThinker

  • Plan reviewPolicy and per-resource analysis
  • Drift detectplan -refresh-only on a schedule
  • ReconcileCode forward, cloud back, or ignore
  • Right-sizeCapacity diffs from utilization

Runs in a sandbox

03Where the team works

Chat and MR queue

  • SlackSlack or TeamsDashboard and summary per run
  • GitLabMerge RequestOne reconciliation MR per run
  • ApproverA human on the merge button

Same pipeline as every change

CloudThinker reads the repository, the live cloud and telemetry, and hands changes back as Merge Requests.

CloudThinker plugs into the IaC repository, the cloud accounts and the team's chat workspace, and works across the full lifecycle of every Terraform resource:

  • Author. When a developer drafts new HCL, CloudThinker proposes the approved module from the platform team's catalog before the resource is hand-rolled. Tags, IAM and observability defaults come with it.
  • Plan. Every Merge Request goes through policy-as-code review and a per-resource analysis (database resources, EKS workloads, security posture) before the human reviewer sees the diff.
  • Apply. The apply goes through Auto Mode, so you decide what runs on its own and what waits for approval.
  • Drift detect. On a schedule, CloudThinker runs terraform plan -refresh-only against the live cloud and produces a drift dashboard.
  • Reconcile. When drift is intentional, CloudThinker opens a Merge Request that brings the code forward. When it's accidental, it proposes the inverse.
  • Right-size. Database capacity, EKS node groups and storage throughput are evaluated against actual utilization, and proposed HCL diffs land in the queue.
  • Deprecate. When a resource has been idle, CloudThinker proposes a terraform destroy plan and tags the change for a human approver.

Every step runs in CloudThinker's Sandbox, under the Guardrails Engine, and connects through CloudThinker Connections to GitLab, GitHub, AWS Organizations, Amazon EKS and the team's identity provider. Nothing is installed inside the customer cluster.


A drift run, end to end

01Detect

Refresh-only plan

Asks where live has moved away from state, without proposing changes to live.

02Evidence

Three sources

State, live API and telemetry for each drifted resource.

03Decide

Who owns the field

Adopt live, restore code, or hand the field to the runtime.

04Approved

Reconcile MR

One MR per run, each hunk linked to its evidence.

05Close

Clean baseline

The next run starts from state that matches production.

Every reconciliation is remembered, so the next matching drift arrives with the earlier decision attached.

The screenshot below is what an on-call engineer sees at the end of a scheduled drift run: which resources drifted, how severe, and what was opened as a result. The depth lives behind it.

CloudThinker drift dashboard for a production workspace. Header tiles summarize drifted resources by severity, the open reconciliation Merge Request, and detection coverage gaps. A donut chart breaks drift down by severity, and a bar chart shows detection coverage for AWS-native resources, EKS addons, and the VPC module.
CloudThinker drift dashboard for a production workspace. Header tiles summarize drifted resources by severity, the open reconciliation Merge Request, and detection coverage gaps. A donut chart breaks drift down by severity, and a bar chart shows detection coverage for AWS-native resources, EKS addons, and the VPC module.

1. Ask where live has moved. CloudThinker runs terraform init inside the sandbox, then a refresh-only plan against the live AWS account:

terraform plan -refresh-only -out=drift.tfplan
terraform show -json drift.tfplan \
  | jq '.resource_drift[] | {address, actions: .change.actions}'

The -refresh-only flag is the safe way to ask "where has the cloud moved away from the state file?" It compares state with live and proposes no changes to live resources. In this run, four resources drifted: one high severity, two medium, one low.

2. Read each drift against its domain. Each one gets the evidence a reviewer would ask for (utilization figures here are illustrative):

  • Aurora max_capacity 8 to 4 ACU (high). Someone lowered the Serverless v2 ceiling. CloudWatch shows peak usage under 3 ACU for 30 days, so 4 looks deliberate and safe. It could also be a mistake during an incident. CloudThinker can't know intent, so the MR says so: confirm 4 ACU is the intended ceiling before merge; if 8 was intended, revert that hunk instead.
  • EFS throughput_mode bursting to elastic, plus a 60-day archive lifecycle (medium). The two changed together, which fits: EFS Archive storage needs Elastic throughput. Access patterns support it. Adopt live.
  • EKS node group desired_size (medium). The autoscaler changed it, and will again. Reconciling the number would fight the autoscaler on every plan. This one gets a different answer, covered below.

3. Surface what it couldn't see. Coverage gaps go in the report rather than under the rug. In this run, EKS addons were blocked on authentication (no access entry for the drift role yet) and the VPC module failed on an evaluation error. The dashboard shows both, and the follow-up proposes the access entry so the next run covers Helm and addon drift.

4. Open one reconciliation MR. For drift you want to keep, the MR updates the HCL to match live, and the state catches up with terraform apply -refresh-only, which writes live values into state without touching infrastructure. For accidental drift, the MR leaves the code alone and a normal apply puts the cloud back. Every hunk links to its detection, telemetry and policy check:

resource "aws_rds_cluster" "orders" {
  # ...
  serverlessv2_scaling_configuration {
    min_capacity = 0.5
    max_capacity = 4 # was 8; lowered in console, confirm intent before merge
  }
}

resource "aws_efs_file_system" "exports" {
  throughput_mode = "elastic"

  lifecycle_policy {
    transition_to_archive = "AFTER_60_DAYS"
  }
}

5. Post and wait. A summary lands in Slack or Microsoft Teams with the dashboard, the MR link and the open questions. The team reviews and merges, and the next scheduled run starts from a clean baseline.


Reconciling against the right source of truth

01Intent

Terraform state

What the last apply recorded. A memory, and the lowest authority of the three.

02Configuration

Live cloud

What the AWS API reports right now, including console hot-patches.

03Behavior

Telemetry

What the workload actually does: utilization, throughput, scaling history.

When they disagree, the recommendation comes from reading all three, not from re-applying the state.

The same resource gets a different answer from each system that observes it. Terraform state remembers the last intent. The live cloud reports the current configuration. Telemetry records what the workload actually does.

When the three disagree, state has the lowest authority: it's a memory of intent, not a measurement. CloudThinker reads all three before recommending a reconciliation:

  • Database resources are checked against state, the live RDS or Aurora API, and CloudWatch metrics before recommending whether a capacity ceiling goes up or down.
  • Storage resources are checked against state, the live cloud and the workload's access patterns before recommending a throughput-mode or lifecycle change.
  • Cluster workloads are checked against state, the live cluster and the autoscaler's history before recommending whether a field belongs in Terraform at all.

The output is a recommendation with the evidence attached, which a reviewer can accept, edit or reject. It's never just a number to merge.


Knowing when not to reconcile

Some fields belong to the runtime, not to IaC. An EKS node group's desired_size is the standard example: the cluster autoscaler edits it as workloads scale, and re-applying Terraform would fight it on every plan.

The right answer there is to take the field out of Terraform's hands:

resource "aws_eks_node_group" "workers" {
  # ...
  scaling_config {
    desired_size = 3
    min_size     = 2
    max_size     = 10
  }

  lifecycle {
    ignore_changes = [scaling_config[0].desired_size]
  }
}

The same logic applies to RDS minor versions with auto minor version upgrade on, EKS addon versions managed outside Terraform, and any other field an AWS-managed service or autoscaler owns by design. The MR explains the reasoning in a comment so the next engineer understands why the field is intentionally ignored. That judgment is what separates a domain-aware reviewer from a lint rule.


Why this beats the traditional workflow

Traditional Terraform workflowWith CloudThinker
Drift detectionDiscovered in the next unrelated planScheduled refresh-only runs with a dashboard
EvidenceA one-line diff with no historyState, live API and telemetry per resource
Runtime-owned fieldsFought on every applyMoved to ignore_changes with the reasoning
Console hot-patchesReverted by the next applyWritten back to HCL through a reviewed MR
Coverage gapsInvisibleReported with a fix for the next run
KnowledgeIn two senior engineers' headsStored with each reconciliation and reused

The deeper difference is that Terraform answers "what differs" and leaves "what should we do about it" to whoever is reading the plan. That second question needs utilization history, ownership and judgment about who owns each field. It's the work CloudThinker does before the MR reaches a reviewer.


Gated by design

CloudThinker reads freely (plans, state, live APIs, metrics) and posts the dashboard. Writes, from opening a reconciliation MR to applying a refresh-only plan, go through Auto Mode. A human always merges.

For teams that want the lifecycle without running the pipeline themselves, Managed Cloud Service 24/7 runs the same workflow under a managed on-call rotation.


From writing HCL to asking in chat

The change customers mention most is the team's daily habit. Developers who used to hand-write a resource block, and on-call engineers who used to patch through the console because the MR queue was too slow, now describe what they want in chat:

"I need an S3 bucket for the export pipeline: encrypted, tagged for the billing team, with a 90-day lifecycle to Glacier."

CloudThinker proposes an HCL diff using the platform team's approved S3 module, posts it in the same channel and opens a draft MR. The developer reviews, the platform engineer merges, and the bucket lands through the normal pipeline. The console hot-patches stop because the conversational path is faster than opening a tab.

CloudThinker also remembers. Every reconciliation, incident fix and approval is stored with its reasoning and outcome. When a similar pattern shows up (a drifted resource like one reconciled before, an addon authentication error that matches a past incident), the proposed MR arrives with a note like "this pattern was reconciled previously; reapplying the same fix", linked to the original case.


A typical week

Beyond scheduled drift runs, the same path (ask, propose, review, apply) covers the rest of the week:

An incident. An Aurora cluster crosses a CPU threshold. CloudThinker correlates the alert with recent metrics, proposes a capacity diff against the Terraform module and opens an MR linked to the incident ticket. The on-call approves, and the next maintenance window applies it.

A scheduled cost and posture review. Weekly or monthly, CloudThinker reports cost waste, right-sizing candidates, security posture changes and policy drift across every connected account. Each finding the team accepts arrives as an HCL diff in the MR queue, with no separate console to bounce between.


What changes for the team

  • IaC stops going stale. Scheduled drift runs give the platform team a daily check on every workspace, turning "is the live cloud what we say it is?" from a quarterly audit into a continuous signal that catches issues before they become incidents.
  • Drift becomes routine, not an incident. Reconciliation MRs land between standups. The on-call no longer discovers drift in the middle of a P1.
  • The repository becomes the source of truth in practice. Plans go from "many changes I didn't make" to "the plan I'm proposing." Reviewers can trust the diff.
  • Day-2 conversations move from blame to architecture. Right-sizing, lifecycle policies and module reuse become a running discussion in the MR queue.
  • Cost optimization arrives as Terraform diffs. Idle resources, oversized capacity and missing lifecycle policies come back as HCL in the next reconciliation MR, not as advice in another console.
  • Security posture is checked on every plan. CIS, SOC 2 and PCI guardrails run against the proposed plan before the apply is gated. A public S3 bucket, a 0.0.0.0/0 ingress or a plaintext secret never reaches the state file.
  • Recurring reports run themselves. Drift summaries, compliance evidence, cost reviews and posture reports are generated on the team's schedule and delivered to the audit folder, the leadership channel or the SOC 2 Type II portal.
  • Audit-ready change history. Every MR carries the dashboard, telemetry, policy report and reasoning, the same artifact pattern used during SOC 2 Type II audits.

"We leverage CloudThinker to re-organize and standardize our AWS Landing Zone infrastructure, so we can automate 80% of daily operation tasks with CloudThinker."

Tung Nguyen, Infrastructure Leader, F88


Getting started

The fastest path to a first drift run is three steps.

1. Connect the IaC repository and the AWS Organization. CloudThinker Connections has first-party integrations for GitLab, GitHub, AWS, Azure, GCP and Amazon EKS, and reaches the cloud through the identity provider the team already uses.

2. Inventory your Terraform estate. From Slack, Microsoft Teams or the CloudThinker chat:

"Inventory every Terraform workspace I have connected. Give me a per-region resource breakdown and flag any state that hasn't been refreshed in the last 24 hours."

3. Watch production for drift, propose only, so the team sees the dashboard before any reconciliation MR is opened:

"Run a Terraform drift check on production every hour. Post the dashboard to #platform-ops. Propose only: don't open reconciliation MRs yet, I want to review the first few runs by hand."

When the team is ready, let that one action run on its own:

"For the production drift check, open one draft reconciliation MR per run without asking first. Attach the dashboard and evidence to the MR body. Merging still needs human review."

Full reference at docs.cloudthinker.io.


Related reading


Conclusion

Terraform was never the problem. The lifecycle around it was. With author, plan, apply, drift detect, reconcile, right-size and deprecate handled in the team's chat and code review, gated by Auto Mode and bound to the Guardrails Engine and CloudThinker's Sandbox, the IaC repository goes back to being the source of truth it was meant to be.

To see the lifecycle running against your own estate, visit the CloudThinker Platform, explore the documentation, or book a discovery call.

Steve Tran, CTO, CloudThinker

terraformiacdriftdetectionfinopsawscloudthinkermanagedcloud