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:
- Two-step apply (simplest): apply once with a placeholder in
witness_peers, read the real address fromterraform output public_ip, updatewitness_peers(and your two sites’ own peer config) with it, then apply again. cloud-init only runs on first boot, so a secondapplythat only changesuser_datawill 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. - Pre-allocate an Elastic IP (add an
aws_eipresource bound toaws_instance.witness, referencing its.public_ipoutput 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 — seevariables.tf’sinstance_typecomment on sizing). - Reuse
../cloud-init.tpl.yamlas-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 theruncmdblock: this file uses AWS’s IMDSv2 (169.254.169.254with a token header) — GCP’s metadata server uses a different URL and aMetadata-Flavor: Googleheader instead; Azure’s IMDS uses yet another path and header. Everything else (installing Go, cloning the repo, runninginstall.sh, writingwitness.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.