Skip to content

Infrastructure: Terraform

infra/terraform/ is a Terraform module that provisions everything an EtcFS cluster runs on, on AWS: EC2 key pair, security group, a shared io2 Multi-Attach EBS volume (raw, never formatted), N compute nodes with etcd colocated, and the volume attachments.

It replaces scripts/infra/create-infra.sh and scripts/infra/destroy-infra.sh. Both paths still exist — pick one per cluster; they never touch the same account-wide resources (see "Coexistence" below).

Running EtcFS on Kubernetes instead of directly on EC2 is a separate root module, infra/terraform-eks/ — EKS control plane, worker nodes, the shared volume, EtcFS's own etcd and daemon pair, and the CSI driver, all in one apply. See that directory's README.md; nothing on this page applies to it.

A cluster that needs to grow and shrink on its own — an ASG rather than a fixed node_count — is infra/terraform-asg/, a third root module. Nodes join and leave without an operator's involvement: see Autoscaling.

What it does not do

Installing etcd, compiling the two EtcFS binaries on-node, and starting a fresh etcd cluster on every node at once are ordered, imperative steps — etcd has to be up before the daemon starts, and a joining node needs etcd member add before it starts at all. Expressing that as Terraform provisioners would reintroduce a bug class this repo already paid to fix once (see scripts/infra/bootstrap-cluster.sh's header). So: Terraform stops at infrastructure, and scripts/infra/bootstrap-cluster.sh installs and starts the software on top of it.

Prerequisite: the fencing instance profile

Once per AWS account, before the first terraform apply:

./scripts/infra/fencing-iam.sh create

This creates the permanent etcfs-nodes IAM role and instance profile (ec2:DetachVolume/AttachVolume on the shared volume, read-only Describe*). The Terraform module references it via a data source rather than managing it — an identity that can create EC2 instances often cannot also iam:CreateRole/iam:TagRole, and the role is meant to outlive any single cluster's Terraform state anyway.

Without it the daemon degrades to single-signal fencing: a fenced node stops publishing metadata, but nothing stops it writing bytes to the shared device. See Fencing and split-brain avoidance.

Usage

# everything: terraform apply, export state, bootstrap the cluster software
./scripts/infra/tf-up.sh

# infrastructure only, bootstrap by hand later
./scripts/infra/tf-up.sh --no-bootstrap

# override any variable — anything after -- goes straight to `terraform apply`
./scripts/infra/tf-up.sh -- -var node_count=5 -var instance_type=t3.large

# teardown
terraform -chdir=infra/terraform destroy

Or drive Terraform directly and hand off manually:

terraform -chdir=infra/terraform init
terraform -chdir=infra/terraform apply
./scripts/infra/tf-export-state.sh                    # -> infra-state.json
./scripts/infra/bootstrap-cluster.sh infra-state.json
./scripts/infra/run-full-test.sh

tf-export-state.sh writes infra-state.json in exactly the shape scripts/infra/state.sh reads, so setup-compute.sh, run-full-test.sh, benchmark.sh and the chaos scripts all work unchanged against a Terraform-provisioned cluster.

Variables

Variable Default Notes
region eu-west-1
cluster_name etcfuse Tags every resource ClusterName=<this>; also the key pair / SG / role name prefix.
az eu-west-1a Nodes and the shared volume must share an AZ.
node_count 3 Minimum 3 for etcd quorum. for_each over fixed keys, not count — growing it doesn't renumber or replace existing nodes.
instance_type t3.medium
volume_size_gb / volume_iops 10 / 100 io2 Multi-Attach, raw.
key_name derived: <cluster_name>-key Not the account-wide etcfuse-keypair the bash scripts import — Terraform owns this one's full lifecycle, including deleting it on destroy.
public_key_path ~/.ssh/id_ed25519.pub
subnet_id auto-resolved in az
ssh_ingress_cidr auto-detected caller IP
instance_profile etcfs-nodes Must already exist — see Prerequisite above.

Coexistence with the bash path

Every Terraform-managed name is cluster-scoped (<cluster_name>-key, <cluster_name>-sg), so a Terraform cluster never adopts or deletes the account-wide etcfuse-keypair that create-infra.sh imports. Run both paths at once by giving them different cluster_name values. The etcfs-nodes instance profile is the one thing shared between the two, read-only from Terraform's side.

State

Local backend (infra/terraform/terraform.tfstate), gitignored. A remote backend (S3 + a DynamoDB lock table) only earns its keys once more than one person applies against the same cluster.

Adding a node

Raising node_count and re-applying brings up the new instance and attaches the shared volume, but the new node still has to join etcd — that's etcd member add, which stays in scripts/infra/add-compute-node.sh (unlike create-infra.sh, not yet mirrored into Terraform).