Definition
- terraform taint / -replace marks a resource Terraform already manages for destruction and recreation on the next apply, used when the resource is unhealthy but the configuration is unchanged
- terraform import takes a resource that exists in the cloud but is not in Terraform's state and records the mapping so Terraform starts managing it, without creating or destroying anything
- Module outputs are the way child modules expose data to callers within the same configuration, using the
module.name.outputsyntax - terraform_remote_state is a data source that reads outputs from a separately-applied configuration's state file, enabling cross-stack references
- Replace and import are precise, opposite tools: replace recreates what Terraform manages, import starts managing what Terraform does not
At the National Bank of India, the replace action is like telling maintenance to rip out a specific faulty ATM that the bank already owns and install a fresh one in its place, even though the blueprint for that ATM did not change. Import is the reverse: an ATM was installed by a contractor and never entered into the inventory ledger, so you add it to the ledger so the bank officially manages it from now on, without touching the machine. Referencing between modules is like the vault module publishing the vault's location so the security-camera module can point a camera at it, either by handing over the address directly within one project or by looking it up from another department's published records.
Understanding the Concepts
What is terraform taint and -replace?
The terraform taint command, now deprecated in favour of the -replace flag on terraform apply, marks a resource that Terraform already manages for destruction and recreation on the next apply. You reach for it when a resource is unhealthy in a way the configuration cannot express. For example, a NAT gateway is stuck in a bad state, or a virtual machine's disk is corrupted, but the declared HCL is correct and unchanged.
The modern approach uses the -replace flag directly on apply or plan, which is clearer because it shows the destroy-and-recreate in the plan output so you can review it before applying.
# Old way (deprecated) -- mark the resource as tainted in state
terraform taint aws_instance.transfer_service
terraform apply
# New way (preferred) -- replace inline during apply
terraform plan -replace="aws_instance.transfer_service"
terraform apply -replace="aws_instance.transfer_service"
# Plan output shows:
# aws_instance.transfer_service must be replaced
-/+ resource "aws_instance" "transfer_service" {
~ id = "i-0abc123" -> (known after apply)
# (all other attributes will be recomputed)
}
Plan: 1 to add, 0 to change, 1 to destroy.
The key point is that replace operates entirely on a resource that is already under management. It changes nothing about ownership, only forces a recreate.
What is terraform import?
Import solves the opposite problem: a resource exists in the real cloud but Terraform has no record of it in state, so Terraform would either ignore it or try to create a duplicate. This happens when a resource was created manually, by another tool, or when rebuilding a lost state.
# Step 1: Write a resource block in your HCL (even a shell)
resource "aws_s3_bucket" "audit_logs" {
bucket = "nbi-audit-logs-prod"
}
# Step 2: Import the real bucket into state
terraform import aws_s3_bucket.audit_logs nbi-audit-logs-prod
# Output:
Import successful!
The resources that were imported are shown above.
These resources are now in your Terraform state.
Import never creates or destroys anything in the cloud. It only writes a mapping into state. After importing, you run terraform plan and expect no changes if your configuration accurately describes the imported resource.
How is -replace different from import? (comparison table)
Replace and import move in opposite directions, and confusing them can either needlessly destroy a healthy resource or accidentally duplicate an existing one.
| Aspect | -replace (taint) | import |
|---|---|---|
| Direction | Recreates a managed resource | Adopts an unmanaged resource |
| State before | Resource is in state | Resource is NOT in state |
| Cloud effect | Destroys old, creates new | No cloud mutation at all |
| Config change needed | No | Yes, HCL must exist first |
| When to use | Unhealthy resource, correct config | Manual or legacy resource to adopt |
| Risk | Data loss if resource is stateful | Drift if config does not match reality |
# Replace: resource is already managed, force recreate
terraform apply -replace="aws_nat_gateway.payments"
# Import: resource exists in cloud but not in state
terraform import aws_s3_bucket.audit_logs nbi-audit-logs-prod
How do you reference one module's resources from another within the same configuration?
Within a single Terraform configuration, child modules declare output values exposing exactly what callers may use. The root or another module reads that output with the module.name.output syntax and passes it as an input variable to the consuming module.
# modules/networking/outputs.tf
output "private_subnet_ids" {
description = "Subnet IDs for the NBI private network"
value = aws_subnet.private[*].id
}
# root main.tf -- wire the networking output into the EKS module
module "networking" {
source = "./modules/networking"
vpc_cidr = var.vpc_cidr
}
module "eks" {
source = "./modules/eks"
subnet_ids = module.networking.private_subnet_ids # <-- reference
cluster_name = "nbi-payments-cluster"
}
This keeps modules encapsulated: a module exposes a deliberate contract of outputs and never lets callers depend on its internal resource names.
How do you reference resources across separately-applied configurations?
Across configurations that are applied independently with their own state files, the terraform_remote_state data source reads another configuration's outputs.
# In the application stack's main.tf
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "nbi-terraform-state"
key = "networking/terraform.tfstate"
region = "ap-south-1"
}
}
resource "aws_instance" "transfer_service" {
ami = var.ami_id
instance_type = "m5.large"
subnet_id = data.terraform_remote_state.networking.outputs.private_subnet_ids[0]
tags = {
Name = "nbi-transfer-service"
}
}
Producer state (networking) publishes:
outputs.private_subnet_ids = ["subnet-aaa", "subnet-bbb"]
Consumer state (application) reads:
data.terraform_remote_state.networking.outputs.private_subnet_ids[0]
=> "subnet-aaa"
What are common mistakes?
A common mistake is using -replace when you meant to import, or vice versa. Another is tightly coupling two configurations through terraform_remote_state when a more stable contract would be better.
# WRONG: Reaching into another module's internal resource
resource "aws_security_group_rule" "allow_transfer" {
security_group_id = module.networking.aws_security_group.internal.id
# This breaks encapsulation -- internal resource names are not outputs
}
# CORRECT: Use the module's declared output
resource "aws_security_group_rule" "allow_transfer" {
security_group_id = module.networking.internal_sg_id
# The module deliberately exposes this through an output
}
When should you use SSM parameters instead of remote state?
In a large bank platform, terraform_remote_state creates a hard dependency on another configuration's internal state layout. A more stable approach is to publish shared values through AWS SSM Parameter Store, so the consumer reads them with a normal data source.
# Producer: networking stack writes to SSM
resource "aws_ssm_parameter" "vpc_id" {
name = "/nbi/networking/vpc_id"
type = "String"
value = aws_vpc.main.id
}
# Consumer: application stack reads from SSM
data "aws_ssm_parameter" "vpc_id" {
name = "/nbi/networking/vpc_id"
}
resource "aws_instance" "transfer_service" {
subnet_id = data.aws_ssm_parameter.vpc_id.value
}
This decouples the stacks because the consumer depends on a stable parameter name, not on another configuration's state file layout.
What should a senior engineer know?
A senior engineer should know that -replace is always preferred over the deprecated taint command because it shows the destroy-and-recreate in the plan output. They should also know that import only fills state, not configuration, so a terraform plan after import will show drift if the HCL does not match reality.
For module references, the senior discipline is to prefer explicit outputs and, where possible, provider data sources or published parameters over tightly coupling two states with terraform_remote_state. In a large banking platform, hard-coupling two stacks through remote state means a refactor in one breaks the other.
# After importing, always verify with a plan
terraform import aws_s3_bucket.audit_logs nbi-audit-logs-prod
terraform plan
# If the plan shows changes, your HCL does not match reality -- fix it
What are common interview follow-up questions?
- What happens if you taint a resource inside a module? You use the full address:
terraform taint module.networking.aws_subnet.private[0]. - Can you import a resource that is inside a module? Yes, using the full module path:
terraform import module.networking.aws_vpc.main vpc-abc123. - What is the difference between
terraform_remote_stateand a provider data source likeaws_ssm_parameter? Remote state couples you to another configuration's state layout; a data source couples you only to a stable resource name. - Can you undo an import? Yes, with
terraform state rmwhich removes the mapping from state without destroying the real resource.
Visual Overview
-replace (managed resource) ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Terraform │ │ Cloud │ │ Terraform │ │ State: YES │────▶│ Destroy old │────▶│ State: YES │ │ Resource: ✓ │ │ Create new │ │ Resource: ✓ │ └──────────────┘ └──────────────┘ └──────────────┘ import (unmanaged resource) ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Terraform │ │ State │ │ Terraform │ │ State: NO │────▶│ Write map │────▶│ State: YES │ │ Resource: ✓ │ │ No cloud op │ │ Resource: ✓ │ └──────────────┘ └──────────────┘ └──────────────┘ Module output wiring (same config) ┌────────────────┐ outputs ┌────────────────┐ │ networking │─────────────▶│ eks │ │ module │ subnet_ids │ module │ └────────────────┘ └────────────────┘ Remote state (cross-config) ┌────────────────┐ S3 state ┌────────────────┐ │ networking │─────────────▶│ application │ │ config │ .tfstate │ config │ └────────────────┘ └────────────────┘
Key Takeaway
The replace operation and import move in opposite directions, and keeping that straight is the heart of the answer. Replace, which used to be done with terraform taint and is now done with the -replace flag on apply, takes a resource Terraform already manages and forces it to be destroyed and recreated. You reach for it when a resource is unhealthy in a way the configuration cannot express, such as a corrupted virtual machine or a NAT gateway stuck in a bad state, while the declared HCL is correct and unchanged. Import does the reverse: it takes a resource that exists in the cloud but is not in Terraform's state and records the mapping so Terraform starts managing it, without creating or destroying anything.
Referencing resources between modules is about passing data through declared contracts rather than reaching into internals. Within one configuration, a module exposes output values and the caller reads them with the module.name.output syntax. Across separately-applied configurations with their own state files, the producer declares outputs and the consumer reads them through the terraform_remote_state data source. In a large platform, publishing shared values through SSM parameters decouples the stacks instead of hard-wiring one configuration to another's state layout.
In the National Bank of India project, a managed NAT gateway went bad and the team used -replace to recreate just that one resource without disturbing anything else. A contractor created an audit bucket manually and the team imported it so Terraform managed it going forward without recreating it and losing the logs. And because the networking and application stacks are applied separately, the application's EKS module reads the shared VPC's subnet IDs from the networking stack through remote state outputs.
Remember one sentence: replace recreates a managed resource, import adopts an unmanaged one, and modules communicate through declared outputs, never by reaching into each other's resources directly.
---