Skip to content

Instantly share code, notes, and snippets.

@koliadych
Last active May 11, 2026 09:22
Show Gist options
  • Select an option

  • Save koliadych/870a61eb5fa33e80597854723e19cb15 to your computer and use it in GitHub Desktop.

Select an option

Save koliadych/870a61eb5fa33e80597854723e19cb15 to your computer and use it in GitHub Desktop.
fintechner deploy app to tokyo EC2
#!/bin/bash
# fintechner — build connector + dashboard in CloudShell and ship to EC2.
# Idempotent: re-running re-builds + re-installs.
set -euo pipefail
REGION=ap-northeast-1
INSTANCE_ID=i-089ad4a6b76132ba2
WORKDIR=$HOME/work/fintechner
TMPDIR=$(mktemp -d)
S3_BUCKET="fintechner-deploy-$(date +%s)-$(openssl rand -hex 4 2>/dev/null || echo $$)"
cd "$HOME"
# Redirect Go and npm caches to /tmp — CloudShell's $HOME is only 1 GB
# but /tmp has several GB of ephemeral space.
export GOPATH=/tmp/go
export GOMODCACHE=/tmp/go/pkg/mod
export GOCACHE=/tmp/go/build-cache
export NPM_CONFIG_CACHE=/tmp/npm-cache
mkdir -p "$GOPATH" "$GOMODCACHE" "$GOCACHE" "$NPM_CONFIG_CACHE"
# Free any prior $HOME caches that might still take up space.
rm -rf "$HOME/go" "$HOME/.npm" 2>/dev/null || true
# Trap only removes the build temp + S3 bucket. /tmp/go and /tmp/npm-cache
# stay — they're free space we want to reuse on re-run, and Go module
# files are chmod 0444 which makes blanket rm noisy.
trap 'rm -rf "$TMPDIR" 2>/dev/null; aws --region "$REGION" s3 rb "s3://$S3_BUCKET" --force >/dev/null 2>&1 || true' EXIT
if [ ! -d "$WORKDIR/.git" ]; then
echo "ERROR: $WORKDIR not a git checkout — run 02-tf-plan.sh first."
exit 1
fi
cd "$WORKDIR"
# CloudShell wipes /usr/bin/gh on session restart but ~/.gitconfig keeps a
# credential.helper pointing at it — that prompts for a password. Reinstall
# gh first, then re-bind the helper.
if ! command -v gh >/dev/null 2>&1; then
echo " reinstalling gh..."
sudo curl -fsSL https://cli.github.com/packages/rpm/gh-cli.repo -o /etc/yum.repos.d/gh-cli.repo
sudo dnf install -y gh
fi
# Strip stale credential helpers (multi-line array) and let gh re-add the right one.
git config --global --unset-all credential.helper 2>/dev/null || true
gh auth setup-git --hostname github.com >/dev/null 2>&1 || true
echo " pulling latest feature/tokyo-deploy..."
git fetch origin feature/tokyo-deploy
git checkout feature/tokyo-deploy
git pull --ff-only origin feature/tokyo-deploy
echo " HEAD: $(git rev-parse --short HEAD) — $(git log -1 --pretty=%s)"
echo "=== Installing build tools ==="
# CloudShell's bundled Go is 1.25 and GOSUMDB=off blocks toolchain auto-download.
# Install Go 1.26 directly from go.dev into /tmp (ephemeral, no $HOME pressure).
GO_VERSION=1.26.0
if [ ! -x /tmp/go-1.26/bin/go ]; then
ARCH=amd64; [ "$(uname -m)" = "aarch64" ] && ARCH=arm64
echo "Installing Go ${GO_VERSION} (${ARCH}) into /tmp/go-1.26..."
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${ARCH}.tar.gz" -o /tmp/go.tgz
rm -rf /tmp/go-1.26 && mkdir -p /tmp/go-1.26
tar -xzf /tmp/go.tgz -C /tmp/go-1.26 --strip-components=1
rm /tmp/go.tgz
fi
export PATH=/tmp/go-1.26/bin:$PATH
unset GOTOOLCHAIN GOSUMDB GOPROXY # let Go pick safe defaults
export GOTOOLCHAIN=local # use the binary we just installed, don't fetch more
if ! command -v node >/dev/null; then
sudo dnf install -y nodejs20 >/dev/null 2>&1 || sudo dnf install -y nodejs >/dev/null
fi
echo " Go: $(go version)"
echo " Node: $(node --version)"
echo " npm: $(npm --version)"
echo
echo "=== Building connector for linux/arm64 (auto-fetches Go 1.26 if needed) ==="
cd "$WORKDIR/code/connector"
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 \
go build -ldflags="-w -s" -o "$TMPDIR/connector" ./cmd/connector
echo " connector binary: $(du -h "$TMPDIR/connector" | cut -f1)"
echo
echo "=== Building dashboard (npm ci + next build, 2-4 min) ==="
# Copy source into /tmp so node_modules + .next don't fight $HOME's 1 GB cap
# and we sidestep npm's quirks with symlinked node_modules.
DBUILD=/tmp/dashboard-build
rm -rf "$DBUILD"
cp -r "$WORKDIR/code/dashboard" "$DBUILD"
cd "$DBUILD"
rm -rf node_modules .next
NEXT_TELEMETRY_DISABLED=1 npm ci 2>&1 | tail -3
# NEXT_PUBLIC_* vars are baked into the client bundle at build time. The WS
# URL must point at Caddy's HTTPS (443), not the connector's raw port 8080
# which is blocked by the security group from the public internet.
NEXT_TELEMETRY_DISABLED=1 \
NEXT_PUBLIC_WS_URL="wss://13-193-136-150.sslip.io/ws" \
NODE_OPTIONS=--max-old-space-size=2048 \
npm run build 2>&1 | tail -8
mkdir -p "$TMPDIR/dashboard"
cp -r .next/standalone/. "$TMPDIR/dashboard/"
mkdir -p "$TMPDIR/dashboard/.next"
cp -r .next/static "$TMPDIR/dashboard/.next/"
[ -d public ] && cp -r public "$TMPDIR/dashboard/" || true
echo " dashboard bundle: $(du -sh "$TMPDIR/dashboard" | cut -f1)"
echo
echo "=== Writing install.sh into bundle ==="
cat > "$TMPDIR/install.sh" <<'INSTALL'
#!/bin/bash
set -euxo pipefail
cd "$(dirname "$0")"
sudo install -m 0755 connector /opt/fintechner/bin/connector
sudo rm -rf /opt/fintechner/dashboard
sudo cp -r dashboard /opt/fintechner/dashboard
sudo chown -R ec2-user:ec2-user /opt/fintechner
sudo install -m 0644 /dev/stdin /etc/fintechner/dashboard.env <<DENV
PORT=3000
HOSTNAME=127.0.0.1
NEXT_PUBLIC_WS_URL=wss://13-193-136-150.sslip.io/ws
DENV
# DO NOT touch /etc/fintechner/connector.env if it already has the required
# bootstrap (DATABASE_URL, MASTER_ENCRYPTION_KEY, ADMIN_EMAIL). Earlier
# revisions of this script clobbered those, killing the connector on every
# deploy. The bootstrap script (14-bootstrap-and-start.sh) is the only thing
# that writes connector.env; deploys only touch the binary.
if [ ! -s /etc/fintechner/connector.env ] || ! grep -q "^DATABASE_URL=" /etc/fintechner/connector.env; then
echo "WARNING: connector.env missing or incomplete. Connector will fail to start."
echo "Run 14-bootstrap-and-start.sh OR 36-restore-and-fix-deploy.sh first."
fi
# Patch the Caddyfile: previous version used handle_path /ws which strips
# the path and breaks the WS upgrade. handle /ws preserves it.
if grep -q "handle_path /ws" /etc/caddy/Caddyfile; then
sudo sed -i 's|handle_path /ws|handle /ws|' /etc/caddy/Caddyfile
sudo systemctl reload caddy
fi
sudo systemctl daemon-reload
sudo systemctl enable --now fintechner-dashboard
sudo systemctl restart fintechner-dashboard
# Connector binary changed too — restart so the new build (with the
# 60s HTTP timeouts, etc.) actually runs.
if sudo systemctl is-enabled fintechner-connector >/dev/null 2>&1; then
sudo systemctl restart fintechner-connector
fi
INSTALL
chmod +x "$TMPDIR/install.sh"
echo "=== Tarballing (connector + dashboard + install.sh) ==="
cd "$TMPDIR"
tar -czf bundle.tar.gz connector dashboard install.sh
echo " bundle.tar.gz: $(du -h bundle.tar.gz | cut -f1)"
echo
echo "=== Uploading via temp S3 bucket ==="
aws --region "$REGION" s3 mb "s3://$S3_BUCKET" >/dev/null
aws --region "$REGION" s3api put-public-access-block --bucket "$S3_BUCKET" \
--public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true" >/dev/null
aws --region "$REGION" s3 cp "$TMPDIR/bundle.tar.gz" "s3://$S3_BUCKET/" >/dev/null
URL=$(aws --region "$REGION" s3 presign "s3://$S3_BUCKET/bundle.tar.gz" --expires-in 1200)
echo " bucket: $S3_BUCKET (auto-deleted on script exit)"
echo
echo "=== Installing on EC2 via SSM ==="
# Each array element is a separate command (no embedded newlines to mangle).
cat > /tmp/ssm-params.json <<JSON
{
"commands": [
"set -e",
"cd /tmp",
"curl -fsSL '$URL' -o bundle.tar.gz",
"rm -rf /tmp/fintechner-stage",
"mkdir /tmp/fintechner-stage",
"tar -xzf bundle.tar.gz -C /tmp/fintechner-stage",
"bash /tmp/fintechner-stage/install.sh"
]
}
JSON
CMD_ID=$(aws --region "$REGION" ssm send-command \
--instance-ids "$INSTANCE_ID" \
--document-name "AWS-RunShellScript" \
--parameters file:///tmp/ssm-params.json \
--query 'Command.CommandId' --output text)
for i in $(seq 1 40); do
sleep 5
STATUS=$(aws --region "$REGION" ssm get-command-invocation \
--command-id "$CMD_ID" --instance-id "$INSTANCE_ID" \
--query 'Status' --output text 2>/dev/null || echo "Pending")
case "$STATUS" in
Success) echo " install: Success"; break ;;
Failed|Cancelled|TimedOut)
echo " install: $STATUS"
aws --region "$REGION" ssm get-command-invocation \
--command-id "$CMD_ID" --instance-id "$INSTANCE_ID" \
--query 'StandardErrorContent' --output text
exit 1 ;;
*) echo " install: $STATUS" ;;
esac
done
echo
echo "============================================================"
echo " DEPLOY COMPLETE"
echo "============================================================"
echo
echo " Dashboard: https://13-193-136-150.sslip.io"
echo " Connector: NOT started — needs HL agent key set in Secrets Manager."
echo
echo "Next: set the admin login password."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment