Post

Cloud-1 - Infrastructure as Code and Configuration Management

Introduction :

Infrastructure as Code (IaC) is the process of managing and provisioning computing infrastructure through machine-readable definition files, rather than physical hardware configuration or interactive configuration tools. Instead of manually clicking through a cloud console, every resource — from a VPC to an EC2 instance to a firewall rule — is described in code, stored in Git, reviewed, and applied automatically. This ensures consistency, repeatability, auditability, and the ability to destroy and recreate an entire environment in minutes.

Project goals :

Cloud-1 is a 1337 project aimed at mastering cloud deployments. The objective is to use AWS to provision a complete network architecture (VPC, Subnets, Internet Gateway, Security Groups, EC2 Instances) using Terraform for infrastructure provisioning, and then configure all software on those instances using Ansible for configuration management — keeping the two concerns cleanly separated.

Tool Overview :

ToolRoleApproach
TerraformProvision cloud resources (VMs, networks, firewalls)Declarative — describe what you want
AnsibleConfigure software on existing machinesProcedural — describe how to configure
AWS CLIInteract with AWS API from the terminalImperative — run commands manually

Walkthrough :

:one: AWS Credentials and Provider Configuration :

Never hardcode credentials in Terraform files. Use environment variables or an aws configure profile. Define the provider in a dedicated provider.tf file and pin the version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# provider.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "eu-west-3"
  # Credentials are read from env vars:
  # AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
}

# Set credentials in your shell (never commit these):
$ export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
$ export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

:two: Provisioning Network Infrastructure :

Define the complete network topology in Terraform. A well-structured VPC separates public-facing resources from private ones:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# network.tf

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  tags = { Name = "cloud1-vpc" }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "eu-west-3a"
  map_public_ip_on_launch = true
  tags = { Name = "cloud1-public-subnet" }
}

resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id
  tags = { Name = "cloud1-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.gw.id
  }
}

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

:three: Security Groups (Virtual Firewalls) :

Security Groups act as stateful firewalls — they track connections, so you only need to allow inbound traffic and return traffic is automatically permitted. Define one per service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# security_groups.tf

resource "aws_security_group" "web" {
  name   = "cloud1-web-sg"
  vpc_id = aws_vpc.main.id

  ingress {
    description = "HTTP from anywhere"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "HTTPS from anywhere"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "SSH from admin only"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["YOUR_IP/32"]   # restrict SSH to your IP
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

:four: Deploying EC2 Compute Instances :

Define instances referencing the network resources by their Terraform IDs. Use data sources to look up the latest Ubuntu AMI dynamically:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# compute.tf

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]   # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

resource "aws_key_pair" "deployer" {
  key_name   = "cloud1-key"
  public_key = file("~/.ssh/id_rsa.pub")
}

resource "aws_instance" "web" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web.id]
  key_name               = aws_key_pair.deployer.key_name

  tags = { Name = "cloud1-web" }
}

output "web_public_ip" {
  value = aws_instance.web.public_ip
}

:five: Configuration Management with Ansible :

Once Terraform provisions the instances, Ansible connects over SSH to configure software. Generate an inventory file from Terraform outputs:

1
2
3
4
5
6
# Generate inventory dynamically after terraform apply
$ terraform output -raw web_public_ip > inventory.ini
# Or write a proper inventory file:
# inventory.ini
[webservers]
1.2.3.4 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa

Write a playbook to install and configure Nginx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# playbook.yml
---
- name: Configure Web Server
  hosts: webservers
  become: yes

  tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600

    - name: Install Nginx
      apt:
        name: nginx
        state: present

    - name: Deploy site configuration
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/cloud1
      notify: Reload Nginx

    - name: Enable site
      file:
        src: /etc/nginx/sites-available/cloud1
        dest: /etc/nginx/sites-enabled/cloud1
        state: link

  handlers:
    - name: Reload Nginx
      service:
        name: nginx
        state: reloaded

Run the playbook:

1
$ ansible-playbook -i inventory.ini playbook.yml

:six: Applying and Managing Terraform State :

The Terraform workflow always follows three commands:

1
2
3
4
5
6
7
8
# Preview all planned changes (safe — no changes made)
$ terraform plan

# Apply the changes (prompts for confirmation)
$ terraform apply

# Destroy all managed resources
$ terraform destroy

Terraform stores the current state of all managed resources in terraform.tfstate. For team environments, store this file remotely in an S3 bucket with DynamoDB locking to prevent concurrent state corruption:

1
2
3
4
5
6
7
8
9
10
# backend.tf
terraform {
  backend "s3" {
    bucket         = "cloud1-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "eu-west-3"
    dynamodb_table = "cloud1-state-lock"
    encrypt        = true
  }
}

Questions and answers

:question: What is idempotent infrastructure and why does it matter?

Idempotence means that applying a configuration multiple times produces the exact same end state as applying it once. If a resource already exists and matches the desired configuration, Terraform or Ansible will take no action. This property is critical for reliability — you can safely re-run terraform apply or ansible-playbook after a partial failure and the tool will only act on resources that need to be created or changed, never duplicating work.

:question: Why separate provisioning (Terraform) from configuration (Ansible)?

The separation respects distinct responsibilities. Terraform manages the lifecycle of immutable infrastructure — creating, modifying, and destroying cloud resources like VMs, networks, and storage. Ansible manages the state of software on existing machines — installing packages, writing config files, managing services. This boundary means you can reprovision infrastructure without touching application config, and vice versa. It also maps cleanly to different team ownership (platform team vs. application team).

:question: What is a Security Group in AWS and how does it differ from a NACL?

A Security Group is a stateful virtual firewall attached to EC2 instances. Stateful means it automatically tracks connection state — if you allow inbound traffic on port 80, the response traffic is automatically permitted without an explicit outbound rule. A Network Access Control List (NACL) is stateless and attached to a subnet — inbound and outbound rules must both be explicitly defined. Security Groups are the primary tool for EC2 firewall rules; NACLs add a coarser-grained second layer of defense.

:question: What is the Terraform state file and why must it be protected?

terraform.tfstate is a JSON file recording the current known state of all resources Terraform manages, including their IDs, attributes, and relationships. Terraform reads this file to calculate the diff between the current state and the desired state on each plan/apply. It often contains sensitive data (passwords, private IPs, ARNs). It must be protected from deletion (it cannot be reconstructed without re-importing every resource), protected from concurrent modification (use DynamoDB locking), and secured from unauthorized access (encrypt at rest in S3 with access controls).

:question: What is the difference between terraform plan and terraform apply?

terraform plan is a dry run — it reads the current state and the configuration, calculates what changes would be made (create, update, destroy), and prints a human-readable diff. No changes are made to any infrastructure. terraform apply executes the changes, first showing the same plan and asking for confirmation (or using -auto-approve to skip). Always run plan first in CI/CD or production environments to review changes before committing them.

:question: What is an Ansible handler and when is it triggered?

A handler is a task in Ansible that is only executed when notified by another task that actually made a change. For example, an Nginx configuration template task notifies a “Reload Nginx” handler. If the configuration file already exists with the correct content (no change), the notification is never sent, and Nginx is not needlessly reloaded. Handlers run once at the end of a play, even if notified multiple times — preventing redundant service restarts.

Ressources :

  • Terraform Documentation : https://developer.hashicorp.com/terraform/docs
  • Terraform AWS Provider : https://registry.terraform.io/providers/hashicorp/aws/latest/docs
  • Ansible Documentation : https://docs.ansible.com/
  • Ansible Best Practices : https://docs.ansible.com/ansible/latest/tips_tricks/ansible_tips_tricks.html
  • AWS VPC Guide : https://docs.aws.amazon.com/vpc/
  • AWS EC2 Getting Started : https://docs.aws.amazon.com/ec2/
  • Remote Terraform State with S3 : https://developer.hashicorp.com/terraform/language/settings/backends/s3
This post is licensed under CC BY 4.0 by the author.