Terraform: The Infrastructure Layer
Last updated on 2026-07-30 | Edit this page
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.
- Run
terraform planand interpret the output.
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 so Tim and Jamie can work
independently without affecting each other.
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
Tim’s apply and Jamie’s apply cannot see or
affect each other’s state at all – they are backed by different buckets.
This is deliberate: it means destroying or rebuilding one operator’s
environment can’t touch the other’s, which matters a lot given how often
make rebuild tears an environment down and recreates it
(Episode 6). Both backends still use a shared DynamoDB table
(terraform-locks) for locking, which prevents two
terraform apply runs against the same state from
racing each other – but that only protects an operator against
themselves (e.g. two terminal tabs), not against each other.
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) – which is 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 (Episode 2).
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
From terraform-dataverse/environments/tim, run
terraform plan.
Answer these questions from the output:
- How many resources does Terraform plan to create, change, or destroy?
- Which resource type appears the most?
- Find the security group resource. What ports does it open, and to what CIDR range?
The exact output will depend on current state. Things to look for:
-
aws_instance(the EC2 instance) -
aws_security_groupandaws_security_group_rule(firewall rules) -
aws_eipandaws_eip_association(Elastic IP) -
aws_db_instance(RDS) - Port 22 (SSH), 80 (HTTP), 443 (HTTPS), and 8080/4848 (Payara) in the security group rules
State isolation and IP stability
Without checking the episode:
- Can Tim accidentally destroy Jamie’s environment by running
terraform destroyin his own environment directory? Point to the specific piece of config that proves your answer. - Does today’s public EC2 IP survive
make rebuild ENV=tim? Why or why not?
- No – each environment’s
main.tfpoints at a different S3 bucket/key for its backend (ucla-tim-terraform-statevs.ucla-library-terraform-state, different keys). Terraform only knows about resources tracked in the state it’s pointed at, so Tim’sdestroyhas no way to reach anything in Jamie’s state file. - 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).