From 2eb4275e6f30bf6a1d4f0a210da485ac6638ccc2 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Tue, 23 Jun 2026 22:56:59 +0800 Subject: [PATCH] =?UTF-8?q?chore(deploy):=20CI=20=E5=8F=8C=E8=BD=A8?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=8F=91=E7=89=88=E5=90=8C=E6=97=B6=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=20EC2=20+=20=E9=98=BF=E9=87=8C=EF=BC=88EC2=E2=86=92Al?= =?UTF-8?q?i=20=E8=BF=81=E7=A7=BB=E6=9C=9F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deploy-{server,site,client}.sh 目标主机参数化(DEPLOY_HOST/USER/SSH_KEY), 不传时行为与现状完全一致(只发 EC2)。workflow 的 Deploy 拆成 EC2(前)+ Ali(后,if:always + continue-on-error,影子目标失败不挡线上)。deploy-server 的 ali 分支只换二进制 + reload 独立 nginx(listen 443),不启动 jiu(阿里 DB 为只读从库,切流提升为主后才起)。新增 deploy/nginx-jiu-ali.conf 并打进 configs.tar.gz。 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx --- .gitea/workflows/deploy-client.yml | 15 ++++- .gitea/workflows/deploy-server.yml | 18 ++++- .gitea/workflows/deploy-site.yml | 15 ++++- deploy/nginx-jiu-ali.conf | 103 +++++++++++++++++++++++++++++ scripts/ci/compile-backend.sh | 1 + scripts/ci/deploy-client.sh | 23 ++++--- scripts/ci/deploy-server.sh | 71 ++++++++++++++++---- scripts/ci/deploy-site.sh | 15 +++-- scripts/ci/lib-forgejo.sh | 11 ++- 9 files changed, 238 insertions(+), 34 deletions(-) create mode 100644 deploy/nginx-jiu-ali.conf diff --git a/.gitea/workflows/deploy-client.yml b/.gitea/workflows/deploy-client.yml index 41d467d..84ea62e 100644 --- a/.gitea/workflows/deploy-client.yml +++ b/.gitea/workflows/deploy-client.yml @@ -180,7 +180,7 @@ jobs: GITEA_REPOSITORY: ${{ gitea.repository }} run: sh scripts/ci/release-client.sh "${{ gitea.ref_name }}" - - name: Deploy + - name: Deploy → EC2 env: FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} FORGEJO_URL: ${{ secrets.FORGEJO_URL }} @@ -190,6 +190,19 @@ jobs: EC2_USER: ${{ secrets.EC2_USER }} run: sh scripts/ci/deploy-client.sh "${{ gitea.ref_name }}" + # 迁移期影子目标:同步 Flutter web + version.yaml + 安装包到阿里机。失败不挡 EC2。 + - name: Deploy → Ali (migration shadow) + if: always() + continue-on-error: true + env: + FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} + FORGEJO_URL: ${{ secrets.FORGEJO_URL }} + GITEA_REPOSITORY: ${{ gitea.repository }} + DEPLOY_SSH_KEY: ${{ secrets.ALI_SSH_KEY }} + DEPLOY_HOST: ${{ secrets.ALI_HOST }} + DEPLOY_USER: ${{ secrets.ALI_USER }} + run: sh scripts/ci/deploy-client.sh "${{ gitea.ref_name }}" + - name: Notify if: always() env: diff --git a/.gitea/workflows/deploy-server.yml b/.gitea/workflows/deploy-server.yml index e7d234b..8da1932 100644 --- a/.gitea/workflows/deploy-server.yml +++ b/.gitea/workflows/deploy-server.yml @@ -36,16 +36,32 @@ jobs: GITEA_REPOSITORY: ${{ gitea.repository }} run: sh scripts/ci/release-server.sh "${{ gitea.ref_name }}" - - name: Deploy + - name: Deploy → EC2 env: FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} FORGEJO_URL: ${{ secrets.FORGEJO_URL }} GITEA_REPOSITORY: ${{ gitea.repository }} + DEPLOY_TARGET: ec2 EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }} EC2_HOST: ${{ secrets.EC2_HOST }} EC2_USER: ${{ secrets.EC2_USER }} run: sh scripts/ci/deploy-server.sh "${{ gitea.ref_name }}" + # 迁移期影子目标:部署到阿里机(只换二进制 + reload 独立 nginx,jiu 保持 stopped)。 + # if: always() → EC2 失败也照样尝试;continue-on-error → 阿里失败不让流水线变红。 + - name: Deploy → Ali (migration shadow) + if: always() + continue-on-error: true + env: + FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} + FORGEJO_URL: ${{ secrets.FORGEJO_URL }} + GITEA_REPOSITORY: ${{ gitea.repository }} + DEPLOY_TARGET: ali + DEPLOY_SSH_KEY: ${{ secrets.ALI_SSH_KEY }} + DEPLOY_HOST: ${{ secrets.ALI_HOST }} + DEPLOY_USER: ${{ secrets.ALI_USER }} + run: sh scripts/ci/deploy-server.sh "${{ gitea.ref_name }}" + - name: Notify if: always() env: diff --git a/.gitea/workflows/deploy-site.yml b/.gitea/workflows/deploy-site.yml index 93d75e4..c7ac282 100644 --- a/.gitea/workflows/deploy-site.yml +++ b/.gitea/workflows/deploy-site.yml @@ -32,7 +32,7 @@ jobs: GITEA_REPOSITORY: ${{ gitea.repository }} run: sh scripts/ci/release-site.sh "${{ gitea.ref_name }}" - - name: Deploy + - name: Deploy → EC2 env: FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} FORGEJO_URL: ${{ secrets.FORGEJO_URL }} @@ -42,6 +42,19 @@ jobs: EC2_USER: ${{ secrets.EC2_USER }} run: sh scripts/ci/deploy-site.sh "${{ gitea.ref_name }}" + # 迁移期影子目标:同步官网到阿里机。失败不挡 EC2。 + - name: Deploy → Ali (migration shadow) + if: always() + continue-on-error: true + env: + FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} + FORGEJO_URL: ${{ secrets.FORGEJO_URL }} + GITEA_REPOSITORY: ${{ gitea.repository }} + DEPLOY_SSH_KEY: ${{ secrets.ALI_SSH_KEY }} + DEPLOY_HOST: ${{ secrets.ALI_HOST }} + DEPLOY_USER: ${{ secrets.ALI_USER }} + run: sh scripts/ci/deploy-site.sh "${{ gitea.ref_name }}" + - name: Notify if: always() env: diff --git a/deploy/nginx-jiu-ali.conf b/deploy/nginx-jiu-ali.conf new file mode 100644 index 0000000..73c61ee --- /dev/null +++ b/deploy/nginx-jiu-ali.conf @@ -0,0 +1,103 @@ +# 阿里云独立 nginx 版(宿主机直接终结 TLS,不经 pangolin)。 +# 与 nginx-jiu.conf(EC2 pangolin 容器版,listen 127.0.0.1:8445)等价, +# 仅入口监听改为对公网 443 直连。CI deploy-server.sh 的 ali 分支落到 +# /etc/nginx/conf.d/jiu.conf。 +limit_req_zone $binary_remote_addr zone=jiu_pub:10m rate=10r/s; +limit_req_status 429; + +server { + listen 443 ssl http2; + server_name jiu.51yanmei.com; + + ssl_certificate /etc/letsencrypt/live/jiu.51yanmei.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/jiu.51yanmei.com/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + client_max_body_size 20m; + + # 商品图片静态文件 + location ^~ /images/ { + alias /opt/jiu/images/; + expires 30d; + add_header Cache-Control "public, immutable"; + } + + # 文件导入接口(超时更长) + location ~ ^/api/v1/import/ { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 300s; + } + + # 未鉴权公开/登录接口:加最外层 per-IP 限流 + location ~ ^/api/v1/(public|auth)/ { + limit_req zone=jiu_pub burst=20 nodelay; + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 30s; + } + + # API 反向代理 + location ~ ^/(api|health|version) { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 30s; + } + + # Flutter 管理端(仅 /app/ 前缀) + location /app/ { + alias /opt/jiu/web/; + index index.html; + try_files $uri $uri/ /app/index.html; + } + + location = /app/index.html { + alias /opt/jiu/web/index.html; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + expires 0; + } + + # 公开商品详情页(扫码跳转)→ 后端注入 OG 标签 + location ~ ^/product/ { + limit_req zone=jiu_pub burst=20 nodelay; + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 30s; + } + + # Flutter base-href=/app/ 会把 /product/:id 重写为 /app/product/:id + location ~ ^/app/product/ { + limit_req zone=jiu_pub burst=20 nodelay; + rewrite ^/app(/product/.+)$ $1 break; + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 30s; + } + + # 桌面客户端安装包下载 + location /downloads/ { + alias /opt/jiu/downloads/; + add_header Content-Disposition "attachment"; + add_header Cache-Control "no-cache"; + autoindex off; + } + + # 营销站点兜底 + location / { + root /opt/jiu/marketing; + try_files $uri $uri.html $uri/index.html /index.html; + } +} + +server { + listen 80; + server_name jiu.51yanmei.com; + return 301 https://$host$request_uri; +} diff --git a/scripts/ci/compile-backend.sh b/scripts/ci/compile-backend.sh index 95dcb62..e57b07d 100644 --- a/scripts/ci/compile-backend.sh +++ b/scripts/ci/compile-backend.sh @@ -26,6 +26,7 @@ mv backend/jiu-gencode dist/jiu-gencode # it belongs to the client pipeline. tar -czf dist/configs.tar.gz \ deploy/nginx-jiu.conf \ + deploy/nginx-jiu-ali.conf \ deploy/jiu.service \ deploy/production.env.template \ deploy/setup-ec2.sh \ diff --git a/scripts/ci/deploy-client.sh b/scripts/ci/deploy-client.sh index ae675b3..d022ca6 100644 --- a/scripts/ci/deploy-client.sh +++ b/scripts/ci/deploy-client.sh @@ -1,14 +1,17 @@ #!/usr/bin/env bash # deploy-client.sh — deploy the Flutter web app, desktop/mobile installers, -# and version.yaml to EC2. Does NOT restart the backend (version.yaml is read -# per-request) nor touch nginx / the marketing site. +# and version.yaml. Does NOT restart the backend (version.yaml is read +# per-request) nor touch nginx / the marketing site. Target is EC2 by default; +# set DEPLOY_HOST/DEPLOY_USER/DEPLOY_SSH_KEY to retarget the Ali box (migration). set -euo pipefail # shellcheck source=scripts/ci/lib-forgejo.sh . "$(dirname "$0")/lib-forgejo.sh" TAG="$1" -echo "==> deploy-client: tag=${TAG}" +TARGET_HOST="${DEPLOY_HOST:-$EC2_HOST}" +TARGET_USER="${DEPLOY_USER:-$EC2_USER}" +echo "==> deploy-client: tag=${TAG} host=${TARGET_HOST}" if [ ! -f dist/web.tar.gz ] || [ ! -f dist/version.yaml ]; then download_release_assets "$TAG" @@ -27,18 +30,18 @@ mkdir -p /tmp/jiu-web-new tar -xzf dist/web.tar.gz -C /tmp/jiu-web-new --strip-components=1 setup_ssh -echo "==> deploy-client: uploading files to EC2" -${SCP} "$VERSION_YAML" "${EC2_USER}@${EC2_HOST}:/tmp/version.yaml" +echo "==> deploy-client: uploading files to ${TARGET_HOST}" +${SCP} "$VERSION_YAML" "${TARGET_USER}@${TARGET_HOST}:/tmp/version.yaml" rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" \ - /tmp/jiu-web-new/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-web-new/" + /tmp/jiu-web-new/ "${TARGET_USER}@${TARGET_HOST}:/tmp/jiu-web-new/" # Desktop / mobile installers (served from /downloads/ by nginx). Guarded — # may be absent in a partial manual deploy. -[ -f dist/jiu-windows-x64-setup.exe ] && ${SCP} dist/jiu-windows-x64-setup.exe "${EC2_USER}@${EC2_HOST}:/tmp/jiu-windows-x64-setup.exe" || true -[ -f dist/jiu-macos-x64.zip ] && ${SCP} dist/jiu-macos-x64.zip "${EC2_USER}@${EC2_HOST}:/tmp/jiu-macos-x64.zip" || true -[ -f dist/jiu-android.apk ] && ${SCP} dist/jiu-android.apk "${EC2_USER}@${EC2_HOST}:/tmp/jiu-android.apk" || true +[ -f dist/jiu-windows-x64-setup.exe ] && ${SCP} dist/jiu-windows-x64-setup.exe "${TARGET_USER}@${TARGET_HOST}:/tmp/jiu-windows-x64-setup.exe" || true +[ -f dist/jiu-macos-x64.zip ] && ${SCP} dist/jiu-macos-x64.zip "${TARGET_USER}@${TARGET_HOST}:/tmp/jiu-macos-x64.zip" || true +[ -f dist/jiu-android.apk ] && ${SCP} dist/jiu-android.apk "${TARGET_USER}@${TARGET_HOST}:/tmp/jiu-android.apk" || true -${SSH} "${EC2_USER}@${EC2_HOST}" << 'ENDSSH' +${SSH} "${TARGET_USER}@${TARGET_HOST}" << 'ENDSSH' set -e # Update version config (WorkingDirectory=/opt/jiu reads config/version.yaml). diff --git a/scripts/ci/deploy-server.sh b/scripts/ci/deploy-server.sh index c10d6a7..bf9c9d2 100644 --- a/scripts/ci/deploy-server.sh +++ b/scripts/ci/deploy-server.sh @@ -1,15 +1,26 @@ #!/usr/bin/env bash -# deploy-server.sh — deploy the backend binary + nginx config to EC2. -# Two modes: uses dist/ built in the same job, or downloads the release assets -# by tag (manual rollback). Does NOT touch the web app, marketing site, or -# version.yaml. +# deploy-server.sh — deploy the backend binary + nginx config. +# Two source modes: uses dist/ built in the same job, or downloads the release +# assets by tag (manual rollback). Does NOT touch the web app, marketing site, +# or version.yaml. +# +# Target is EC2 by default. During the EC2→Ali migration, set DEPLOY_TARGET=ali +# plus DEPLOY_HOST/DEPLOY_USER/DEPLOY_SSH_KEY to deploy to the Alibaba Cloud box. +# - ec2 (default): stop jiu → swap binary → start jiu → health check → +# reload nginx inside the pangolin-edge container. +# - ali: swap binary only (jiu.service stays stopped — the Ali DB is a +# read-only replica until cutover; backend is started at cutover) → +# reload the host nginx with the standalone (listen 443) config. set -euo pipefail # shellcheck source=scripts/ci/lib-forgejo.sh . "$(dirname "$0")/lib-forgejo.sh" TAG="$1" -echo "==> deploy-server: tag=${TAG}" +DEPLOY_TARGET="${DEPLOY_TARGET:-ec2}" +TARGET_HOST="${DEPLOY_HOST:-$EC2_HOST}" +TARGET_USER="${DEPLOY_USER:-$EC2_USER}" +echo "==> deploy-server: tag=${TAG} target=${DEPLOY_TARGET} host=${TARGET_HOST}" if [ ! -f dist/jiu-server ] || [ ! -f dist/configs.tar.gz ]; then download_release_assets "$TAG" @@ -22,19 +33,54 @@ rm -rf /tmp/jiu-configs mkdir -p /tmp/jiu-configs tar -xzf dist/configs.tar.gz -C /tmp/jiu-configs +# Pick the nginx config matching the target's reverse-proxy topology. +if [ "$DEPLOY_TARGET" = "ali" ]; then + NGINX_SRC=/tmp/jiu-configs/deploy/nginx-jiu-ali.conf +else + NGINX_SRC=/tmp/jiu-configs/deploy/nginx-jiu.conf +fi + setup_ssh -echo "==> deploy-server: uploading files to EC2" -${SCP} dist/jiu-server "${EC2_USER}@${EC2_HOST}:/tmp/jiu-server" -${SCP} /tmp/jiu-configs/deploy/nginx-jiu.conf "${EC2_USER}@${EC2_HOST}:/tmp/nginx-jiu.conf" +echo "==> deploy-server: uploading files to ${TARGET_HOST}" +${SCP} dist/jiu-server "${TARGET_USER}@${TARGET_HOST}:/tmp/jiu-server" +${SCP} "${NGINX_SRC}" "${TARGET_USER}@${TARGET_HOST}:/tmp/nginx-jiu.conf" # Platform code-minting CLI + wrapper (absent when rolling back to a pre-gencode # release — upload only when present). if [ -f dist/jiu-gencode ]; then - ${SCP} dist/jiu-gencode "${EC2_USER}@${EC2_HOST}:/tmp/jiu-gencode" - ${SCP} /tmp/jiu-configs/deploy/jiu-gencode.sh "${EC2_USER}@${EC2_HOST}:/tmp/jiu-gencode.sh" + ${SCP} dist/jiu-gencode "${TARGET_USER}@${TARGET_HOST}:/tmp/jiu-gencode" + ${SCP} /tmp/jiu-configs/deploy/jiu-gencode.sh "${TARGET_USER}@${TARGET_HOST}:/tmp/jiu-gencode.sh" fi -${SSH} "${EC2_USER}@${EC2_HOST}" << 'ENDSSH' +if [ "$DEPLOY_TARGET" = "ali" ]; then + # --- Alibaba Cloud (shadow target, pre-cutover) --- + # Swap the binary but DO NOT start jiu: the Ali DB is a read-only replica, so + # the backend's startup AutoMigrate/backfill would fail. jiu.service is started + # only at cutover (after the replica is promoted to primary). Reload the host + # nginx with the standalone listen-443 config. + ${SSH} "${TARGET_USER}@${TARGET_HOST}" << 'ENDSSH' +set -e + +cp /tmp/jiu-server /opt/jiu/backend/jiu-server +chmod +x /opt/jiu/backend/jiu-server + +if [ -f /tmp/jiu-gencode ]; then + cp /tmp/jiu-gencode /opt/jiu/backend/jiu-gencode + chmod +x /opt/jiu/backend/jiu-gencode + if [ -f /tmp/jiu-gencode.sh ]; then + cp /tmp/jiu-gencode.sh /opt/jiu/backend/jiu-gencode.sh + chmod +x /opt/jiu/backend/jiu-gencode.sh + fi +fi + +cp /tmp/nginx-jiu.conf /etc/nginx/conf.d/jiu.conf +nginx -t && systemctl reload nginx + +echo "Ali server staged (binary swapped, jiu.service left stopped, nginx reloaded)" +ENDSSH +else + # --- EC2 (live primary) --- + ${SSH} "${TARGET_USER}@${TARGET_HOST}" << 'ENDSSH' set -e # Replace backend binary @@ -72,7 +118,8 @@ docker exec pangolin-edge nginx -t && docker exec pangolin-edge nginx -s reload echo "Server deploy complete!" ENDSSH +fi teardown_ssh rm -rf /tmp/jiu-configs -echo "==> deploy-server: done" +echo "==> deploy-server: done (target=${DEPLOY_TARGET})" diff --git a/scripts/ci/deploy-site.sh b/scripts/ci/deploy-site.sh index 0db12b5..276f530 100644 --- a/scripts/ci/deploy-site.sh +++ b/scripts/ci/deploy-site.sh @@ -1,13 +1,16 @@ #!/usr/bin/env bash -# deploy-site.sh — deploy the Eleventy marketing site to EC2 -# (/opt/jiu/marketing). Does NOT restart the backend or touch nginx. +# deploy-site.sh — deploy the Eleventy marketing site to /opt/jiu/marketing. +# Does NOT restart the backend or touch nginx. Target is EC2 by default; set +# DEPLOY_HOST/DEPLOY_USER/DEPLOY_SSH_KEY to retarget the Ali box (migration). set -euo pipefail # shellcheck source=scripts/ci/lib-forgejo.sh . "$(dirname "$0")/lib-forgejo.sh" TAG="$1" -echo "==> deploy-site: tag=${TAG}" +TARGET_HOST="${DEPLOY_HOST:-$EC2_HOST}" +TARGET_USER="${DEPLOY_USER:-$EC2_USER}" +echo "==> deploy-site: tag=${TAG} host=${TARGET_HOST}" if [ ! -f dist/marketing.tar.gz ]; then download_release_assets "$TAG" @@ -21,11 +24,11 @@ mkdir -p /tmp/jiu-marketing-new tar -xzf dist/marketing.tar.gz -C /tmp/jiu-marketing-new setup_ssh -echo "==> deploy-site: uploading marketing site to EC2" +echo "==> deploy-site: uploading marketing site to ${TARGET_HOST}" rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" \ - /tmp/jiu-marketing-new/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-marketing-new/" + /tmp/jiu-marketing-new/ "${TARGET_USER}@${TARGET_HOST}:/tmp/jiu-marketing-new/" -${SSH} "${EC2_USER}@${EC2_HOST}" << 'ENDSSH' +${SSH} "${TARGET_USER}@${TARGET_HOST}" << 'ENDSSH' set -e mkdir -p /opt/jiu/marketing rsync -a --delete /tmp/jiu-marketing-new/ /opt/jiu/marketing/ diff --git a/scripts/ci/lib-forgejo.sh b/scripts/ci/lib-forgejo.sh index ad9fe6e..ab09a7c 100644 --- a/scripts/ci/lib-forgejo.sh +++ b/scripts/ci/lib-forgejo.sh @@ -106,12 +106,17 @@ for a in rel.get('assets', []): PYEOF } -# setup_ssh — write the EC2 deploy key and export SSH / SCP commands. +# setup_ssh — write the deploy key and export SSH / SCP commands. +# Target is the EC2 host by default; set DEPLOY_HOST / DEPLOY_USER / DEPLOY_SSH_KEY +# to retarget another host (e.g. the Alibaba Cloud box during the EC2→Ali migration). +# Backward-compatible: with no DEPLOY_* set, behaves exactly as before (EC2). setup_ssh() { + local host="${DEPLOY_HOST:-$EC2_HOST}" + local key="${DEPLOY_SSH_KEY:-$EC2_SSH_KEY}" mkdir -p ~/.ssh - printf '%s' "${EC2_SSH_KEY}" > ~/.ssh/ec2_deploy.pem + printf '%s' "${key}" > ~/.ssh/ec2_deploy.pem chmod 600 ~/.ssh/ec2_deploy.pem - ssh-keyscan -H "${EC2_HOST}" >> ~/.ssh/known_hosts 2>/dev/null + ssh-keyscan -H "${host}" >> ~/.ssh/known_hosts 2>/dev/null SSH="ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" SCP="scp -O -i ~/.ssh/ec2_deploy.pem" }