Terraform: The Infrastructure Layer
Last updated on 2026-10-04 | Edit this page
Estimated time: 35 minutes
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.
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.
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.
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.
- How many resources would be created, changed, or destroyed?
- Which source networks could reach SSH and HTTPS?
- 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.
State isolation and IP stability
Without checking the episode:
- Can Tim accidentally destroy Jamie’s environment by running
terraform destroyin his own environment directory? Is a different backend bucket sufficient evidence? - Does today’s public EC2 IP survive
make rebuild ENV=tim? Why or why not?
- 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.
- No.
aws_eip.dataverselives in the same module asaws_instance.dataverseand is associated directly to it, soterraform destroyremoves both together. The address changes on every rebuild until roadmap item02-01makes the EIP persistent.
- 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 (roadmap02-01). -
terraform.tfvarsis gitignored per environment, but is not currently secret-free in practice: a known gap (audit F8).