Category: Cloud & Infrastructure Tags: infrastructure-as-code terraform aws-cloudformation

What Is Infrastructure as Code?

By Oleksandr Andrushchenko — Published on — Modified on
0 Likes
0 Dislikes
Infrastructure as Code explained with Terraform, AWS CloudFormation, declarative configuration, automated provisioning, and CI/CD pipelines
Infrastructure as Code explained with Terraform, AWS CloudFormation, declarative configuration, automated provisioning, and CI/CD pipelines

Infrastructure as Code (IaC) is the practice of defining and managing infrastructure using machine-readable configuration files instead of manually creating resources through cloud consoles. Servers, networks, databases, load balancers, IAM roles, and Kubernetes resources can all be described as code, reviewed through pull requests, and provisioned automatically. Tools such as Terraform, AWS CloudFormation, Pulumi, and Ansible help teams make infrastructure changes repeatable, traceable, and easier to maintain.

IaC is not simply a collection of scripts that create cloud resources. A reliable implementation must handle dependencies, state, configuration drift, secrets, permissions, and infrastructure changes that can destroy or replace production resources. Understanding how these pieces work is essential for building reproducible environments, integrating infrastructure into CI/CD pipelines, and safely operating cloud systems at scale.

Table of Contents

How Infrastructure as Code Works

Imagine a team deploying a FastAPI application to AWS. The application needs a VPC, subnets, security groups, an Application Load Balancer, ECS services, a database, and IAM permissions. Creating these resources manually may work once, but repeating the process across development, staging, and production introduces opportunities for inconsistent settings and undocumented changes.

Infrastructure as Code replaces those manual steps with configuration stored in a repository. The infrastructure tool reads the configuration, determines which resources are required, and creates or updates them through cloud provider APIs.

Infrastructure as a Desired State

In a declarative IaC system, configuration describes the infrastructure that should exist. The tool compares that desired configuration with its recorded state and, when supported, the actual provider resources before calculating changes.

For example, a Terraform configuration might declare an S3 bucket:

resource "aws_s3_bucket" "assets" {
  bucket = "example-company-assets-prod"

  tags = {
    Environment = "production"
    ManagedBy   = "terraform"
  }
}

The configuration expresses the intended resource. Terraform handles the underlying AWS API operations required to create and manage it.

The bucket name must be globally unique within the applicable S3 namespace. A production configuration should use a naming convention that avoids collisions rather than copying a fixed example name.

The Infrastructure Lifecycle

A typical IaC workflow follows these steps:

  1. Define: Describe infrastructure resources in configuration files.
  2. Validate: Check syntax, provider configuration, and resource constraints.
  3. Plan: Review which resources will be created, modified, or destroyed.
  4. Approve: Evaluate cost, security, and operational impact.
  5. Apply: Execute the approved infrastructure changes.
  6. Observe: Monitor resources and detect configuration drift.
  7. Update: Modify configuration as application requirements evolve.

This workflow allows infrastructure changes to use many of the same engineering practices as application changes: code review, automated testing, version control, and controlled releases.

What Can Infrastructure as Code Manage?

IaC can manage infrastructure across several layers:

  • Networking: VPCs, subnets, routing tables, NAT gateways, and security groups
  • Compute: EC2 instances, ECS services, Kubernetes clusters, and serverless functions
  • Storage: S3 buckets, block volumes, and file systems
  • Databases: RDS instances, DynamoDB tables, and database networking
  • Security: IAM policies, roles, encryption settings, and resource permissions
  • Operations: Monitoring alarms, log groups, DNS records, and event rules

IaC defines the infrastructure surrounding an application, but it does not automatically replace application deployment, data migrations, or runtime configuration management. Those concerns often use separate tools and workflows.

For a broader view of cloud infrastructure components, see Cloud Architecture Explained: Building Modern Applications.

Declarative vs. Imperative Infrastructure

Infrastructure automation generally follows two approaches: declarative and imperative. Both can be useful, but they differ in what the engineer specifies and how the system handles repeated execution.

Declarative Infrastructure

Declarative configuration describes the desired result without requiring the engineer to specify every API call needed to reach it.

For example, Terraform can declare an EC2 instance:

resource "aws_instance" "app" {
  ami           = var.ami_id
  instance_type = "t3.micro"

  tags = {
    Name = "api-server"
  }
}

The configuration specifies the instance properties. Terraform determines whether the instance needs to be created, updated, or replaced based on the configuration, provider behavior, and recorded state.

Declarative systems are particularly useful for managing infrastructure over time because they maintain a model of what should exist.

Imperative Infrastructure

Imperative automation describes the operations to perform.

For example, an AWS CLI command explicitly creates an EC2 instance:

aws ec2 run-instances \
  --image-id "$AMI_ID" \
  --instance-type t3.micro \
  --subnet-id "$SUBNET_ID" \
  --security-group-ids "$SECURITY_GROUP_ID"

Executing this command twice can create two instances unless the script implements additional checks or idempotency controls.

Imperative scripts are useful for one-time operations, procedural workflows, and tasks that do not fit a declarative resource model. However, they require more explicit handling of existing resources, dependencies, and cleanup.

Aspect Declarative Imperative
Focus Desired end state Sequence of operations
Examples Terraform, CloudFormation Shell scripts, direct AWS CLI commands
Repeated execution Reconciles managed resources toward configuration Depends on script design
Change management Plans, diffs, and resource dependencies Explicit procedural logic
Best suited for Long-lived managed infrastructure Procedural operations and custom automation

These approaches are not mutually exclusive. A Terraform-managed environment may still use scripts for application initialization, migrations, or operational maintenance.

Infrastructure as Code Tools

Different IaC tools solve overlapping but not identical problems. Some focus on cloud resource provisioning, while others focus on configuring operating systems or managing application workloads.

Terraform

Terraform uses HashiCorp Configuration Language (HCL) to describe infrastructure resources. Providers integrate Terraform with AWS, Azure, Google Cloud, Kubernetes, and many external services.

Terraform maintains state to associate configuration resources with real infrastructure. It can generate execution plans, manage dependencies, and organize reusable infrastructure through modules.

Terraform is commonly chosen for multi-provider infrastructure and teams that want a consistent provisioning workflow across environments.

AWS CloudFormation

AWS CloudFormation is AWS's native infrastructure provisioning service. Resources are defined in YAML or JSON templates and managed as stacks.

For example, a CloudFormation template can define an S3 bucket:

AWSTemplateFormatVersion: "2010-09-09"

Resources:
  AssetsBucket:
    Type: AWS::S3::Bucket
    Properties:
      VersioningConfiguration:
        Status: Enabled

CloudFormation tracks stack resources and supports change sets, dependency ordering, and rollback behavior. It integrates directly with AWS resource types and services.

CloudFormation is often a strong choice when the infrastructure is entirely AWS-based and the team prefers AWS-native tooling.

Pulumi, Ansible, and Kubernetes

Pulumi allows infrastructure to be defined using general-purpose programming languages such as Python, TypeScript, and Go. It is useful when teams prefer familiar language constructs and tooling.

Ansible primarily focuses on automation and configuration management, although it can also provision infrastructure. Kubernetes manifests describe the desired state of cluster workloads and related resources, but they do not by themselves manage every underlying cloud resource.

Tool Configuration Primary Use
Terraform HCL Multi-provider infrastructure provisioning
AWS CloudFormation YAML / JSON AWS infrastructure and stack management
Pulumi Python, TypeScript, Go, and others Infrastructure defined with programming languages
Ansible YAML Configuration management and procedural automation
Kubernetes YAML / JSON manifests Declarative cluster workload management

Tool selection should consider existing infrastructure, provider support, state management, team experience, and operational complexity rather than configuration language alone.

State, Dependencies, and Configuration Drift

Writing infrastructure configuration is usually straightforward. Keeping that configuration synchronized with production infrastructure over months or years is more challenging.

State management, dependency ordering, and drift detection are fundamental to safe infrastructure operations.

Terraform State

Terraform uses a state file to track managed resources and their relationships to configuration addresses.

For example, if Terraform creates an S3 bucket, state records information that allows Terraform to identify the existing bucket during future plans.

Without reliable state, Terraform may not know which infrastructure resources correspond to a configuration, potentially resulting in unexpected creation, replacement, or orphaned resources.

State files can contain sensitive information, including resource attributes and sometimes secret values. Marking a Terraform output as sensitive limits normal display but does not necessarily remove the value from state.

Remote State and Locking

Storing production state only on a developer's laptop is risky. If the file is lost, corrupted, or modified concurrently, infrastructure management can become difficult.

Teams commonly use remote state storage with encryption, restricted access, versioning, and state locking.

For an AWS-based Terraform setup, an S3 backend can be configured as follows:

terraform {
  backend "s3" {
    bucket       = "company-terraform-state"
    key          = "production/api/terraform.tfstate"
    region       = "us-west-2"
    encrypt      = true
    use_lockfile = true
  }
}

This configuration assumes the S3 bucket already exists and that the installed Terraform version supports S3 lockfiles. The bucket should have versioning enabled and access restricted to authorized infrastructure operators and CI/CD roles.

Only one writer should modify a particular state at a time. Locking prevents concurrent applies from racing against the same infrastructure state.

Resource Dependencies

Infrastructure resources often depend on one another. An ECS service may require an IAM role, network configuration, and a load balancer target group before it can run correctly.

Terraform automatically infers many dependencies when one resource references another:

resource "aws_security_group" "api" {
  name   = "api-security-group"
  vpc_id = aws_vpc.main.id
}

resource "aws_vpc" "main" {
  cidr_block = "10.20.0.0/16"
}

The security group references aws_vpc.main.id, so Terraform understands that the VPC must be created first.

Explicit depends_on should be reserved for dependencies that cannot be inferred from resource references. Overusing it can make plans unnecessarily conservative and harder to understand.

Configuration Drift

Configuration drift occurs when real infrastructure differs from the declared configuration or expected state.

For example, an engineer may manually modify an EC2 security group in the AWS console to troubleshoot an incident. Terraform configuration remains unchanged, but the real security group now has different rules.

Running terraform plan with normal refresh behavior can reveal the difference and propose changes to reconcile the infrastructure with the configuration.

Drift is not always an error. Autoscaling systems and external controllers may legitimately modify certain resource properties. IaC ownership boundaries must account for those changes to avoid controllers repeatedly overwriting one another.

Production Design Example

Consider a SaaS platform running a FastAPI backend on Amazon ECS with AWS Fargate. The application uses an Application Load Balancer, PostgreSQL on Amazon RDS, Redis, and Amazon S3.

The team needs development, staging, and production environments with consistent architecture but different resource sizes and operational policies.

Organizing Infrastructure into Modules

A Terraform repository can separate reusable infrastructure modules from environment-specific configuration:

infrastructure/
├── modules/
│   ├── networking/
│   ├── ecs-service/
│   ├── database/
│   ├── redis/
│   └── monitoring/
├── environments/
│   ├── development/
│   ├── staging/
│   └── production/
└── README.md

Each environment can instantiate shared modules with different parameters, such as ECS task counts, database instance classes, and autoscaling limits.

Separate environment state reduces the risk that an ordinary development change accidentally modifies production infrastructure.

Environment-Specific Configuration

A production environment might use a reusable ECS module:

module "api_service" {
  source = "../../modules/ecs-service"

  name          = "production-api"
  desired_count = 4
  cpu           = 512
  memory        = 1024

  subnet_ids        = module.networking.private_subnet_ids
  security_group_id = module.networking.api_security_group_id

  environment = "production"
}

This is an illustrative module interface. The corresponding module must define these inputs and implement the ECS resources.

Modules improve consistency, but excessive abstraction can make infrastructure difficult to debug. Create reusable modules around meaningful infrastructure boundaries rather than wrapping every individual resource.

Failure Scenario: A Destructive Infrastructure Plan

Suppose an engineer changes the database resource configuration and Terraform proposes replacing the production RDS instance.

A routine pull request that appears to modify only a few configuration lines may produce a plan containing a destructive replacement.

The pipeline should stop the change before applying it. Production infrastructure requires explicit review of destructive operations, backup policies, and recovery implications.

For critical resources, lifecycle protections can reduce accidental destruction:

resource "aws_db_instance" "production" {
  # Other required RDS configuration omitted.

  lifecycle {
    prevent_destroy = true
  }
}

prevent_destroy blocks Terraform plans that would destroy the protected resource while the lifecycle rule remains in configuration. It is not a complete protection against deletion outside Terraform or removal of the resource configuration itself.

Use cloud-side protections, backups, and least-privilege permissions alongside IaC lifecycle controls.

For related architecture decisions, see Cloud Architecture Best Practices.

Ready-to-Use Example: Terraform on AWS

This example creates a small AWS environment using Terraform: a VPC, a public subnet, an internet gateway, a route table, and an S3 bucket with versioning and public-access blocking.

The example is intentionally small enough to run without an application or database. It demonstrates resource references, variables, outputs, planning, deployment, and cleanup.

Prerequisites

You need Terraform installed locally, AWS credentials configured for the target account, and permissions to manage the resources in the example.

Use a non-production AWS account for the exercise. The example creates billable cloud resources in some configurations, and resources should be destroyed when no longer needed.

1. Configure Terraform and AWS

Create versions.tf:

terraform {
  required_version = ">= 1.5.0"

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

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Project   = "iac-demo"
      ManagedBy = "terraform"
    }
  }
}

The provider uses the standard AWS credential chain, such as an AWS profile or an IAM role. Avoid hardcoding access keys in Terraform configuration.

2. Define Variables

Create variables.tf:

variable "aws_region" {
  description = "AWS region for the demo"
  type        = string
  default     = "us-west-2"
}

variable "project_name" {
  description = "Prefix for infrastructure resources"
  type        = string
  default     = "iac-demo"
}

variable "bucket_name" {
  description = "Globally unique S3 bucket name"
  type        = string
}

The S3 bucket name is required because S3 bucket names must be unique within the applicable namespace.

3. Create the Infrastructure

Create main.tf:

resource "aws_vpc" "main" {
  cidr_block           = "10.20.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.project_name}-vpc"
  }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.20.1.0/24"
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.project_name}-public-subnet"
  }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.project_name}-igw"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = {
    Name = "${var.project_name}-public-rt"
  }
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

resource "aws_s3_bucket" "assets" {
  bucket = var.bucket_name

  tags = {
    Name = "${var.project_name}-assets"
  }
}

resource "aws_s3_bucket_versioning" "assets" {
  bucket = aws_s3_bucket.assets.id

  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_public_access_block" "assets" {
  bucket = aws_s3_bucket.assets.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

The subnet has a route to an internet gateway, but instances launched into it will not automatically receive public IP addresses because map_public_ip_on_launch is disabled.

This is a networking foundation rather than a complete production VPC. A production design would normally include multiple availability zones, private subnets, carefully scoped security groups, and routing appropriate to the workloads.

4. Define Outputs

Create outputs.tf:

output "vpc_id" {
  description = "Created VPC ID"
  value       = aws_vpc.main.id
}

output "public_subnet_id" {
  description = "Created subnet ID"
  value       = aws_subnet.public.id
}

output "assets_bucket_name" {
  description = "Created S3 bucket name"
  value       = aws_s3_bucket.assets.id
}

Outputs make resource identifiers available after deployment and can be consumed by other tooling when appropriate.

5. Initialize, Plan, and Apply

Initialize the working directory and validate the configuration:

terraform init

terraform fmt -check

terraform validate

Generate an execution plan. Replace the bucket name with a unique value that you control:

terraform plan \
  -var="bucket_name=my-unique-iac-demo-bucket-12345" \
  -out=tfplan

Review the plan carefully, then apply the saved plan:

terraform apply tfplan

Using a saved plan ensures that Terraform applies the reviewed plan rather than generating a new one during the apply command. The plan file can contain sensitive data and should be handled as a protected artifact.

Inspect the created resources:

terraform output

terraform state list

Terraform state records the managed resources, while outputs expose the selected resource identifiers.

6. Modify Infrastructure

To see how Terraform handles an update, add another tag to the VPC:

tags = {
  Name        = "${var.project_name}-vpc"
  Environment = "development"
}

Run another plan using the same bucket name:

terraform plan \
  -var="bucket_name=my-unique-iac-demo-bucket-12345"

Terraform should propose updating the VPC tags in place rather than recreating the entire VPC.

Infrastructure changes are not always in-place updates. Some resource properties require replacement, so every plan must be reviewed before applying it.

7. Clean Up Resources

When finished, destroy the demo infrastructure:

terraform destroy \
  -var="bucket_name=my-unique-iac-demo-bucket-12345"

If objects have been uploaded to the S3 bucket, Terraform may be unable to delete the bucket until it is empty. Versioned buckets also retain historical object versions, which must be considered during cleanup.

Do not use terraform destroy against production state without a deliberate, reviewed decommissioning process.

CI/CD, Security, and Production Workflows

Infrastructure as Code becomes most valuable when it is integrated into a controlled engineering workflow. Developers should be able to propose infrastructure changes, review their impact, and deploy them without sharing unrestricted production credentials.

Infrastructure Changes Through Pull Requests

A typical CI/CD workflow can perform these steps:

  1. Run formatting and configuration validation.
  2. Scan infrastructure for security and policy violations.
  3. Generate a Terraform plan using a restricted CI/CD identity.
  4. Present the plan for review, especially resource replacements and deletions.
  5. Require approval for production changes.
  6. Apply the approved plan using a dedicated deployment identity.
  7. Record the deployment and monitor resulting infrastructure health.

For production environments, prefer short-lived credentials obtained through workload identity federation, such as GitHub Actions OIDC with AWS IAM roles, rather than long-lived AWS access keys stored as repository secrets.

Secrets and Sensitive Infrastructure Data

IaC configuration should not contain plaintext database passwords, private keys, or API tokens.

Use services such as AWS Secrets Manager or AWS Systems Manager Parameter Store for runtime secrets. However, merely reading a secret from a secrets manager does not guarantee that Terraform state will remain free of sensitive values.

Where possible, configure infrastructure to reference secrets at runtime rather than passing secret values through Terraform resource attributes.

See Secrets Management in Cloud Applications for more information.

Policy and Infrastructure Guardrails

Production infrastructure should have automated controls that detect unsafe configurations before they are deployed.

Examples include:

  • Rejecting publicly accessible database instances
  • Preventing unrestricted inbound administrative ports
  • Requiring encryption for persistent storage
  • Restricting destructive changes to critical resources
  • Enforcing mandatory resource tags
  • Checking that production deployments use approved regions and instance types

Policy-as-code tools and infrastructure scanners can enforce these controls during pull requests. Cloud-side policies and IAM restrictions provide additional protection if a deployment bypasses the CI/CD pipeline.

Scheduled Drift Detection

Even a well-managed IaC environment can drift when engineers make emergency changes or external systems modify resources.

Schedule read-only infrastructure plans or dedicated drift checks, review unexpected differences, and reconcile them deliberately.

Do not automatically apply every detected difference. Some drift reflects an intentional emergency fix, and blindly restoring the previous configuration can recreate the original incident.

Production Checklist

Before managing critical infrastructure with IaC, verify that the repository, state backend, and deployment pipeline provide appropriate operational safeguards.

  • Version control: Store infrastructure configuration in a reviewed repository.
  • State: Use protected remote state with encryption, versioning, and locking.
  • Credentials: Prefer short-lived deployment identities with least-privilege access.
  • Planning: Review resource creations, updates, replacements, and deletions.
  • Isolation: Separate production and non-production environments and state.
  • Secrets: Avoid storing secret values in configuration or exposing them through state.
  • Protection: Apply safeguards to critical databases, storage, and networking resources.
  • Validation: Run syntax checks, security scanning, and policy enforcement.
  • Drift: Detect and investigate changes made outside the infrastructure workflow.
  • Recovery: Test state restoration, backups, and infrastructure recovery procedures.

Common Mistakes

Infrastructure as Code reduces manual configuration errors, but it also makes infrastructure changes easier to execute at scale. Incorrect configuration can therefore affect many resources very quickly.

Keeping Production State Only Locally

Local state can be lost, copied, or modified concurrently. Use a protected remote backend with locking and recovery capabilities for shared infrastructure.

Putting Secrets in Terraform Variables

Marking a variable as sensitive does not automatically prevent its value from appearing in Terraform state. Use runtime secret references and avoid passing plaintext credentials through managed resource attributes where possible.

Applying Without Reviewing the Plan

A small configuration change can force replacement of a database, load balancer, or other critical resource. Always inspect planned replacements and deletions before production applies.

Mixing Manual Console Changes with IaC

Untracked console modifications create drift and make future plans harder to predict. Emergency changes should be documented and reconciled into the authoritative infrastructure configuration.

Building Too Many Modules Too Early

Modules improve reuse, but excessive abstraction hides important resource details and makes debugging difficult. Start with clear infrastructure boundaries and extract modules when repeated patterns justify them.

Frequently Asked Questions

Infrastructure as Code is a broad practice that covers resource provisioning, configuration management, and operational workflows. These questions address common distinctions and implementation decisions.

Is Infrastructure as Code the Same as DevOps?

No. Infrastructure as Code is a technical practice for managing infrastructure through configuration and automation. DevOps is a broader approach involving collaboration, delivery processes, automation, observability, and operational responsibility.

IaC is often an important part of a DevOps workflow, but it does not replace the other practices.

Should I Use Terraform or AWS CloudFormation?

Terraform is useful when managing multiple providers or when a team prefers its provider ecosystem and module workflows. CloudFormation integrates directly with AWS and is a strong option for AWS-only infrastructure.

Both can manage production AWS resources. State management, team experience, deployment workflows, and provider coverage often matter more than syntax preferences.

Is Infrastructure as Code Idempotent?

Declarative IaC tools are designed to reconcile infrastructure toward a desired configuration, so repeated applies with no relevant changes generally produce no new operations.

However, idempotency is not absolute. Provider behavior, external modifications, unstable resource inputs, and procedural provisioners can still produce unexpected changes.

Why Does Terraform Need a State File?

Terraform state maps managed configuration resources to actual infrastructure objects and stores information required to plan subsequent changes.

It also helps Terraform track dependencies and detect differences between recorded infrastructure and current configuration.

Can Infrastructure as Code Manage Existing Resources?

Yes. Terraform supports importing existing infrastructure into state, and other IaC systems provide their own import or adoption mechanisms.

Importing a resource does not automatically produce a complete, correct configuration. The declared properties must be reconciled with the existing resource before applying changes.

Is Kubernetes YAML Infrastructure as Code?

Kubernetes manifests are a form of declarative configuration and can be managed as code. They define resources such as Deployments, Services, ConfigMaps, and Ingresses.

However, Kubernetes manifests primarily manage resources within a cluster. Tools such as Terraform or CloudFormation are commonly used to provision the underlying cloud infrastructure and the cluster itself.

Conclusion

Infrastructure as Code makes infrastructure reproducible, reviewable, and manageable through automated workflows. Instead of relying on manual cloud-console changes, teams define resources in version-controlled configuration and use provisioning tools to create and maintain them consistently.

The main engineering challenges are not writing resource definitions but managing state, understanding destructive changes, handling configuration drift, protecting secrets, and building safe deployment workflows. Production IaC requires the same discipline as production application code, with additional attention to infrastructure dependencies and irreversible operations.

Key Takeaway: Infrastructure as Code turns infrastructure management into a controlled software engineering process. Use declarative configuration, protected state, reviewed execution plans, least-privilege automation, and drift detection to make cloud environments more predictable and easier to operate.

Explore these related SysDesPro articles for deeper coverage of cloud infrastructure, Kubernetes, and production operations.

Author

Enjoyed this article?

Support Oleksandr Andrushchenko

Buy me a coffee

This helps Oleksandr Andrushchenko continue creating useful content

Related articles

Comments (0)