Terraform: The Infrastructure Layer

Last updated on 2026-10-04 | Edit this page

Callout

Dataverse extension: case study

This is outside the six-episode public core. Operational commands and historical status descriptions are examples for analysis, not a current production runbook. No AWS or private access is required to discuss the included material. Only authorized maintainers using a reviewed, current runbook should operate the actual service. Do not run these commands as workshop exercises.

Overview

Questions

  • What AWS resources does Terraform manage for this project?
  • How is Terraform state stored and shared between operators?
  • How are Tim’s and Jamie’s environments organized?

Objectives

  • Read a Terraform config and identify the AWS resource types it defines.
  • Explain what remote state is and why it matters for a multi-operator project.
  • Describe what an Elastic IP is and why the one defined here doesn’t yet simplify the rebuild cycle.
  • Interpret an illustrative Terraform plan excerpt without AWS access.

What Terraform manages


Terraform is responsible for the AWS resources that exist: the “what” of the infrastructure. For this project, those resources are:

  • EC2 instance: the virtual machine where Dataverse and its supporting services run
  • RDS instance: the managed PostgreSQL database
  • S3 buckets: file storage for Dataverse, plus a separate bucket for migration assets and backups
  • Security groups: firewall rules controlling what traffic reaches the EC2 instance
  • Elastic IP: a static IP address attached to the EC2 instance (more on this below)
  • IAM roles and policies: permissions for the EC2 instance to read and write S3

Terraform does not install software. It creates the resources. Once the EC2 instance exists, Ansible takes over and configures it.

Repository layout


The terraform-dataverse repo is organized by operator environment:

terraform-dataverse/
  environments/
    tim/        <- Tim's dev environment
    jamie/      <- Jamie's dev environment
  modules/      <- shared resource definitions used by both environments

Each environment directory has its own main.tf, variables.tf, and terraform.tfvars. The environments are mostly identical (they share modules) but use different resource names and sizes to support independent work. Resource ownership and permissions must also be checked before assuming isolation.

Remote state


By default, Terraform stores its state in a local file called terraform.tfstate. This is a problem for a team: if two people run Terraform from different machines, their state files diverge and Terraform loses track of what actually exists in AWS.

This project uses remote state, but each operator has their own bucket and key, not a shared one:

Tim:   s3://ucla-tim-terraform-state/terraform-dataverse/tim/terraform.tfstate
Jamie: s3://ucla-library-terraform-state/terraform-dataverse/jamie/terraform.tfstate

Separate bucket/key backends separate state bookkeeping, not necessarily resources or permissions. Two states can mistakenly track the same resource, reference shared resources, or use credentials authorized to change both environments. Isolation also requires distinct resource ownership, correct configuration, and scoped IAM permissions. A shared lock table serializes writers to the same state identity; it does not protect different states from changing the same cloud resource.

Diagram: Tim's environment applies to S3 bucket ucla-tim-terraform-state, and Jamie's environment applies to S3 bucket ucla-library-terraform-state. Both environments take a lock against a shared DynamoDB table called terraform-locks before applying.
Tim and Jamie’s operator environments each apply against their own S3 state bucket. Both share a DynamoDB lock table, so the shared table prevents same-operator races but not cross-operator conflicts.

s3://ucla-dataverse-migration-assets/ is a different bucket entirely: it holds database dumps and migration assets, not Terraform state. Don’t confuse the two.

Callout

Never edit the state file directly

The state file is JSON and technically editable, but Terraform is the only thing that should write to it. Editing it by hand can put Terraform in an inconsistent state that is painful to recover from. If state gets out of sync, use terraform state subcommands to inspect and repair it.

Elastic IP


An Elastic IP (EIP) is a static IP address you reserve in AWS and attach to an EC2 instance.

Without an Elastic IP, every time you run make rebuild (which destroys and recreates the EC2 instance), the instance gets a new public IP address. That means:

  • The Ansible inventory file needs to be updated before Ansible can run
  • Any DNS records pointing at the old IP are wrong
  • You spend time tracking down the current IP instead of doing work

In principle, an Elastic IP fixes this: reserve the address once, reassociate it with whatever instance exists, and the address survives even when the instance doesn’t.

That’s not what happens today, though. The aws_eip.dataverse resource in modules/dataverse_ec2/main.tf is defined in the same module as the EC2 instance, associated directly to aws_instance.dataverse.id. When make rebuild runs terraform destroy, it tears down the whole module, instance and EIP together, so the address does not survive a rebuild yet. Roadmap item 02-01 (“Elastic IP resource… for both environments”) is still unchecked for exactly this reason: having an aws_eip resource isn’t the same as having a persistent one. The real make rebuild output today prints a new IP and pauses for you to update the DNS A record by hand (Makefile, the rebuild target). That’s the friction this feature is meant to remove and hasn’t yet.

Variables and tfvars


Terraform configurations use variables to avoid hardcoding values that differ between environments.

  • variables.tf: declares the variables and their types (like a function signature)
  • terraform.tfvars: provides values for those variables (like the function call)

The terraform.tfvars file for each environment is gitignored, not committed. Each operator creates their own from terraform.tfvars.example during setup (Tooling setup). That’s intentional, because in practice it’s not secret-free: Tim’s real tfvars includes a plaintext db_password. This is a known, flagged gap (the security audit calls it out as finding F8): the intended design keeps secrets in Ansible Vault, not Terraform, but db_password currently lives in both places, and the Terraform copy isn’t encrypted. Don’t treat “it’s gitignored” as equivalent to “it’s safe”: gitignore keeps a file out of version control, it doesn’t encrypt what’s on disk.

Challenge

Read a Terraform plan

Read this simplified, fictional plan excerpt. Do not run Terraform:

+ aws_instance.example
+ aws_security_group.example
    ingress: tcp 22 from 192.0.2.0/24
    ingress: tcp 443 from 0.0.0.0/0
Plan: 2 to add, 0 to change, 0 to destroy.
  1. How many resources would be created, changed, or destroyed?
  2. Which source networks could reach SSH and HTTPS?
  3. Does this excerpt establish isolation from another environment?

Two resources would be created. SSH is limited to the example documentation network 192.0.2.0/24; HTTPS permits all IPv4 sources. Neither this excerpt nor a separate state key establishes isolation: inspect full ownership, references, credentials, and policies. A plan excerpt is analysis, not permission to apply.

Challenge

State isolation and IP stability

Without checking the episode:

  1. Can Tim accidentally destroy Jamie’s environment by running terraform destroy in his own environment directory? Is a different backend bucket sufficient evidence?
  2. Does today’s public EC2 IP survive make rebuild ENV=tim? Why or why not?
  1. You cannot conclude that from backend separation. Check for shared or overlapping resource IDs, cross-environment references, and IAM permissions. Separate states do not prevent one operator from affecting shared resources.
  2. No. aws_eip.dataverse lives in the same module as aws_instance.dataverse and is associated directly to it, so terraform destroy removes both together. The address changes on every rebuild until roadmap item 02-01 makes the EIP persistent.
Key Points
  • Terraform manages EC2, RDS, S3, security groups, an Elastic IP resource, and IAM for this project.
  • State is stored remotely in S3, but Tim and Jamie each have their own bucket/key: state is isolated per operator, not shared.
  • Each operator has their own environment directory; both use shared modules.
  • An Elastic IP resource exists, but it’s tied to the instance’s lifecycle, so it does not yet survive make rebuild. That’s still open work (roadmap 02-01).
  • terraform.tfvars is gitignored per environment, but is not currently secret-free in practice: a known gap (audit F8).