| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- #!/usr/bin/env bash
- #
- # build-snapshot.sh — build a reusable Amazon Lightsail snapshot of 3x-ui.
- #
- # Flow (mirrors the Packer golden-image model, via the Lightsail API):
- # 1. create an Ubuntu Lightsail instance with snapshot-userdata.sh
- # (installs the panel, NO database, enables the first-boot unit)
- # 2. wait for provisioning, then (optionally) pin a known panel port and run
- # the shared cleanup.sh (wipes any DB/creds/keys/host-keys/cloud-init state)
- # 3. stop the instance and create an instance snapshot
- # 4. delete the build instance (unless --keep-instance)
- #
- # Every instance you later launch from the snapshot generates its OWN unique
- # credentials on first boot (see deploy/firstboot/). The snapshot is private to
- # your AWS account.
- #
- # Requirements: awscli v2, jq, ssh. AWS credentials with Lightsail permissions.
- # Usage:
- # deploy/lightsail/build-snapshot.sh --region eu-central-1 [options]
- # Options:
- # --region <r> AWS region (default: $AWS_REGION or eu-central-1)
- # --blueprint-id <id> Lightsail blueprint (default: ubuntu_24_04)
- # --bundle-id <id> Lightsail bundle/size (default: small_3_0)
- # --availability-zone <z> AZ (default: <region>a)
- # --panel-port <p> Pin the panel port in the snapshot so you can pre-open
- # it in the Lightsail firewall (default: random per instance)
- # --snapshot-name <n> Snapshot name (default: 3x-ui-ubuntu-24.04-<timestamp>)
- # --keep-instance Do not delete the build instance afterwards
- set -euo pipefail
- REGION="${AWS_REGION:-eu-central-1}"
- BLUEPRINT="ubuntu_24_04"
- BUNDLE="small_3_0"
- AZ=""
- PANEL_PORT=""
- SNAPSHOT_NAME=""
- KEEP_INSTANCE=0
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
- STAMP="$(date +%Y%m%d-%H%M%S)"
- INSTANCE_NAME="3xui-build-${STAMP}"
- KEY_FILE=""
- log() { echo "[build-snapshot] $*"; }
- die() {
- echo "[build-snapshot] ERROR: $*" >&2
- exit 1
- }
- while [ $# -gt 0 ]; do
- case "$1" in
- --region) REGION="$2"; shift 2 ;;
- --blueprint-id) BLUEPRINT="$2"; shift 2 ;;
- --bundle-id) BUNDLE="$2"; shift 2 ;;
- --availability-zone) AZ="$2"; shift 2 ;;
- --panel-port) PANEL_PORT="$2"; shift 2 ;;
- --snapshot-name) SNAPSHOT_NAME="$2"; shift 2 ;;
- --keep-instance) KEEP_INSTANCE=1; shift ;;
- -h | --help) sed -n '2,40p' "$0"; exit 0 ;;
- *) die "unknown option: $1" ;;
- esac
- done
- [ -n "$AZ" ] || AZ="${REGION}a"
- [ -n "$SNAPSHOT_NAME" ] || SNAPSHOT_NAME="3x-ui-ubuntu-24.04-${STAMP}"
- for cmd in aws jq ssh; do
- command -v "$cmd" > /dev/null 2>&1 || die "'$cmd' is required"
- done
- SSH_OPTS=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -o LogLevel=ERROR)
- cleanup() {
- [ -n "$KEY_FILE" ] && rm -f "$KEY_FILE"
- if [ "$KEEP_INSTANCE" -eq 0 ]; then
- aws lightsail delete-instance --instance-name "$INSTANCE_NAME" --region "$REGION" > /dev/null 2>&1 || true
- fi
- }
- trap cleanup EXIT
- wait_state() {
- local want="$1" tries="${2:-60}" st
- for _ in $(seq 1 "$tries"); do
- st=$(aws lightsail get-instance-state --instance-name "$INSTANCE_NAME" --region "$REGION" \
- --query 'state.name' --output text 2> /dev/null || echo "")
- [ "$st" = "$want" ] && return 0
- sleep 5
- done
- return 1
- }
- log "creating build instance ${INSTANCE_NAME} (${BLUEPRINT}/${BUNDLE}) in ${REGION}..."
- aws lightsail create-instances \
- --instance-names "$INSTANCE_NAME" \
- --availability-zone "$AZ" \
- --blueprint-id "$BLUEPRINT" \
- --bundle-id "$BUNDLE" \
- --user-data "file://${SCRIPT_DIR}/snapshot-userdata.sh" \
- --region "$REGION" > /dev/null
- log "waiting for instance to run..."
- wait_state running 60 || die "instance did not reach 'running'"
- IP=$(aws lightsail get-instance --instance-name "$INSTANCE_NAME" --region "$REGION" \
- --query 'instance.publicIpAddress' --output text)
- if [ -z "$IP" ] || [ "$IP" = "None" ]; then die "no public IP"; fi
- log "instance IP: ${IP}"
- KEY_FILE="$(mktemp)"
- # download-default-key-pair returns the key in 'privateKeyBase64'. Despite the
- # name, the CLI historically emits the plaintext PEM (-----BEGIN...); the API
- # docs describe it as base64. Handle both: write PEM as-is, else base64-decode.
- KEY_RAW="$(aws lightsail download-default-key-pair --region "$REGION" \
- --query 'privateKeyBase64' --output text)"
- [ -n "$KEY_RAW" ] && [ "$KEY_RAW" != "None" ] || die "failed to download default key pair"
- case "$KEY_RAW" in
- *-----BEGIN*) printf '%s\n' "$KEY_RAW" > "$KEY_FILE" ;;
- *) printf '%s' "$KEY_RAW" | base64 -d > "$KEY_FILE" 2> /dev/null \
- || die "private key is neither PEM nor valid base64" ;;
- esac
- grep -q -- "-----BEGIN" "$KEY_FILE" || die "downloaded key is not a valid PEM private key"
- chmod 600 "$KEY_FILE"
- log "waiting for provisioning to finish (this installs the panel)..."
- ok=0
- for _ in $(seq 1 72); do # ~12 min
- if ssh "${SSH_OPTS[@]}" -i "$KEY_FILE" "ubuntu@${IP}" \
- 'test -f /var/lib/3xui-provision-done' 2> /dev/null; then
- ok=1
- break
- fi
- sleep 10
- done
- [ "$ok" -eq 1 ] || die "provisioning did not complete in time"
- log "provisioning complete."
- if [ -n "$PANEL_PORT" ]; then
- log "pinning panel port ${PANEL_PORT} (username/password stay random)..."
- ssh "${SSH_OPTS[@]}" -i "$KEY_FILE" "ubuntu@${IP}" \
- "echo 'XUI_PANEL_PORT=${PANEL_PORT}' | sudo tee -a /etc/default/x-ui >/dev/null"
- fi
- log "stripping instance state (shared cleanup.sh)..."
- ssh "${SSH_OPTS[@]}" -i "$KEY_FILE" "ubuntu@${IP}" \
- 'curl -fsSL https://raw.githubusercontent.com/MHSanaei/3x-ui/main/deploy/packer/scripts/cleanup.sh | sudo bash'
- log "stopping instance..."
- aws lightsail stop-instance --instance-name "$INSTANCE_NAME" --region "$REGION" > /dev/null
- wait_state stopped 60 || die "instance did not stop"
- log "creating snapshot ${SNAPSHOT_NAME}..."
- aws lightsail create-instance-snapshot \
- --instance-name "$INSTANCE_NAME" \
- --instance-snapshot-name "$SNAPSHOT_NAME" \
- --region "$REGION" > /dev/null
- log "waiting for snapshot to become available..."
- snap_ok=0
- for _ in $(seq 1 120); do # ~20 min
- state=$(aws lightsail get-instance-snapshot --instance-snapshot-name "$SNAPSHOT_NAME" \
- --region "$REGION" --query 'instanceSnapshot.state' --output text 2> /dev/null || echo "")
- [ "$state" = "available" ] && {
- snap_ok=1
- break
- }
- sleep 10
- done
- [ "$snap_ok" -eq 1 ] || die "snapshot did not become available"
- log "DONE."
- echo
- echo "================================================================"
- echo " Lightsail snapshot ready: ${SNAPSHOT_NAME} (region ${REGION})"
- echo "================================================================"
- echo " Launch an instance from it:"
- echo " aws lightsail create-instances-from-snapshot \\"
- echo " --instance-snapshot-name ${SNAPSHOT_NAME} \\"
- echo " --instance-names my-3xui-1 --bundle-id ${BUNDLE} \\"
- echo " --availability-zone ${AZ} --region ${REGION}"
- if [ -n "$PANEL_PORT" ]; then
- echo
- echo " Then open the panel port (pinned to ${PANEL_PORT}):"
- echo " aws lightsail open-instance-public-ports --region ${REGION} \\"
- echo " --instance-name my-3xui-1 \\"
- echo " --port-info fromPort=${PANEL_PORT},toPort=${PANEL_PORT},protocol=TCP"
- else
- echo
- echo " Each instance picks a RANDOM panel port. After it boots, read it from"
- echo " sudo cat /etc/x-ui/credentials.txt"
- echo " and open that TCP port in the instance's Lightsail IPv4 firewall."
- fi
- echo "================================================================"
|