ServerlessBase Blog
  • Introduction to Terraform: The IaC Standard

    Terraform is the industry-standard tool for infrastructure as code, enabling you to manage cloud resources declaratively and consistently across multiple providers.

    Introduction to Terraform: The IaC Standard

    You've probably deployed infrastructure manually at least once. You spin up a server, configure the firewall, set up the database, and deploy your application. Then you do it again for a staging environment, and again for production. Each time, you make small tweaks—this time you changed the instance size, next time you added a load balancer. Eventually, you realize you're not just deploying applications; you're maintaining a fragile, undocumented process that's hard to reproduce and even harder to scale.

    This is where infrastructure as code (IaC) changes everything. Instead of clicking through a web console or running a series of manual commands, you define your infrastructure as code, version it, and deploy it consistently. Among the many IaC tools available, Terraform has emerged as the industry standard, and for good reason.

    What is Terraform?

    Terraform is an open-source tool that lets you define and provision infrastructure using a declarative configuration language called HashiCorp Configuration Language (HCL). Unlike imperative scripts that tell the system how to do something step by step, Terraform describes what you want to have, and it figures out the steps to get there.

    Think of it like a recipe. An imperative script is like saying "add 2 cups of flour, then add 1 cup of sugar, then mix for 3 minutes." Terraform is like saying "make a cake with 2 cups of flour and 1 cup of sugar." Terraform handles the mixing, baking, and timing automatically.

    The Core Concept: Declarative vs Imperative

    The fundamental difference between Terraform and traditional scripting tools is declarative vs imperative approaches:

    ApproachDescriptionExample
    ImperativeDescribes the steps to achieve a result"Create a server, then install Nginx, then configure it"
    DeclarativeDescribes the desired state"A server running Nginx on port 80"

    Terraform's declarative model means you don't need to worry about the intermediate steps. You just declare the desired state, and Terraform creates, updates, or destroys resources to match that state.

    How Terraform Works

    Terraform operates on three core concepts: providers, resources, and state.

    Providers

    Providers are plugins that allow Terraform to interact with various cloud providers and services. Each provider exposes a set of resources that Terraform can manage. For example:

    • AWS Provider: Manages EC2 instances, S3 buckets, RDS databases, and hundreds of other AWS resources
    • Azure Provider: Manages Azure Virtual Machines, Storage Accounts, and Azure services
    • Google Cloud Provider: Manages GCP Compute Engine, Cloud Storage, and other services
    • Kubernetes Provider: Manages Kubernetes clusters and resources

    You can think of providers as adapters that translate Terraform's configuration into API calls for specific services.

    Resources

    Resources are the building blocks of your infrastructure. Each resource represents a piece of infrastructure that Terraform can create, update, or delete. A resource block looks like this:

    resource "aws_instance" "web_server" {
      ami           = "ami-0c55b159cbfafe1f0"
      instance_type = "t3.micro"
      tags = {
        Name = "web-server"
      }
    }

    This configuration creates an AWS EC2 instance with a specific AMI (Amazon Machine Image) and instance type, and tags it with the name "web-server".

    State

    Terraform maintains a state file that tracks the resources it manages. This state file is crucial because it tells Terraform what resources exist and their current properties. When you run terraform apply, Terraform compares your configuration to the state file and determines what changes are needed.

    The state file is a JSON file (usually named terraform.tfstate) that looks like this:

    {
      "version": 4,
      "terraform_version": "1.5.0",
      "serial": 1,
      "lineage": "some-uuid",
      "outputs": {},
      "resources": [
        {
          "mode": "managed",
          "type": "aws_instance",
          "name": "web_server",
          "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
          "instances": [
            {
              "schema_version": 0,
              "attributes": {
                "ami": "ami-0c55b159cbfafe1f0",
                "arn": "arn:aws:ec2:us-east-1:123456789012:instance/i-0abc123def456",
                "availability_zone": "us-east-1a",
                "id": "i-0abc123def456",
                "instance_type": "t3.micro",
                "tags": {
                  "Name": "web-server"
                }
              }
            }
          ]
        }
      ]
    }

    The state file is your source of truth. If you manually modify a resource outside of Terraform (like changing an instance type in the AWS console), Terraform won't know about the change until you run terraform refresh to update the state.

    Getting Started with Terraform

    Let's walk through creating a simple infrastructure with Terraform.

    Step 1: Install Terraform

    First, download and install Terraform from the official website: https://www.terraform.io/downloads.html

    For Linux, you can download and extract it directly:

    wget https://releases.hashicorp.com/terraform/1.5.0/terraform_1.5.0_linux_amd64.zip
    unzip terraform_1.5.0_linux_amd64.zip
    sudo mv terraform /usr/local/bin/
    terraform --version

    Step 2: Initialize Your Project

    Create a new directory for your Terraform project and initialize it:

    mkdir my-terraform-project
    cd my-terraform-project
    terraform init

    The init command downloads the required providers and sets up the backend configuration. You'll see output like:

    Initializing the backend...
    
    Initializing provider plugins...
    - Finding hashicorp/aws versions matching ">= 4.0.0"...
    - Installing hashicorp/aws v4.62.0...
    - Installed hashicorp/aws v4.62.0 (signed by HashiCorp)
    
    Terraform has been successfully initialized!
    
    You may now begin working with Terraform. To run a Terraform command with
    the chosen backend, please run:
        terraform <command>

    Step 3: Write Your Configuration

    Create a file named main.tf with the following configuration:

    terraform {
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 4.0"
        }
      }
    }
     
    provider "aws" {
      region = "us-east-1"
    }
     
    resource "aws_instance" "web_server" {
      ami           = "ami-0c55b159cbfafe1f0"
      instance_type = "t3.micro"
     
      tags = {
        Name = "web-server"
      }
    }

    This configuration:

    1. Specifies the required AWS provider version
    2. Configures the AWS provider with the us-east-1 region
    3. Creates an EC2 instance with a specific AMI and instance type

    Step 4: Plan Your Changes

    Before applying your configuration, run terraform plan to see what Terraform will do:

    terraform plan

    You'll see output like:

    Terraform used the selected providers to generate the following execution plan:
    Resource actions are indicated with the following symbols:
      + create
    
    Terraform will perform the following actions:
    
      # aws_instance.web_server will be created
      + resource "aws_instance" "web_server" {
          + ami                          = "ami-0c55b159cbfafe1f0"
          + arn                          = (after applying)
          + associate_public_ip_address  = true
          + availability_zone            = (known after apply)
          + cpu_core_count               = (known after apply)
          + cpu_threads_per_core         = (known after apply)
          + get_password_data            = false
          + id                           = (known after apply)
          + instance_type                = "t3.micro"
          + ipv6_address_count           = (known after apply)
          + ipv6_addresses               = (known after apply)
          + key_name                     = (known after apply)
          + outpost_arn                  = (known after apply)
          + password_data                = (known after apply)
          + placement_group              = (known after apply)
          + primary_network_interface_id  = (known after apply)
          + private_dns                  = (known after apply)
          + private_ip                   = (known after apply)
          + private_ip_addresses         = (known after apply)
          + public_dns                   = (known after apply)
          + public_ip                    = (known after apply)
          + secondary_private_ips        = (known after apply)
          + security_groups              = (known after apply)
          + source_dest_check            = true
          + subnet_id                    = (known after apply)
          + tags                         = {
              + "Name" = "web-server"
            }
          + tags_all                     = {
              + "Name" = "web-server"
            }
          + tenancy                      = (known after apply)
          + user_data                    = (known after apply)
          + vpc_security_group_ids       = (known after apply)
        }
    
    Plan: 1 to add, 0 to change, 0 to destroy.
    
    ────────────────────────────────────────────────────────────────────────────────────────────────
    
    Note: You didn't specify an `-out` file to save this plan, so Terraform can't guarantee that
    exactly the same actions will be performed if you run `terraform apply` again.

    The plan shows that Terraform will create one resource (the EC2 instance) and nothing else. This is your chance to review the changes before applying them.

    Step 5: Apply Your Configuration

    When you're satisfied with the plan, run terraform apply:

    terraform apply

    Terraform will prompt for confirmation:

    Do you want to perform these actions?
      Terraform will perform the actions described above.
      Only 'yes' will be accepted to approve.
    
      Enter a value: yes

    Type yes and press Enter. Terraform will create the EC2 instance:

    aws_instance.web_server: Creating...
    aws_instance.web_server: Still creating... [10s elapsed]
    aws_instance.web_server: Still creating... [20s elapsed]
    aws_instance.web_server: Creation complete after 25s [id=i-0abc123def456]
    
    Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

    Step 6: Verify Your Infrastructure

    You can verify that your infrastructure was created by checking the AWS console or running:

    aws ec2 describe-instances --instance-ids i-0abc123def456

    You should see your instance with the name "web-server" and instance type "t3.micro".

    Step 7: Destroy Your Infrastructure

    When you're done, you can destroy the infrastructure with:

    terraform destroy

    Terraform will prompt for confirmation:

    Do you really want to destroy all resources?
      Terraform will destroy all managed resources, including those not defined in
      the configuration.
    
      Do you want to perform these actions?
      Enter a value: yes

    Type yes and press Enter. Terraform will delete the EC2 instance.

    Terraform Modules

    As your infrastructure grows, you'll want to avoid repeating configuration. Terraform modules let you package reusable infrastructure components.

    Creating a Module

    Create a new directory for your module:

    mkdir modules/web-server
    cd modules/web-server

    Create a main.tf file in the module directory:

    resource "aws_instance" "web_server" {
      ami           = "ami-0c55b159cbfafe1f0"
      instance_type = var.instance_type
     
      tags = {
        Name = var.name
      }
    }

    Create a variables.tf file:

    variable "name" {
      description = "Name tag for the instance"
      type        = string
      default     = "web-server"
    }
     
    variable "instance_type" {
      description = "Instance type for the EC2 instance"
      type        = string
      default     = "t3.micro"
    }

    Using a Module

    Now you can use this module in your main configuration:

    module "web_server" {
      source = "../modules/web-server"
     
      name     = "production-web-server"
      instance_type = "t3.medium"
    }

    This creates an EC2 instance with the name "production-web-server" and instance type "t3.medium".

    Terraform State Management

    The state file is critical, but it can also be a source of problems. If you lose the state file, you lose track of your infrastructure. If multiple people work on the same state file, you'll have conflicts.

    Remote State

    By default, Terraform stores the state file locally. For team collaboration, you should use a remote backend. Common options include:

    • S3: Stores state in Amazon S3 with optional DynamoDB locking
    • Azure Storage: Stores state in Azure Storage with optional Blob locking
    • GCS: Stores state in Google Cloud Storage with optional Lock Service

    Example S3 backend configuration:

    terraform {
      backend "s3" {
        bucket         = "my-terraform-state"
        key            = "production/terraform.tfstate"
        region         = "us-east-1"
        encrypt        = true
        dynamodb_table = "terraform-locks"
      }
    }

    State Locking

    State locking prevents concurrent modifications to the state file. When one person is applying Terraform, others are blocked until the operation completes. The DynamoDB table provides distributed locking.

    State Workflows

    Terraform provides several commands for state management:

    • terraform state list: Lists all resources in the state
    • terraform state show <resource>: Shows the current state of a resource
    • terraform state mv <from> <to>: Moves a resource in the state
    • terraform state rm <resource>: Removes a resource from the state

    Best Practices

    1. Use Variables and Outputs

    Don't hardcode values in your configuration. Use variables for configurable values and outputs to expose important information.

    # variables.tf
    variable "environment" {
      description = "Environment name"
      type        = string
      default     = "production"
    }
     
    variable "instance_type" {
      description = "Instance type"
      type        = string
      default     = "t3.micro"
    }
     
    # outputs.tf
    output "instance_id" {
      description = "ID of the created instance"
      value       = aws_instance.web_server.id
    }
     
    output "public_ip" {
      description = "Public IP address of the instance"
      value       = aws_instance.web_server.public_ip
    }

    2. Use Modules for Reusability

    Break your infrastructure into logical modules. Each module should have a single responsibility.

    3. Use Workspaces for Environments

    Terraform workspaces allow you to manage multiple environments (dev, staging, production) in the same configuration.

    terraform workspace new dev
    terraform workspace new staging
    terraform workspace new production

    4. Version Control Your Configuration

    Always commit your Terraform configuration to version control. This ensures you have a history of changes and can collaborate with others.

    5. Use Terraform Cloud or Enterprise

    For larger teams, Terraform Cloud or Enterprise provides features like remote state, private module registry, and policy as code.

    6. Test Your Configuration

    Use tools like Terratest to write automated tests for your Terraform configurations.

    Common Pitfalls

    1. Forgetting to Run terraform init

    The init command downloads providers and sets up the backend. If you forget to run it, you'll get errors like "provider hashicorp/aws not found."

    2. Modifying Resources Outside of Terraform

    If you manually change a resource (like changing an instance type in the AWS console), Terraform won't know about the change. Run terraform refresh to update the state.

    3. Losing the State File

    The state file is critical. Always back it up and use a remote backend for team collaboration.

    4. Not Using Variables

    Hardcoding values makes your configuration inflexible. Use variables for configurable values.

    5. Ignoring State Locking

    State locking prevents concurrent modifications. If you don't use a remote backend with locking, multiple people applying Terraform simultaneously can cause conflicts.

    Terraform vs Other IaC Tools

    Terraform isn't the only IaC tool available. Here's how it compares to other popular tools:

    FeatureTerraformPulumiCloudFormationAnsible
    LanguageHCLReal languages (Python, TypeScript, etc.)JSON/YAMLYAML
    Multi-CloudYesYesNo (cloud-specific)Yes
    State ManagementYesNoNoNo
    DeclarativeYesYesYesNo (imperative)
    Learning CurveModerateModerate to HighLowLow
    CommunityVery LargeGrowingLarge (AWS-specific)Large

    Terraform's multi-cloud support and large community make it a popular choice for organizations managing infrastructure across multiple providers.

    Conclusion

    Terraform has revolutionized infrastructure management by making it possible to define and provision infrastructure as code. Its declarative model, multi-cloud support, and strong ecosystem make it the industry standard for IaC.

    The key takeaways are:

    1. Declarative approach: Describe what you want, not how to get there
    2. Providers: Plugins that connect Terraform to various cloud providers
    3. State management: Track and manage the state of your infrastructure
    4. Modules: Reusable components that reduce duplication
    5. Best practices: Use variables, outputs, and version control

    If you're new to IaC, start with a simple Terraform configuration and gradually build up to more complex infrastructure. Platforms like ServerlessBase can help you manage your infrastructure deployments more easily, but understanding the fundamentals of Terraform will give you the skills to work with any platform.

    The next step is to practice. Create a Terraform configuration for a simple infrastructure (like an EC2 instance and a security group), apply it, and then destroy it. Once you're comfortable with the basics, explore modules, remote state, and more advanced features.

    Leave comment