Opsphere
← All articles

Terraform on AWS for DevOps: 5 Setup Checks Before Tracing Drift

Terraform on AWS for DevOps: 5 Setup Checks Before Tracing Drift

Geometric setup layers with a drift trace

Terraform is a practical, repeatable way to provision and manage AWS infrastructure as code. Start with a minimal terraform.tf that declares the AWS provider, then run terraform init and terraform plan to see what would change. Treat two things as priorities from day one: protect your state file and keep secrets out of version control.


TL;DR:

  • Before running Terraform, verify the AWS account and principal with aws sts get-caller-identity; use scoped IAM permissions and reserve CloudShell for short tasks.
  • Pin the AWS provider to ~> 5.92, commit the lock file, and apply the exact saved plan in automation to prevent version drift and unreviewed changes.
  • Remote state needs locking, backups, encryption, and restricted access; marking variables sensitive redacts output but does not remove secrets from state.
  • Import existing resources only after writing matching resource blocks, then review plans after each import because Terraform changes state but does not generate configuration.

Opsphere
Bring Cloud Operations Into One View
Opsphere unifies AWS, Kubernetes, observability, CI/CD, security and engineering context to help teams investigate incidents and understand infrastructure.

Table of Contents

What you need before running Terraform on AWS

You need a few things in place before the first command: an AWS account, an IAM identity with permissions for the resources you plan to create, the Terraform CLI, and the AWS CLI. HashiCorp’s AWS get-started tutorial walks through the same prerequisites and the same init, validate, plan, apply flow that we describe here.

  • AWS account and IAM identity: scope permissions to the services in your configuration. A learning sandbox can be broader. A shared account should not be.
  • Terraform CLI: install it, then run terraform version to confirm the binary is on your path.
  • AWS CLI: install it and run aws --version. Terraform’s AWS provider can read the same credential sources the CLI uses.
  • Credentials: use environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN for temporary credentials), a shared credentials file, or an IAM role attached to compute that runs Terraform.
  • Identity check: run aws sts get-caller-identity before anything else. It tells you which account and principal Terraform will act as, which prevents the classic mistake of applying to the wrong account.

Choosing where to run it

Your laptop works well for learning. AWS CloudShell is convenient for short tasks because it is pre-authenticated with your console session and ships with the AWS CLI, and the CloudShell CLI guide confirms that console credentials are forwarded to the session. AWS also states that CloudShell is meant for short-term work, not as a replacement for CI/CD, so production Terraform belongs in a pipeline with controlled credentials. If you work from an IDE environment, the AWS Cloud9 CLI tutorial shows the same AWS CLI setup.

Pro Tip: Run aws sts get-caller-identity as the first step of every pipeline job that runs Terraform. The output in your logs becomes a record of which identity applied which change.

How to configure the terraform block and AWS provider

Configuration starts with two blocks. The terraform {} block declares which provider Terraform needs and which Terraform versions are acceptable. The provider "aws" block sets how that provider connects to AWS.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.92"
    }
  }

  required_version = ">= 1.2.0"
}

provider "aws" {
  region = "us-west-2"
}

HashiCorp’s AWS tutorial uses this same shape, including the ~> 5.92 constraint and a Terraform CLI version of 1.2.0 or newer.

Why version constraints matter

The ~> operator is the pessimistic constraint. ~> 5.92 allows 5.92 and later 5.x releases but blocks 6.0. That gives you bug fixes and new resource arguments without a surprise major-version change in the middle of a deploy.

  • Pin the provider: an unpinned provider can change behavior between two runs of the same code, which makes plans hard to compare.
  • Set required_version: it stops a teammate with an older CLI from applying code that assumes newer features.
  • Commit the lock file: .terraform.lock.hcl records the exact provider builds selected during terraform init, so every machine and pipeline resolves the same versions.
  • Authentication stays outside the code: do not put access keys in the provider block. Let the provider read environment variables, a shared credentials file, or a role.

Providers and the registry

Terraform does not know AWS by itself. The AWS provider is a separate plugin, published in the Terraform Registry and developed in the open in the hashicorp/terraform-provider-aws repository. Each AWS resource type has a registry page with arguments, attributes, examples and import instructions. The aws_elb resource page is a typical example of what you will consult when writing resource blocks.

Statistic note: the HashiCorp tutorial sets its provider constraint to ~> 5.92, which means the AWS provider can float within the 5.x series but never jump a major version unattended.

What the core Terraform CLI workflow looks like

Almost everything you do with Terraform on AWS runs through five commands. The Terraform CLI overview lists them as the core subcommands, and each has a distinct job.

  1. terraform init downloads the provider plugins declared in your configuration and prepares the working directory. Run it first and again whenever you add a provider or change a backend.
  2. terraform validate checks syntax and internal consistency. It does not contact AWS, so it is cheap to run on every commit.
  3. terraform plan compares your configuration with the state and the real infrastructure, then prints a diff-style execution plan. Lines marked + will be created, - destroyed, ~ updated in place, and -/+ replaced.
  4. terraform apply executes the plan after you confirm. In automation, save the plan with terraform plan -out=tfplan and apply that exact file with terraform apply tfplan, so what was reviewed is what runs.
  5. terraform destroy removes everything the configuration manages. Use it for sandboxes and ephemeral environments, and read the plan before confirming.

Reading a plan like an operator

The summary line at the bottom, for example Plan: 2 to add, 1 to change, 1 to destroy., is where to start. A destroy or a replacement on a stateful resource such as a database or an EBS-backed instance deserves a pause. Check which argument forced the replacement; the plan marks it with # forces replacement.

Directory and workspace options

Use terraform -chdir=infra/network plan to run a command against a configuration in another directory without changing your shell location, which is handy in pipelines with several root modules. Workspaces, covered later, let one configuration hold several state instances.

Pro Tip: In CI, run terraform fmt -check and terraform validate as a fast first job, and run plan only after those pass. Failed syntax never needs an AWS call.

Why Terraform state needs protection

State is Terraform’s record of which real AWS objects correspond to which blocks in your code. HashiCorp describes it as the source of truth that must be protected and backed up in its remote state tutorial. Lose it or corrupt it, and Terraform loses track of what it manages.

  • Local state is a learning default, not a team setup. It lives on one disk, has no locking, and cannot be shared safely.
  • Remote backends fix this. HCP Terraform stores state versions, exposes them through an API, and locks the workspace when state is being edited or uploaded. An S3 bucket with DynamoDB for locking is the common self-managed alternative. The trade-off is operational: HCP Terraform gives you versioning and locking as a service, while S3 plus DynamoDB gives you control and more pieces to secure and maintain.
  • Locking prevents concurrent writes. Two applies running at once against one state can interleave and corrupt it. A lock makes the second run wait or fail.
  • Corruption usually has ordinary causes: an apply that dies partway, a hand-edited state file, or an import that was run against the wrong address.
  • Recovery starts with versions. Turn on bucket versioning for S3 backends, or use state versions in HCP Terraform, so you can roll back to the last good copy.
  • In CI/CD, give the pipeline role access to the state backend and nothing broader, and keep secret variables in the platform’s secure store rather than in the repository.

How to import existing AWS resources

Not everything in an AWS account started life in Terraform. The terraform import command maps an existing resource into state using an address and a resource-specific ID, as documented in the import command reference.

terraform import aws_instance.web i-abcd1234
terraform import aws_route53_zone.main Z0123456789ABCDEFGHIJ

For an EC2 instance, use its instance ID. For a Route 53 hosted zone, use its zone ID. Each resource’s registry page states which ID it expects.

  • Import changes state only. It does not write HCL. If your configuration has no matching resource block, or the block differs from reality, the next plan may propose to replace or modify the resource.
  • Write the block first, then import, then run plan until it shows no unexpected changes. Red Hat’s overview of infrastructure as code makes the same practical point: you must hand-write code that matches the imported state.
  • Modules and indexed resources need full addresses, such as module.app.aws_instance.web or aws_instance.web["blue"]. Quote them in your shell.
  • Pipelines can import too. Newer Terraform versions support import blocks in configuration, which lets a reviewed pull request carry the import and removes the need for an engineer to run a one-off command.

Pro Tip: Import one resource, run terraform plan, and fix the diff before importing the next. A clean plan after each import tells you the HCL matches reality.

How to handle sensitive variables and secrets

Declaring a variable as sensitive tells Terraform to redact its value in plan and apply output. HashiCorp’s sensitive variables tutorial shows the declaration:

variable "db_password" {
  type      = string
  sensitive = true
}

Redaction is about the screen, not the storage. Terraform still writes the value into state, so the state file needs the same care as a secret.

  • Supply secrets through environment variables (TF_VAR_db_password), a .tfvars file excluded from version control, or the secure variable store in HCP Terraform or Terraform Enterprise.
  • Prefer a secrets manager such as HashiCorp Vault where you can, so the value is fetched at run time and never lives in a repository.
  • Mark derived outputs as sensitive. An output built from a secret must carry sensitive = true, or Terraform will refuse to display it without the flag.
  • Encrypt the backend and restrict access. Enable encryption on the S3 bucket or use HCP Terraform, and allow only the pipeline role and a small admin group to read state.
  • Never commit plaintext .tfvars files that hold credentials.

Statistic note: the HashiCorp tutorial states that sensitive values are redacted in output but can still appear in the state file, which is why the backend, not the variable flag, is your real protection.

A minimal EC2 example from init to destroy

This walkthrough follows the pattern in HashiCorp’s AWS tutorial: a data source to find an image, a resource to launch it, and the commands to run. Use four files: terraform.tf (the blocks from earlier), main.tf, variables.tf and outputs.tf.

# main.tf
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]

  filter {
    name   = "name"
    values = [var.ubuntu_ami_name_pattern]
  }
}

resource "aws_security_group" "web" {
  name_prefix = "demo-web-"

  ingress {
    from_port   = var.admin_port
    to_port     = var.admin_port
    protocol    = "tcp"
    cidr_blocks = [var.admin_cidr]
  }
}

resource "aws_instance" "web" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = var.instance_type
  vpc_security_group_ids = [aws_security_group.web.id]

  tags = {
    Name = "terraform-demo"
  }
}

Define instance_type and admin_cidr in variables.tf, with an instance type appropriate for your account and experiment, and expose aws_instance.web.id in outputs.tf. Restrict admin_cidr to your own address.

  1. Run terraform init to download the AWS provider.
  2. Run terraform validate to catch typos.
  3. Run terraform plan. Expect a summary of Plan: 2 to add, 0 to change, 0 to destroy. for the security group and the instance.
  4. Run terraform apply and type yes to confirm.
  5. Check the instance in the EC2 console, confirming the terraform-demo tag.
  6. Run terraform destroy when you finish, so the sandbox does not keep billing.

How to investigate Terraform changes with evidence

Terraform tells you what it changed. It does not tell you what that change did to production. When an incident starts, the first question is usually “what changed recently?”, and the answer is spread across state, CI logs, cloud APIs and monitoring.

That gap is where we work. Opsphere is the operational intelligence layer that connects Terraform infrastructure-as-code context with cloud resources, Kubernetes, observability, deployments and engineering systems. We do not replace Terraform. Opsphere is read-only by default, so it helps teams investigate infrastructure changes with evidence across multiple systems without taking write actions. Our Terraform integration page covers the integration in detail, and our use case for Terraform-native teams describes the investigation patterns.

Questions worth asking during triage:

  • Which applies ran in the last few hours, and which commit and author were behind each?
  • Which resources show drift between state and what AWS reports now?
  • Did a failed apply leave resources half-created, and do alerts line up with them?
  • Which workloads sit behind the changed security group, instance or load balancer?

To make those answers queryable, prepare your pipeline:

  • Stamp every CI apply run with a run ID, commit SHA and actor.
  • Log the plan summary and apply result in a consistent format.
  • Keep state and run metadata readable by the tools that investigate.

Pro Tip: Emit the apply run ID as a resource tag where the resource supports tags. A tag on the instance that links back to the pipeline run turns “who changed this?” into a lookup.

How Terraform modules for AWS work

A module is a directory of Terraform files that you call from another configuration. It packages a pattern, such as a VPC with subnets or a standard EC2 service, behind a small set of input variables and outputs. HashiCorp’s AWS get-started series includes a tutorial on using modules.

Create a module by moving related resources into their own folder, declaring variables for what callers may change, and outputs for what callers need back. Call it with a module block that sets source and passes inputs. Local paths work for a single repository. For sharing across teams, publish modules to a private registry or a versioned Git tag and pin the version in the source or version argument, the same way you pin providers.

Keep modules narrow. A module that creates “everything for an application” becomes hard to change. A module that creates one coherent unit, such as an ALB with its target group, stays reusable. Expose only the inputs people need, and document defaults, because every exposed variable is a promise you maintain.

How workspaces and environments fit together

Workspaces let a single configuration keep several separate state instances. terraform workspace new staging creates one, terraform workspace select staging switches to it, and terraform.workspace returns the current name inside your code so you can vary tags or sizes.

They are useful for short-lived copies of the same stack, such as a feature environment. For long-lived environments with different accounts, permissions or approval rules, many teams prefer separate directories or separate root modules per environment, each with its own backend configuration and its own IAM role. The reason is blast radius: a mistaken apply in the wrong workspace is easier than a mistaken cd into the wrong directory with a different AWS account behind it.

Separate Terraform environments with isolated state boundaries

Whatever you choose, make the target obvious. Print the workspace and account ID in pipeline logs, give production its own credentials, and require a reviewed plan before apply. With HCP Terraform, workspaces are also the unit for variables, run history and access control, which makes environment boundaries explicit.

Common patterns for organizing AWS infrastructure

Good organization starts with splitting by lifecycle and ownership, not by resource type. Networking changes rarely, application stacks change daily, and data stores need extra caution. Separate root modules, each with its own state, keep a routine app deploy from touching the VPC.

Patterns that hold up on AWS:

  • Layered stacks: foundation (accounts, networking), platform (clusters, shared services), then applications, passing values between layers through outputs or data sources.
  • Consistent naming and tags: apply required tags such as owner, environment and cost center, ideally with the provider’s default tags.
  • Small state files: a smaller state means faster plans and a smaller blast radius when something goes wrong.
  • Code review for infrastructure: AWS’s DevOps guidance on infrastructure as code frames IaC as applying development rigor, with configurations kept in source control, which makes provisioning repeatable.
  • Tool fit: AWS’s IaC tooling guidance recommends aligning the tool with team skills and multi-cloud needs, and Terraform’s declarative style suits teams that want to describe desired state.

How Terraform handles creation, updates and deletion

Every plan classifies each resource as a create, an in-place update, a destroy, or a replacement. The provider decides which arguments can change in place and which force a new resource, and the plan output shows which applies.

When a replacement is risky, the lifecycle block gives you control:

  • create_before_destroy = true builds the new resource before removing the old one, which avoids downtime for things like security groups or launch configurations.
  • prevent_destroy = true makes Terraform error on any plan that would delete the resource, a sensible guard for databases and state buckets.
  • ignore_changes tells Terraform to ignore listed arguments that another system manages, such as an autoscaling group’s desired capacity.

Deletion deserves the same attention as creation. Removing a block from your code means Terraform will plan to destroy the real resource. If you want Terraform to stop managing something without deleting it, remove it from state with terraform state rm, then remove the block. Read destroy lines in every plan, especially in automated pipelines.

How to debug Terraform errors on AWS

Start with the error text. Most failures fall into a few buckets, and each points to a different fix.

  • Authentication errors (NoCredentialProviders, ExpiredToken): run aws sts get-caller-identity to see what identity your shell or runner actually has, and refresh temporary credentials.
  • Permission errors (AccessDenied, UnauthorizedOperation): the error names the denied API action. Add that action to the role, or confirm you are using the intended role.
  • State lock errors: another run holds the lock, or a crashed run left it behind. Confirm nobody is applying before using terraform force-unlock with the lock ID.
  • Drift and unexpected diffs: run terraform plan -refresh-only to see what changed outside Terraform, then decide whether to update the code or revert the change.
  • Provider or version errors: check your constraints and lock file, and rerun terraform init -upgrade deliberately.

For deeper detail, set TF_LOG=DEBUG and, if needed, TF_LOG_PATH to capture provider and API traffic. Treat those logs as sensitive, since they can include request details. After a failed apply, run terraform plan again: it shows exactly which resources were created and which remain.

Where Opsphere fits next to your Terraform workflow

Terraform stays your provisioning tool. We add the investigation layer around it, so your team can see what changed and what it affected without switching between consoles.

  • Read-only connections to Terraform metadata, AWS and observability tools, so evidence comes from the systems that already hold it
  • Drift context, failed apply triage, and correlation of resource changes with incidents
  • Reads, not writes: Opsphere is read-only by default

Opsphere operational intelligence for Terraform workflows

Opsphere

Start with the Terraform integration page to see how the connection works, then compare plans on our pricing page. Our Community plan is listed at 0 € per month, and our Developer plan at 19 € per month.

FAQ

What is the use of Terraform in DevOps?

Terraform lets DevOps teams define infrastructure in code, review it like application code, and apply it repeatably. AWS describes this approach in its guidance on infrastructure as code: configurations live in source control, which makes provisioning repeatable and reliable. Terraform’s declarative style means you describe the desired end state and let it work out the steps.

How do I install Terraform on AWS CloudShell?

CloudShell already includes the AWS CLI and is pre-authenticated with your console session, so you only add the Terraform binary. Download the Linux build from HashiCorp’s release page, unzip it into a directory on your path in your home folder, and run terraform version. AWS positions CloudShell for short-term tasks, so use a CI/CD pipeline for production work.

Why are people moving away from Terraform?

The reasons vary by team, and we do not have a single sourced explanation. AWS’s IaC tooling guidance advises choosing a tool that fits your goals, team skills and multi-cloud needs, so teams with different priorities reach different answers. Many teams keep Terraform and focus on state hygiene, module design and pipeline controls.

How do I import an EC2 instance into Terraform?

Write a matching aws_instance resource block first, then run terraform import aws_instance.web i-abcd1234 with your own instance ID. The import reference explains that import updates state but does not generate configuration. Run terraform plan afterward and adjust your code until it shows no unexpected changes.

Sources

Opsphere
Discuss Your Cloud Operations
Contact Opsphere to ask how its operational intelligence platform brings cloud, infrastructure and engineering context together for your team.

This article is provided for general informational purposes only and does not constitute professional, legal, security, or compliance advice. Please evaluate recommendations against your organization’s specific environment and requirements.