Deploy #22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy | |
| on: | |
| workflow_run: | |
| workflows: [Release] | |
| types: [completed] | |
| env: | |
| IMAGE_NAME: networkcat/rustyip | |
| permissions: | |
| contents: read | |
| jobs: | |
| deploy: | |
| name: Deploy | |
| runs-on: ubuntu-latest | |
| if: github.event.workflow_run.conclusion == 'success' | |
| environment: production | |
| concurrency: | |
| group: deploy-production | |
| cancel-in-progress: false | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Resolve image tag from release | |
| id: image | |
| run: | | |
| TAG="${{ github.event.workflow_run.head_branch }}" | |
| TAG="${TAG#v}" | |
| echo "full=${{ env.IMAGE_NAME }}:${TAG}" >> "$GITHUB_OUTPUT" | |
| # ---- Terraform: provision infrastructure ---- | |
| - name: Setup Terraform | |
| uses: hashicorp/setup-terraform@v3 | |
| with: | |
| terraform_wrapper: false | |
| - name: Terraform init | |
| working-directory: deploy/terraform | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.TF_STATE_ACCESS_KEY }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.TF_STATE_SECRET_KEY }} | |
| run: | | |
| terraform init \ | |
| -backend-config="bucket=${{ secrets.TF_STATE_BUCKET }}" \ | |
| -backend-config="endpoints={s3=\"${{ secrets.TF_STATE_ENDPOINT }}\"}" | |
| - name: Terraform apply | |
| working-directory: deploy/terraform | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.TF_STATE_ACCESS_KEY }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.TF_STATE_SECRET_KEY }} | |
| run: | | |
| terraform apply -auto-approve \ | |
| -var="vultr_api_key=${{ secrets.VULTR_API_KEY }}" \ | |
| -var="ssh_public_key=${{ secrets.SSH_PUBLIC_KEY }}" | |
| - name: Extract VPS IP (masked) | |
| id: vps | |
| working-directory: deploy/terraform | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.TF_STATE_ACCESS_KEY }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.TF_STATE_SECRET_KEY }} | |
| run: | | |
| VPS_IP=$(terraform output -raw instance_ip) | |
| echo "::add-mask::${VPS_IP}" | |
| echo "ip=${VPS_IP}" >> "$GITHUB_OUTPUT" | |
| # ---- SSH setup ---- | |
| - name: Setup SSH key | |
| run: | | |
| mkdir -p ~/.ssh | |
| echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key | |
| chmod 600 ~/.ssh/deploy_key | |
| for i in 1 2 3; do | |
| keys=$(ssh-keyscan -H "${{ steps.vps.outputs.ip }}" 2>/dev/null) || true | |
| if [ -n "$keys" ]; then | |
| echo "$keys" >> ~/.ssh/known_hosts | |
| echo "ssh-keyscan succeeded on attempt $i" | |
| exit 0 | |
| fi | |
| echo "ssh-keyscan attempt $i failed, retrying in 10s..." | |
| sleep 10 | |
| done | |
| echo "ssh-keyscan failed after 3 attempts" | |
| exit 1 | |
| # ---- Detect first deployment ---- | |
| - name: Detect first deployment | |
| id: detect | |
| run: | | |
| set +e | |
| ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \ | |
| -i ~/.ssh/deploy_key root@"${{ steps.vps.outputs.ip }}" \ | |
| "test -f /opt/rustyip/.active_color" | |
| if [ $? -ne 0 ]; then | |
| echo "first_deploy=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "first_deploy=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| # ---- Ansible: configure VPS and deploy ---- | |
| - name: Install Ansible | |
| run: pip install ansible | |
| - name: Generate Ansible inventory | |
| run: | | |
| cat > deploy/ansible/inventory.yml <<EOF | |
| all: | |
| hosts: | |
| vps: | |
| ansible_host: "${{ steps.vps.outputs.ip }}" | |
| ansible_user: root | |
| ansible_ssh_private_key_file: ~/.ssh/deploy_key | |
| ansible_ssh_common_args: "-o StrictHostKeyChecking=accept-new" | |
| EOF | |
| - name: Run Ansible playbook | |
| working-directory: deploy/ansible | |
| env: | |
| APP_IMAGE: ${{ steps.image.outputs.full }} | |
| SITE_DOMAIN: ${{ secrets.SITE_DOMAIN }} | |
| DB_UPDATE_URL: ${{ secrets.DB_UPDATE_URL }} | |
| ORIGIN_CERT: ${{ secrets.ORIGIN_CERT }} | |
| ORIGIN_KEY: ${{ secrets.ORIGIN_KEY }} | |
| IPV4_DOMAIN: ${{ secrets.IPV4_DOMAIN }} | |
| IPV4_ORIGIN_CERT: ${{ secrets.IPV4_ORIGIN_CERT }} | |
| IPV4_ORIGIN_KEY: ${{ secrets.IPV4_ORIGIN_KEY }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
| FIRST_DEPLOY: ${{ steps.detect.outputs.first_deploy }} | |
| run: | | |
| ansible-playbook -i inventory.yml playbook.yml | |
| # ---- Verify deployment ---- | |
| - name: Verify deployment health | |
| env: | |
| SSH_OPTS: -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -i ~/.ssh/deploy_key | |
| VPS_HOST: root@${{ steps.vps.outputs.ip }} | |
| run: | | |
| DEPLOY_COLOR=$(ssh $SSH_OPTS "$VPS_HOST" "cat /opt/rustyip/.active_color") | |
| echo "Active deployment color: ${DEPLOY_COLOR}" | |
| for i in $(seq 1 30); do | |
| if ssh $SSH_OPTS "$VPS_HOST" \ | |
| "docker exec rustyip-app-${DEPLOY_COLOR} wget -qO /dev/null http://127.0.0.1:3000/health" 2>/dev/null; then | |
| echo "Deployment verified: app-${DEPLOY_COLOR} is healthy" | |
| exit 0 | |
| fi | |
| echo "Waiting for health check... (attempt $i/30)" | |
| sleep 4 | |
| done | |
| echo "Health check failed after 120 seconds" | |
| exit 1 | |
| # ---- Cleanup ---- | |
| - name: Cleanup SSH key | |
| if: always() | |
| run: rm -f ~/.ssh/deploy_key |