CD - Deploy to On-Premise #18
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: CD - Deploy to On-Premise | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| image_tag: | |
| description: 'Docker image tag to deploy (leave empty to use commit SHA)' | |
| required: false | |
| default: '' | |
| type: string | |
| deploy_targets: | |
| description: 'Services to deploy (all, nest, spring, web, or comma-separated)' | |
| required: false | |
| default: 'all' | |
| type: string | |
| skip_build: | |
| description: 'Skip building Docker images? (use existing images)' | |
| required: false | |
| default: false | |
| type: boolean | |
| operation: | |
| description: 'Deployment operation' | |
| required: false | |
| default: 'app' | |
| type: choice | |
| options: | |
| - app | |
| - monitoring | |
| - all | |
| env: | |
| AWS_REGION: us-east-1 | |
| NODE_VERSION: '24.3.0' | |
| JAVA_VERSION: '21' | |
| jobs: | |
| # ============================================ | |
| # Build and Push Docker Images | |
| # ============================================ | |
| build-images: | |
| name: Build & Push Images | |
| runs-on: ubuntu-latest | |
| if: ${{ github.ref == 'refs/heads/production' && !inputs.skip_build && (inputs.operation == 'app' || inputs.operation == 'all') }} | |
| outputs: | |
| image_tag: ${{ steps.set-tag.outputs.image_tag }} | |
| ecr_registry: ${{ steps.login-ecr.outputs.registry }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set image tag | |
| id: set-tag | |
| run: | | |
| TAG="${{ inputs.image_tag }}" | |
| if [ -z "$TAG" ]; then | |
| # Use GitHub SHA (short) as default | |
| TAG="prod-${GITHUB_SHA::8}" | |
| fi | |
| echo "image_tag=${TAG}" >> $GITHUB_OUTPUT | |
| echo "π·οΈ Image Tag: ${TAG}" | |
| echo "π Full SHA: ${GITHUB_SHA}" | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| aws-region: ${{ env.AWS_REGION }} | |
| - name: Login to Amazon ECR | |
| id: login-ecr | |
| uses: aws-actions/amazon-ecr-login@v2 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Build and push NestJS API image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: apps/api/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| tags: | | |
| ${{ steps.login-ecr.outputs.registry }}/flowly/api:${{ steps.set-tag.outputs.image_tag }} | |
| ${{ steps.login-ecr.outputs.registry }}/flowly/api:prod-latest | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Build and push Spring Boot API image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: apps/api-springboot/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| tags: | | |
| ${{ steps.login-ecr.outputs.registry }}/flowly/api-springboot:${{ steps.set-tag.outputs.image_tag }} | |
| ${{ steps.login-ecr.outputs.registry }}/flowly/api-springboot:prod-latest | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Build and push Web image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: apps/web/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| tags: | | |
| ${{ steps.login-ecr.outputs.registry }}/flowly/web:${{ steps.set-tag.outputs.image_tag }} | |
| ${{ steps.login-ecr.outputs.registry }}/flowly/web:prod-latest | |
| build-args: | | |
| NEXT_PUBLIC_API_URL=${{ secrets.ONPREMISE_API_URL }} | |
| NEXT_PUBLIC_SPRINGBOOT_API_URL=${{ secrets.ONPREMISE_API_URL }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Build and push Log Consumer image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: apps/log-consumer/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| tags: | | |
| ${{ steps.login-ecr.outputs.registry }}/flowly/log-consumer:${{ steps.set-tag.outputs.image_tag }} | |
| ${{ steps.login-ecr.outputs.registry }}/flowly/log-consumer:prod-latest | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Build and push AI API image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: apps/ai-agent | |
| file: apps/ai-agent/ai-api/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| tags: | | |
| ${{ steps.login-ecr.outputs.registry }}/flowly/ai-api:${{ steps.set-tag.outputs.image_tag }} | |
| ${{ steps.login-ecr.outputs.registry }}/flowly/ai-api:prod-latest | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Build and push AI Indexer image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: apps/ai-agent | |
| file: apps/ai-agent/ai-indexer/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| tags: | | |
| ${{ steps.login-ecr.outputs.registry }}/flowly/ai-indexer:${{ steps.set-tag.outputs.image_tag }} | |
| ${{ steps.login-ecr.outputs.registry }}/flowly/ai-indexer:prod-latest | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Build and push AI Worker image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: apps/ai-agent | |
| file: apps/ai-agent/ai-worker/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| tags: | | |
| ${{ steps.login-ecr.outputs.registry }}/flowly/ai-worker:${{ steps.set-tag.outputs.image_tag }} | |
| ${{ steps.login-ecr.outputs.registry }}/flowly/ai-worker:prod-latest | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Image build summary | |
| run: | | |
| echo "## π³ Docker Images Built" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Service | Tag | Registry |" >> $GITHUB_STEP_SUMMARY | |
| echo "|---------|-----|----------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| NestJS API | \`${{ steps.set-tag.outputs.image_tag }}\` | ${{ steps.login-ecr.outputs.registry }}/flowly/api |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Spring Boot API | \`${{ steps.set-tag.outputs.image_tag }}\` | ${{ steps.login-ecr.outputs.registry }}/flowly/api-springboot |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Web | \`${{ steps.set-tag.outputs.image_tag }}\` | ${{ steps.login-ecr.outputs.registry }}/flowly/web |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Log Consumer | \`${{ steps.set-tag.outputs.image_tag }}\` | ${{ steps.login-ecr.outputs.registry }}/flowly/log-consumer |" >> $GITHUB_STEP_SUMMARY | |
| echo "| AI API | \`${{ steps.set-tag.outputs.image_tag }}\` | ${{ steps.login-ecr.outputs.registry }}/flowly/ai-api |" >> $GITHUB_STEP_SUMMARY | |
| echo "| AI Indexer | \`${{ steps.set-tag.outputs.image_tag }}\` | ${{ steps.login-ecr.outputs.registry }}/flowly/ai-indexer |" >> $GITHUB_STEP_SUMMARY | |
| echo "| AI Worker | \`${{ steps.set-tag.outputs.image_tag }}\` | ${{ steps.login-ecr.outputs.registry }}/flowly/ai-worker |" >> $GITHUB_STEP_SUMMARY | |
| # ============================================ | |
| # Deploy to On-Premise | |
| # ============================================ | |
| deploy-on-premise: | |
| name: Deploy to On-Premise | |
| runs-on: ubuntu-latest | |
| needs: [build-images] | |
| if: github.ref == 'refs/heads/production' && always() && (needs.build-images.result == 'success' || inputs.skip_build) | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| aws-region: ${{ env.AWS_REGION }} | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Ansible and dependencies | |
| run: | | |
| pip install ansible boto3 botocore | |
| - name: Install Ansible collections | |
| run: | | |
| ansible-galaxy collection install amazon.aws | |
| - name: Create Ansible vault password file | |
| run: | | |
| echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > ansible/.vault_pass | |
| chmod 600 ansible/.vault_pass | |
| - name: Create secrets.yml | |
| run: | | |
| echo "${{ secrets.ANSIBLE_SECRETS_YML }}" | base64 -d > ansible/on-premise/environments/production/inventory/group_vars/on_premise_prod/secrets.yml | |
| chmod 600 ansible/on-premise/environments/production/inventory/group_vars/on_premise_prod/secrets.yml | |
| - name: Verify SSM connectivity | |
| run: | | |
| echo "π Looking up SSM instance..." | |
| INSTANCE_ID=$(aws ssm describe-instance-information \ | |
| --filters "Key=tag:Name,Values=flowly-onpremise-prod" \ | |
| --query "InstanceInformationList[0].InstanceId" \ | |
| --output text) | |
| if [ -z "$INSTANCE_ID" ] || [ "$INSTANCE_ID" = "None" ]; then | |
| echo "β Could not find on-premise instance with tag Name=flowly-onpremise-prod" | |
| exit 1 | |
| fi | |
| echo "β Found instance: ${INSTANCE_ID}" | |
| echo "SSM_INSTANCE_ID=${INSTANCE_ID}" >> $GITHUB_ENV | |
| - name: Test SSM connectivity | |
| working-directory: ansible/on-premise/environments/production | |
| env: | |
| SSM_INSTANCE_ID: ${{ env.SSM_INSTANCE_ID }} | |
| run: | | |
| echo "π Testing SSM connection..." | |
| ansible on_premise_prod \ | |
| -i inventory/hybrid.yml \ | |
| -m ping \ | |
| --one-line || { | |
| echo "β SSM connection failed" | |
| echo "Instance ID: ${SSM_INSTANCE_ID}" | |
| exit 1 | |
| } | |
| echo "β SSM connection successful" | |
| - name: Deploy Application | |
| if: ${{ inputs.operation == 'app' || inputs.operation == 'all' }} | |
| working-directory: ansible/on-premise/environments/production | |
| env: | |
| SSM_INSTANCE_ID: ${{ env.SSM_INSTANCE_ID }} | |
| AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} | |
| IMAGE_TAG: ${{ needs.build-images.outputs.image_tag || inputs.image_tag }} | |
| DEPLOY_TARGETS: ${{ inputs.deploy_targets }} | |
| run: | | |
| echo "π Deploying application..." | |
| echo "Image Tag: ${IMAGE_TAG}" | |
| echo "Targets: ${DEPLOY_TARGETS}" | |
| ansible-playbook \ | |
| -i inventory/hybrid.yml \ | |
| ../../playbooks/deploy.yml \ | |
| -e "target_env=on_premise_prod" \ | |
| -e "image_tag=${IMAGE_TAG}" \ | |
| -e "deploy_targets=${DEPLOY_TARGETS}" \ | |
| --vault-password-file ../../../.vault_pass \ | |
| --tags "app,deploy" \ | |
| -v | |
| - name: Deploy Monitoring | |
| if: ${{ inputs.operation == 'monitoring' || inputs.operation == 'all' }} | |
| working-directory: ansible/on-premise/environments/production | |
| env: | |
| SSM_INSTANCE_ID: ${{ env.SSM_INSTANCE_ID }} | |
| run: | | |
| echo "π Deploying monitoring stack..." | |
| ansible-playbook \ | |
| -i inventory/hybrid.yml \ | |
| ../../playbooks/deploy.yml \ | |
| -e "target_env=on_premise_prod" \ | |
| -e "deploy_targets=none" \ | |
| --vault-password-file ../../../.vault_pass \ | |
| --tags "monitoring" \ | |
| -v | |
| # Ensure monitoring containers are up and running with latest configs | |
| ansible on_premise_prod \ | |
| -i inventory/hybrid.yml \ | |
| -m shell \ | |
| -a "cd /opt/flowly && docker compose up -d prometheus grafana" \ | |
| -v | |
| - name: Verify deployment | |
| working-directory: ansible/on-premise/environments/production | |
| env: | |
| SSM_INSTANCE_ID: ${{ env.SSM_INSTANCE_ID }} | |
| run: | | |
| echo "π₯ Checking deployment status..." | |
| # Get container status | |
| ansible on_premise_prod \ | |
| -i inventory/hybrid.yml \ | |
| -m shell \ | |
| -a "cd /opt/flowly && docker compose ps" \ | |
| -o | |
| - name: Health check | |
| working-directory: ansible/on-premise/environments/production | |
| env: | |
| SSM_INSTANCE_ID: ${{ env.SSM_INSTANCE_ID }} | |
| run: | | |
| echo "π©Ί Running health checks via Gateway..." | |
| # Check health endpoints via Gateway (Nginx) | |
| ansible on_premise_prod \ | |
| -i inventory/hybrid.yml \ | |
| -m shell \ | |
| -a "curl -f http://localhost/api/health && curl -f http://localhost/actuator/health" \ | |
| --one-line || { | |
| echo "β οΈ Warning: Health checks failed (may still be starting)" | |
| } | |
| - name: Cleanup vault password | |
| if: always() | |
| run: | | |
| rm -f /tmp/.vault_pass | |
| - name: Deployment summary | |
| if: always() | |
| run: | | |
| echo "## π On-Premise Deployment Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Environment:** on-premise (production)" >> $GITHUB_STEP_SUMMARY | |
| echo "**Operation:** \`${{ inputs.operation }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "**Image Tag:** \`${{ needs.build-images.outputs.image_tag || inputs.image_tag }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "**Targets:** \`${{ inputs.deploy_targets }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "**Instance:** \`${{ env.SSM_INSTANCE_ID }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Access" >> $GITHUB_STEP_SUMMARY | |
| echo "- SSH: \`aws ssm start-session --target ${{ env.SSM_INSTANCE_ID }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- Logs: \`./deploy.sh logs <service>\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Health Check URLs (via Gateway/SSH)" >> $GITHUB_STEP_SUMMARY | |
| echo "- NestJS API: \`http://localhost/api/health\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- Spring Boot: \`http://localhost/actuator/health\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- Prometheus: \`http://localhost/prometheus/-/healthy\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- Grafana: \`http://localhost/grafana/api/health\`" >> $GITHUB_STEP_SUMMARY |