
This post covers two topics: Terraform module design with GCS remote state, and Ansible's core structure with idempotency verification. The two are complementary — Terraform provisions infrastructure, Ansible configures servers internally.

## Factoring Reusable Infrastructure Units

### Avoiding Copy-Paste Across Environments

Without modules, the same VPC logic is written separately for dev and prod in Terraform. Change one, forget the other, and problems arise. Modules work like functions: define once, use with different parameters.

Target structure:

```text
terraform/
├── modules/
│   └── vpc/
│       ├── variables.tf    # inputs
│       ├── main.tf         # resource definitions
│       └── outputs.tf      # outputs
└── environments/
    ├── dev/
    │   ├── backend.tf
    │   ├── variables.tf
    │   ├── terraform.tfvars
    │   └── main.tf
    └── prod/
        └── ...
```

`modules/` defines reusable infra units. `environments/` handles actual deployment, passing in different variable values.

### Input Declarations

```hcl
variable "project_id" {
  type = string
}

variable "region" {
  type    = string
  default = "us-east1"
}

variable "vpc_name" {
  type = string
}

variable "subnet_name" {
  type = string
}

variable "subnet_cidr" {
  type = string
}
```

### Resource Definitions

```hcl
resource "google_compute_network" "vpc" {
  name                    = var.vpc_name
  auto_create_subnetworks = false
  project                 = var.project_id
}

resource "google_compute_subnetwork" "subnet" {
  name          = var.subnet_name
  network       = google_compute_network.vpc.id
  region        = var.region
  ip_cidr_range = var.subnet_cidr
  project       = var.project_id
}

resource "google_compute_firewall" "allow_internal" {
  name    = "${var.vpc_name}-allow-internal"
  network = google_compute_network.vpc.name
  project = var.project_id

  allow {
    protocol = "tcp"
  }
  allow {
    protocol = "udp"
  }
  allow {
    protocol = "icmp"
  }

  source_ranges = [var.subnet_cidr]
}

resource "google_compute_firewall" "allow_ssh" {
  name    = "${var.vpc_name}-allow-ssh"
  network = google_compute_network.vpc.name
  project = var.project_id

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  source_ranges = ["0.0.0.0/0"]
}
```

### Exported Values

```hcl
output "vpc_id" {
  value = google_compute_network.vpc.id
}

output "vpc_name" {
  value = google_compute_network.vpc.name
}

output "subnet_id" {
  value = google_compute_subnetwork.subnet.id
}
```

### Non-Production Workspace Instantiation

```hcl
provider "google" {
  project = var.project_id
  region  = var.region
}

module "vpc" {
  source      = "../../modules/vpc"
  project_id  = var.project_id
  region      = var.region
  vpc_name    = "devops-vpc"
  subnet_name = "devops-subnet"
  subnet_cidr = "10.0.1.0/24"
}
```

### Concrete Variable Assignments

```hcl
project_id = "devops-lab-lou-2026"
region     = "us-east1"
```

The prod environment calls the same module, just passing different vpc_name and subnet_cidr. The module itself remains unchanged.

## Distributed Snapshot Backend and Write-Lock Guard

### On-Disk Snapshots Cannot Serve Multiple Contributors

Terraform's state file records "what currently exists on GCP." Without remote state, the state only exists locally:

- Local machine lost = all infrastructure information gone
- Multi-person collaboration = each person has their own state copy, conflicting with each other

GCS backend stores state centrally in a GCS bucket, shared by everyone.

### Preventing Simultaneous Apply Races

If two people run `terraform apply` simultaneously, both read the same state, each calculates a diff, and each applies changes — unpredictable results.

State locking means the first `apply` locks the state after starting, and the second person must wait for the lock to be released. This corresponds to the Consistency in CAP Theorem: in a distributed system, two operations cannot simultaneously modify the same thing.

```hcl
# environments/dev/backend.tf
terraform {
  backend "gcs" {
    bucket = "devops-lab-lou-2026-tfstate"
    prefix = "environments/dev"
  }
}
```

GCS backend natively supports locking, implemented via GCS object's `generation` mechanism. No additional configuration needed.

Initialization:

```sh
terraform init
terraform plan
terraform apply
```

The reason `plan` and `apply` should be separate: `plan` shows "what will happen," `apply` actually executes. In production environments, plan results are typically reviewed before applying.

### Querying and Reordering Snapshot Entries

```sh
# list all resources tracked in state
terraform state list

# show detail of a specific resource
terraform state show module.vpc.google_compute_network.vpc

# move resource to a different address (e.g. refactor module path)
terraform state mv old_address new_address

# remove resource from state without destroying it
terraform state rm module.vpc.google_compute_network.vpc
```

`state rm` does not delete the resource on GCP — it only makes Terraform stop tracking it. On the next apply, Terraform thinks the resource doesn't exist and may attempt to recreate it. So `state rm` is typically paired with `terraform import`: first remove, then re-import to a new address.

## Host Configuration via Agent Playbooks

### Provisioning Tool Versus Configuration Tool

| | Terraform | Ansible |
|--|-----------|---------|
| Purpose | Provision infrastructure | Configure servers internally |
| Language | HCL (declarative) | YAML playbook |
| State | State file | No state, re-checks each time |
| Connection method | API call | SSH (agentless) |
| Idempotency | Naturally idempotent | Requires correct playbook writing |

Ansible agentless means: target machines don't need any Ansible agent installed, only SSH and Python. Ansible runs on the control machine (your laptop or CI runner), connecting to target machines via SSH to execute operations.

In modern K8s environments, Ansible's role has diminished because K8s YAML replaces much of the configuration work. But VM and on-premise environments are still very common.

### Inventory, Module, Task, Playbook, Role

**Inventory**: Which machines you manage, grouped for organization.

```ini
[local]
localhost ansible_connection=local

[webservers]
web1.example.com
web2.example.com

[databases]
db1.example.com ansible_user=ubuntu
```

**Module**: Ansible's built-in operation units, e.g., `file`, `copy`, `apt`, `service`. Modules are the foundation of idempotency — each module checks target state before executing. If state already matches, it does nothing.

**Task**: One step, calling one module.

**Playbook**: A combination of one or more tasks, defining what to execute on which hosts.

**Role**: A collection of multiple tasks, reusable, similar to Terraform modules. Suitable for encapsulating reusable logic like nginx installation or prometheus configuration.

### Minimal YAML Example

```yaml
---
- name: localhost practice
  hosts: local
  tasks:
    - name: create practice directory
      file:
        path: /tmp/ansible-practice
        state: directory
        mode: "0755"

    - name: write config file
      copy:
        dest: /tmp/ansible-practice/config.txt
        content: |
          env=dev
          version=1.0

    - name: show file content
      command: cat /tmp/ansible-practice/config.txt
      register: file_content

    - name: print result
      debug:
        msg: "{{ file_content.stdout }}"
```

```sh
# check connectivity
ansible local -i inventory.ini -m ping

# run playbook
ansible-playbook -i inventory.ini playbook.yml
```

### Confirming No-Op on Re-Run

First run:

```text
TASK [create practice directory] ... changed
TASK [write config file]         ... changed
TASK [show file content]         ... changed
TASK [print result]              ... ok
```

Second run (without changing anything):

```text
TASK [create practice directory] ... ok      # already exists, no action
TASK [write config file]         ... ok      # content unchanged, no action
TASK [show file content]         ... changed # command module always runs
TASK [print result]              ... ok
```

`file` and `copy` modules are idempotent. The second run showing `ok` means no changes were made. `command` module is not idempotent — it always executes. This is a design point requiring attention. Idempotent command operations need `creates` or `when` condition controls.

## References

- [Terraform Modules documentation](https://developer.hashicorp.com/terraform/language/modules)
- [Terraform GCS Backend](https://developer.hashicorp.com/terraform/language/backend/gcs)
- [Terraform State Commands](https://developer.hashicorp.com/terraform/cli/commands/state)
- [CAP Theorem - Wikipedia](https://en.wikipedia.org/wiki/CAP_theorem)
- [Ansible Getting Started](https://docs.ansible.com/ansible/latest/getting_started/index.html)
- [Ansible Module Index](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/index.html)
- [Ansible Idempotency](https://docs.ansible.com/ansible/latest/reference_appendices/glossary.html#term-Idempotency)
