build-snapshot.sh 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. #!/usr/bin/env bash
  2. #
  3. # build-snapshot.sh — build a reusable Amazon Lightsail snapshot of 3x-ui.
  4. #
  5. # Flow (mirrors the Packer golden-image model, via the Lightsail API):
  6. # 1. create an Ubuntu Lightsail instance with snapshot-userdata.sh
  7. # (installs the panel, NO database, enables the first-boot unit)
  8. # 2. wait for provisioning, then (optionally) pin a known panel port and run
  9. # the shared cleanup.sh (wipes any DB/creds/keys/host-keys/cloud-init state)
  10. # 3. stop the instance and create an instance snapshot
  11. # 4. delete the build instance (unless --keep-instance)
  12. #
  13. # Every instance you later launch from the snapshot generates its OWN unique
  14. # credentials on first boot (see deploy/firstboot/). The snapshot is private to
  15. # your AWS account.
  16. #
  17. # Requirements: awscli v2, jq, ssh. AWS credentials with Lightsail permissions.
  18. # Usage:
  19. # deploy/lightsail/build-snapshot.sh --region eu-central-1 [options]
  20. # Options:
  21. # --region <r> AWS region (default: $AWS_REGION or eu-central-1)
  22. # --blueprint-id <id> Lightsail blueprint (default: ubuntu_24_04)
  23. # --bundle-id <id> Lightsail bundle/size (default: small_3_0)
  24. # --availability-zone <z> AZ (default: <region>a)
  25. # --panel-port <p> Pin the panel port in the snapshot so you can pre-open
  26. # it in the Lightsail firewall (default: random per instance)
  27. # --snapshot-name <n> Snapshot name (default: 3x-ui-ubuntu-24.04-<timestamp>)
  28. # --keep-instance Do not delete the build instance afterwards
  29. set -euo pipefail
  30. REGION="${AWS_REGION:-eu-central-1}"
  31. BLUEPRINT="ubuntu_24_04"
  32. BUNDLE="small_3_0"
  33. AZ=""
  34. PANEL_PORT=""
  35. SNAPSHOT_NAME=""
  36. KEEP_INSTANCE=0
  37. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
  38. STAMP="$(date +%Y%m%d-%H%M%S)"
  39. INSTANCE_NAME="3xui-build-${STAMP}"
  40. KEY_FILE=""
  41. log() { echo "[build-snapshot] $*"; }
  42. die() {
  43. echo "[build-snapshot] ERROR: $*" >&2
  44. exit 1
  45. }
  46. while [ $# -gt 0 ]; do
  47. case "$1" in
  48. --region) REGION="$2"; shift 2 ;;
  49. --blueprint-id) BLUEPRINT="$2"; shift 2 ;;
  50. --bundle-id) BUNDLE="$2"; shift 2 ;;
  51. --availability-zone) AZ="$2"; shift 2 ;;
  52. --panel-port) PANEL_PORT="$2"; shift 2 ;;
  53. --snapshot-name) SNAPSHOT_NAME="$2"; shift 2 ;;
  54. --keep-instance) KEEP_INSTANCE=1; shift ;;
  55. -h | --help) sed -n '2,40p' "$0"; exit 0 ;;
  56. *) die "unknown option: $1" ;;
  57. esac
  58. done
  59. [ -n "$AZ" ] || AZ="${REGION}a"
  60. [ -n "$SNAPSHOT_NAME" ] || SNAPSHOT_NAME="3x-ui-ubuntu-24.04-${STAMP}"
  61. for cmd in aws jq ssh; do
  62. command -v "$cmd" > /dev/null 2>&1 || die "'$cmd' is required"
  63. done
  64. SSH_OPTS=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -o LogLevel=ERROR)
  65. cleanup() {
  66. [ -n "$KEY_FILE" ] && rm -f "$KEY_FILE"
  67. if [ "$KEEP_INSTANCE" -eq 0 ]; then
  68. aws lightsail delete-instance --instance-name "$INSTANCE_NAME" --region "$REGION" > /dev/null 2>&1 || true
  69. fi
  70. }
  71. trap cleanup EXIT
  72. wait_state() {
  73. local want="$1" tries="${2:-60}" st
  74. for _ in $(seq 1 "$tries"); do
  75. st=$(aws lightsail get-instance-state --instance-name "$INSTANCE_NAME" --region "$REGION" \
  76. --query 'state.name' --output text 2> /dev/null || echo "")
  77. [ "$st" = "$want" ] && return 0
  78. sleep 5
  79. done
  80. return 1
  81. }
  82. log "creating build instance ${INSTANCE_NAME} (${BLUEPRINT}/${BUNDLE}) in ${REGION}..."
  83. aws lightsail create-instances \
  84. --instance-names "$INSTANCE_NAME" \
  85. --availability-zone "$AZ" \
  86. --blueprint-id "$BLUEPRINT" \
  87. --bundle-id "$BUNDLE" \
  88. --user-data "file://${SCRIPT_DIR}/snapshot-userdata.sh" \
  89. --region "$REGION" > /dev/null
  90. log "waiting for instance to run..."
  91. wait_state running 60 || die "instance did not reach 'running'"
  92. IP=$(aws lightsail get-instance --instance-name "$INSTANCE_NAME" --region "$REGION" \
  93. --query 'instance.publicIpAddress' --output text)
  94. if [ -z "$IP" ] || [ "$IP" = "None" ]; then die "no public IP"; fi
  95. log "instance IP: ${IP}"
  96. KEY_FILE="$(mktemp)"
  97. # download-default-key-pair returns the key in 'privateKeyBase64'. Despite the
  98. # name, the CLI historically emits the plaintext PEM (-----BEGIN...); the API
  99. # docs describe it as base64. Handle both: write PEM as-is, else base64-decode.
  100. KEY_RAW="$(aws lightsail download-default-key-pair --region "$REGION" \
  101. --query 'privateKeyBase64' --output text)"
  102. [ -n "$KEY_RAW" ] && [ "$KEY_RAW" != "None" ] || die "failed to download default key pair"
  103. case "$KEY_RAW" in
  104. *-----BEGIN*) printf '%s\n' "$KEY_RAW" > "$KEY_FILE" ;;
  105. *) printf '%s' "$KEY_RAW" | base64 -d > "$KEY_FILE" 2> /dev/null \
  106. || die "private key is neither PEM nor valid base64" ;;
  107. esac
  108. grep -q -- "-----BEGIN" "$KEY_FILE" || die "downloaded key is not a valid PEM private key"
  109. chmod 600 "$KEY_FILE"
  110. log "waiting for provisioning to finish (this installs the panel)..."
  111. ok=0
  112. for _ in $(seq 1 72); do # ~12 min
  113. if ssh "${SSH_OPTS[@]}" -i "$KEY_FILE" "ubuntu@${IP}" \
  114. 'test -f /var/lib/3xui-provision-done' 2> /dev/null; then
  115. ok=1
  116. break
  117. fi
  118. sleep 10
  119. done
  120. [ "$ok" -eq 1 ] || die "provisioning did not complete in time"
  121. log "provisioning complete."
  122. if [ -n "$PANEL_PORT" ]; then
  123. log "pinning panel port ${PANEL_PORT} (username/password stay random)..."
  124. ssh "${SSH_OPTS[@]}" -i "$KEY_FILE" "ubuntu@${IP}" \
  125. "echo 'XUI_PANEL_PORT=${PANEL_PORT}' | sudo tee -a /etc/default/x-ui >/dev/null"
  126. fi
  127. log "stripping instance state (shared cleanup.sh)..."
  128. ssh "${SSH_OPTS[@]}" -i "$KEY_FILE" "ubuntu@${IP}" \
  129. 'curl -fsSL https://raw.githubusercontent.com/MHSanaei/3x-ui/main/deploy/packer/scripts/cleanup.sh | sudo bash'
  130. log "stopping instance..."
  131. aws lightsail stop-instance --instance-name "$INSTANCE_NAME" --region "$REGION" > /dev/null
  132. wait_state stopped 60 || die "instance did not stop"
  133. log "creating snapshot ${SNAPSHOT_NAME}..."
  134. aws lightsail create-instance-snapshot \
  135. --instance-name "$INSTANCE_NAME" \
  136. --instance-snapshot-name "$SNAPSHOT_NAME" \
  137. --region "$REGION" > /dev/null
  138. log "waiting for snapshot to become available..."
  139. snap_ok=0
  140. for _ in $(seq 1 120); do # ~20 min
  141. state=$(aws lightsail get-instance-snapshot --instance-snapshot-name "$SNAPSHOT_NAME" \
  142. --region "$REGION" --query 'instanceSnapshot.state' --output text 2> /dev/null || echo "")
  143. [ "$state" = "available" ] && {
  144. snap_ok=1
  145. break
  146. }
  147. sleep 10
  148. done
  149. [ "$snap_ok" -eq 1 ] || die "snapshot did not become available"
  150. log "DONE."
  151. echo
  152. echo "================================================================"
  153. echo " Lightsail snapshot ready: ${SNAPSHOT_NAME} (region ${REGION})"
  154. echo "================================================================"
  155. echo " Launch an instance from it:"
  156. echo " aws lightsail create-instances-from-snapshot \\"
  157. echo " --instance-snapshot-name ${SNAPSHOT_NAME} \\"
  158. echo " --instance-names my-3xui-1 --bundle-id ${BUNDLE} \\"
  159. echo " --availability-zone ${AZ} --region ${REGION}"
  160. if [ -n "$PANEL_PORT" ]; then
  161. echo
  162. echo " Then open the panel port (pinned to ${PANEL_PORT}):"
  163. echo " aws lightsail open-instance-public-ports --region ${REGION} \\"
  164. echo " --instance-name my-3xui-1 \\"
  165. echo " --port-info fromPort=${PANEL_PORT},toPort=${PANEL_PORT},protocol=TCP"
  166. else
  167. echo
  168. echo " Each instance picks a RANDOM panel port. After it boots, read it from"
  169. echo " sudo cat /etc/x-ui/credentials.txt"
  170. echo " and open that TCP port in the instance's Lightsail IPv4 firewall."
  171. fi
  172. echo "================================================================"