Skip to content
The witness on AWS

The witness on AWS

Provisions one small EC2 instance running the qfence witness (§8/§57): an ordinary raft voter with no provider configuration, no fencing credentials, and no Resource Group definitions. Free, OSS, part of the core — this is not the vendor-hosted witness product.

This is the only cloud this project ships a Terraform module for today. It passes terraform validate and terraform fmt -check against the real hashicorp/aws provider. terraform plan was also attempted with dummy credentials (AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY set to well-known placeholder values) to check how far the configuration gets without real access: it correctly reaches AWS’s STS endpoint and is rejected with a real 403 InvalidClientTokenId — i.e. the module’s shape and the provider’s request construction are sound, and real AWS credentials are the only thing standing between this and an actual apply. It has never been run against a real AWS account, so treat it as a strongly-checked starting point, not a field-proven artifact, and review it yourself before using it against real infrastructure.

What the first boot does

cloud-init reads the instance’s public address from the instance metadata service. It then builds, installs and starts the witness as the Proxmox witness’s first boot does, so the instance needs outbound HTTPS to your git host, go.dev and proxy.golang.org. Those steps have been run on Ubuntu 22.04, the AMI’s OS, in a container; the metadata lookup has not been run outside AWS. Raft runs in plaintext unless all three voters get certificates.

Usage

module "witness" {
  source = "github.com/REPLACE_ME/qfence//deploy/witness/terraform/aws"

  aws_region          = "us-west-2"   # pick a region independent of either site
  subnet_id           = "subnet-xxxx"
  key_name            = "your-ssh-key"
  allowed_raft_cidrs  = ["203.0.113.10/32", "198.51.100.20/32"]  # your two sites
  allowed_ssh_cidrs   = ["203.0.113.0/24"]                        # your admin range

  qfence_repo_url = "https://github.com/REPLACE_ME/qfence.git"
  qfence_repo_ref = "v1.0.0"  # pin a real tag/commit, not a moving branch

  witness_id    = "witness"
  witness_peers = [
    "dc1=10.0.0.1:7000",
    "dc2=10.0.0.2:7000",
    "witness=0.0.0.0:7000", # placeholder — see the two-step apply below
  ]
  witness_bootstrap = true # when forming a brand-new cluster with both sites
}

Because this module doesn’t pre-allocate an Elastic IP, the witness’s real advertised address is only known after it boots. Either:

  1. Two-step apply (simplest): apply once with a placeholder in witness_peers, read the real address from terraform output public_ip, update witness_peers (and your two sites’ own peer config) with it, then apply again. cloud-init only runs on first boot, so a second apply that only changes user_data will not retroactively fix an already-running instance — you would need to replace it. Do this before the sites start: the cluster forms with the peer list its voters first start with.
  2. Pre-allocate an Elastic IP (add an aws_eip resource bound to aws_instance.witness, referencing its .public_ip output instead of the instance’s own auto-assigned one) so the address is stable and known before the instance even boots. Recommended for anything beyond a quick test — not included in this base module to keep it legible, but a natural first extension.

Porting to another cloud

Nothing here is AWS-specific in spirit — §8/§57 explicitly want the witness “ideally a different cloud/region than either site” — only this module’s HCL resources are AWS-specific. To port to GCP, Azure, DigitalOcean, or anywhere else with cloud-init support:

  • Replace aws_instance/aws_security_group/the AWS AMI data source with that cloud’s equivalent compute instance + firewall/security-group resources (a small VM is all that’s needed — see variables.tf’s instance_type comment on sizing).
  • Reuse ../cloud-init.tpl.yaml as-is — it only assumes cloud-init and a metadata service reachable at a well-known address for discovering the instance’s own public IP. Adjust the metadata service URL and request shape in the runcmd block: this file uses AWS’s IMDSv2 (169.254.169.254 with a token header) — GCP’s metadata server uses a different URL and a Metadata-Flavor: Google header instead; Azure’s IMDS uses yet another path and header. Everything else (installing Go, cloning the repo, running install.sh, writing witness.env, starting the service) is cloud-agnostic.
  • If your target has no cloud-init support, use the Ansible role in ../../ansible/ instead — it only assumes SSH and systemd.