DEV Community

Cover image for I Added a Tag in AWS. Terraform Removed It Automatically.
Lalit Bagga
Lalit Bagga

Posted on • Originally published at blog.lalitbagga.com

I Added a Tag in AWS. Terraform Removed It Automatically.

I opened the AWS console, selected an EC2 instance, added a tag, and saved it.

The tag did not exist in Terraform. The deployed infrastructure now said one thing while the code said another.

That was the test.

My drift detector already knew how to find the difference and classify the EC2 update as LOW. The missing part was closing the loop: once the system understood that the change was low severity, it needed to restore the infrastructure without waiting for me to run Terraform manually.

So I added a separate remediation pipeline.

The Stack

Manual EC2 tag added in AWS
        ↓
Drift CodeBuild runs terraform plan
        ↓
SNS publishes the structured change
        ↓
Lambda classifies the update as LOW
        ↓
Remediation CodeBuild runs terraform apply
        ↓
Terraform removes the unmanaged tag
Enter fullscreen mode Exit fullscreen mode

Detection and remediation use different CodeBuild projects. The detector reads the infrastructure and reports differences. The remediation project has a separate IAM role because it can change AWS resources.

Step 1: Make EC2 Updates Eligible for Remediation

The classifier already grouped resource types into HIGH, MEDIUM, and LOW categories.

An EC2 instance belongs to MEDIUM_RISK_TYPES. Initially, that meant even a tag update was classified as MEDIUM and stopped before remediation.

The resource type alone was not enough. The action mattered too.

I updated the classifier so an in-place update on a MEDIUM resource becomes LOW, while creates remain MEDIUM and delete or replacement actions become HIGH:

def classify_change(resource_type, actions):
    if resource_type in HIGH_RISK_TYPES:
        return "HIGH"
    elif resource_type in MEDIUM_RISK_TYPES:
        if "delete" in actions or "replace" in actions:
            return "HIGH"
        elif "update" in actions:
            return "LOW"
        return "MEDIUM"
    else:
        return "LOW"
Enter fullscreen mode Exit fullscreen mode

For this test, Terraform reported the manually added EC2 tag as an update. That moved it into the LOW path.

Step 2: Keep Deletions Out of the Automatic Path

LOW did not automatically mean “start Terraform.” I also filtered out deletions:

low_changes_to_remediate = [
    change for change in classified["LOW"]
    if "delete" not in change.get("actions", [])
]
Enter fullscreen mode Exit fullscreen mode

This distinction matters. Updating an existing resource to match the code is different from recreating something that a person deliberately removed. Deletions stay in the manual-review path.

If an eligible LOW change remains, Lambda starts the remediation CodeBuild project:

if low_changes_to_remediate:
    codebuild.start_build(
        projectName=os.environ.get(
            "REMEDIATION_PROJECT_NAME",
            "terraform-drift-remediation",
        )
    )
Enter fullscreen mode Exit fullscreen mode

The Lambda role only needs permission to start that specific project:

{
  Effect   = "Allow"
  Action   = ["codebuild:StartBuild"]
  Resource = aws_codebuild_project.remediation.arn
}
Enter fullscreen mode Exit fullscreen mode

Lambda makes the routing decision. CodeBuild performs the repair.

Step 3: Run Remediation Separately

I did not add terraform apply to the existing drift detector. Detection should remain read-only even when remediation fails or is disabled.

The second build starts in a clean environment, installs Terraform, clones the Three-Tier repository, and applies the declared configuration:

version: 0.2

phases:
  install:
    commands:
      - curl -o terraform.zip https://releases.hashicorp.com/terraform/1.10.0/terraform_1.10.0_linux_amd64.zip
      - unzip terraform.zip -d /usr/local/bin
  pre_build:
    commands:
      - git clone https://github.com/lalitbagga/Three-Tier-Infra.git /tmp/Three-Tier-Infra
      - cd /tmp/Three-Tier-Infra
      - terraform init
  build:
    commands:
      - cd /tmp/Three-Tier-Infra
      - terraform apply -auto-approve -lock=false
Enter fullscreen mode Exit fullscreen mode

The remediation project uses its own CodeBuild role with write access to the AWS services managed by the Three-Tier project. During implementation, Terraform exposed several missing permissions as it evaluated and changed resources, so I expanded the experimental role to cover the project.

That role is intentionally separate from the detector. A job that only runs terraform plan should not inherit the permissions required for terraform apply.

The Repository Was Part of the Control Loop

One remediation run tried to include unrelated resource actions because CodeBuild was cloning a version of the Three-Tier repository that did not match the configuration I intended to operate.

The problem was not the classifier. Lambda correctly identified the triggering drift. The remediation build was applying the complete repository it cloned from GitHub.

I pushed the current Three-Tier configuration before running the test again.

This is easy to miss in CI. Terraform does not know what exists only on a developer's laptop. If CodeBuild clones GitHub, the committed branch is the configuration Terraform will enforce.

The Tag Disappeared

With the current repository in GitHub, I repeated the test:

  1. Opened the EC2 instance in the AWS console.
  2. Added a tag manually and saved it.
  3. Started the Terraform drift detector.
  4. The plan reported an update to the EC2 instance.
  5. Lambda classified the update as LOW.
  6. Lambda started the remediation CodeBuild project.
  7. CodeBuild ran Terraform apply.
  8. I returned to the EC2 Tags view and confirmed that the manually added tag was gone.

Terraform restored the infrastructure to what was declared in Git.

Git configuration: tag absent
AWS resource:       tag added manually
Terraform result:   tag removed
Enter fullscreen mode Exit fullscreen mode

The system was no longer only telling me about drift. For the LOW update I tested, it detected the change, classified it, and repaired it.

MEDIUM, HIGH, and deletion paths still stop for human review. That boundary is intentional. Automatic remediation is useful only when the system can distinguish a small correction from a change that deserves a person.

What Comes Next

The infrastructure is back in the expected state, but the evidence is spread across CodeBuild and CloudWatch logs.

The next part of the system will keep a remediation history in a database, expose it through an API, and visualize it in Grafana. I want to see what changed, how it was classified, whether remediation ran, and whether it succeeded without reconstructing the story from separate AWS services.

Detection found the tag. Classification decided it was LOW. Remediation removed it.

Now the system needs to remember that it happened.

Top comments (0)