|
| 1 | +# gitlawb node on AWS (Terraform) |
| 2 | + |
| 3 | +Single EC2 instance running the published node image + Postgres via Docker |
| 4 | +compose, with a persistent encrypted EBS volume, Elastic IP, SSM access, and |
| 5 | +daily snapshots. |
| 6 | + |
| 7 | +```text |
| 8 | +Elastic IP ──► EC2 t4g.small (Amazon Linux 2023, arm64) |
| 9 | + 7545/tcp docker compose: |
| 10 | + 7546/udp ├─ node (ghcr.io/gitlawb/node, pulled — not built) |
| 11 | + └─ postgres:16-alpine |
| 12 | + EBS gp3 volume mounted at /mnt/data |
| 13 | + ├─ node/ → container /data (repos + identity key) |
| 14 | + └─ postgres/ → postgres data dir |
| 15 | +``` |
| 16 | + |
| 17 | +## Prerequisites |
| 18 | + |
| 19 | +- Terraform ≥ 1.6 |
| 20 | +- AWS credentials configured (`aws sts get-caller-identity` works) |
| 21 | +- AWS CLI + [Session Manager plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) (for shell access) |
| 22 | +- A default VPC in the target region (or pass `subnet_id`) |
| 23 | + |
| 24 | +## Quick start |
| 25 | + |
| 26 | +```sh |
| 27 | +cd infra/aws |
| 28 | +cp terraform.tfvars.example terraform.tfvars # edit: public_url at minimum |
| 29 | +terraform init |
| 30 | +terraform plan |
| 31 | +terraform apply # ⚠ creates billable resources (~$25/mo: EC2 + EBS + snapshots) |
| 32 | +``` |
| 33 | + |
| 34 | +After apply (~3-5 min for first boot to pull images and start): |
| 35 | + |
| 36 | +```sh |
| 37 | +curl "$(terraform output -raw api_url)/health" |
| 38 | +``` |
| 39 | + |
| 40 | +## ⚠ First boot: back up the identity key |
| 41 | + |
| 42 | +The node generates `/data/keys/identity.pem` on first start — it defines the |
| 43 | +node's DID. **Losing it permanently changes the node's identity.** Back it up |
| 44 | +immediately: |
| 45 | + |
| 46 | +```sh |
| 47 | +$(terraform output -raw ssm_session_command) |
| 48 | +# in the session: |
| 49 | +sudo cat /mnt/data/node/keys/identity.pem |
| 50 | +``` |
| 51 | + |
| 52 | +Store the key somewhere safe (password manager / offline). The volume's |
| 53 | +`prevent_destroy` guard and daily DLM snapshots protect against accidents, but |
| 54 | +are not a substitute for an offline backup. |
| 55 | + |
| 56 | +## Shell access |
| 57 | + |
| 58 | +SSM Session Manager — no SSH port, no keys to manage: |
| 59 | + |
| 60 | +```sh |
| 61 | +$(terraform output -raw ssm_session_command) |
| 62 | +``` |
| 63 | + |
| 64 | +Bootstrap log: `/var/log/gitlawb-bootstrap.log`. Stack lives in `/opt/gitlawb` |
| 65 | +(`docker compose ps`, `docker compose logs node`). |
| 66 | + |
| 67 | +SSH is off by default; set `ssh_ingress_cidr` + `ssh_key_name` if you need it. |
| 68 | + |
| 69 | +## Upgrading the node |
| 70 | + |
| 71 | +User-data only runs at first boot, so upgrades go through SSM: |
| 72 | + |
| 73 | +```sh |
| 74 | +$(terraform output -raw upgrade_command) |
| 75 | +``` |
| 76 | + |
| 77 | +This runs `docker compose pull && docker compose up -d` on the instance. |
| 78 | + |
| 79 | +- With `image_tag = "latest"` (default) that picks up the newest release. |
| 80 | +- With a **pinned tag**, first edit the tag in `/opt/gitlawb/compose.yaml` on |
| 81 | + the instance (via SSM session), then run the upgrade command — and keep |
| 82 | + `image_tag` in terraform.tfvars in sync so a future instance replacement |
| 83 | + boots the same version. |
| 84 | + |
| 85 | +Replace the instance itself (OS/AMI/instance-type changes) with |
| 86 | +`terraform apply -replace=aws_instance.node` — the data volume reattaches and |
| 87 | +`/data` (including the identity key) survives. |
| 88 | + |
| 89 | +## Changing configuration |
| 90 | + |
| 91 | +User-data only runs at first boot, and the instance ignores `user_data` drift |
| 92 | +(`ignore_changes`), so editing terraform.tfvars values that feed the bootstrap |
| 93 | +(`bootstrap_peers`, `public_url`, integrations, `image_tag`) does **not** |
| 94 | +affect a running instance on `terraform apply`. To roll out such changes, |
| 95 | +either edit `/opt/gitlawb/.env` on the instance (SSM session, then |
| 96 | +`docker compose up -d`), or replace the instance: |
| 97 | + |
| 98 | +```sh |
| 99 | +terraform apply -replace=aws_instance.node |
| 100 | +``` |
| 101 | + |
| 102 | +The data volume reattaches; repos, postgres data, and the identity key survive. |
| 103 | + |
| 104 | +## Remote state (optional) |
| 105 | + |
| 106 | +Local state is the default. To move state to S3: create a versioned bucket, |
| 107 | +uncomment the `backend "s3"` block in `versions.tf`, then: |
| 108 | + |
| 109 | +```sh |
| 110 | +terraform init -migrate-state |
| 111 | +``` |
| 112 | + |
| 113 | +## Teardown |
| 114 | + |
| 115 | +`terraform destroy` will **fail on the data volume by design** |
| 116 | +(`prevent_destroy`). To tear everything down: |
| 117 | + |
| 118 | +1. Back up the identity key (above) and take a final snapshot if you may return. |
| 119 | +2. Remove the `prevent_destroy` line from `aws_ebs_volume.data` in `main.tf`. |
| 120 | +3. `terraform destroy`. |
| 121 | + |
| 122 | +Note: DLM snapshots created by the policy are not deleted by destroy — clean |
| 123 | +them up in the EC2 console if unwanted. The Elastic IP is released on destroy. |
| 124 | + |
| 125 | +## Security notes |
| 126 | + |
| 127 | +- Postgres password: generated by Terraform, stored as an SSM SecureString, |
| 128 | + fetched at boot via the instance profile — never in user-data or state-free |
| 129 | + files on disk (only in `/opt/gitlawb/.env`, mode 600). It IS in Terraform |
| 130 | + state — treat state as sensitive (another reason for the S3 backend). |
| 131 | +- Sensitive optional vars (`operator_private_key`, `pinata_jwt`, |
| 132 | + `s3_access_key_id`, `s3_secret_access_key`) follow the same SSM path. |
| 133 | +- SSM secrets use the AWS-managed `aws/ssm` key by default; set |
| 134 | + `ssm_kms_key_id` to encrypt with a customer-managed KMS key instead (the |
| 135 | + instance role is granted `kms:Decrypt` on that key automatically). |
| 136 | +- IMDSv2 is required; metrics port is closed unless `metrics_ingress_cidr` is set. |
| 137 | +- The node serves plain HTTP on 7545. For TLS, put a DNS name + proxy |
| 138 | + (ALB/CloudFront/Caddy) in front and set `public_url` accordingly. |
0 commit comments