Skip to content

Deploy Migration changes#48

Merged
NickSavino merged 11 commits intomainfrom
development
Feb 4, 2026
Merged

Deploy Migration changes#48
NickSavino merged 11 commits intomainfrom
development

Conversation

@NickSavino
Copy link
Copy Markdown
Contributor

@NickSavino NickSavino commented Feb 4, 2026

User description

First deployment with migration changes, potential minor issues expected.


PR Type

Enhancement


Description

  • Refactored CI/CD pipeline to support database migrations

  • Extracted deployment logic into reusable shell scripts

  • Added migration job between build and deploy stages

  • Created helper workflows for AWS resource management

  • Updated Dockerfile with separate app and migrator targets

  • Modified database schema with UUID default for users table


Diagram Walkthrough

flowchart LR
  A["Build Job"] -->|"app & migrator images"| B["Migrate Job"]
  B -->|"run migrations"| C["Deploy Job"]
  C -->|"deploy app"| D["Health Check"]
  E["start-env.yml"] -->|"manual trigger"| F["Start EC2 & RDS"]
  G["stop-env.yml"] -->|"manual trigger"| H["Stop EC2 & RDS"]
Loading

File Walkthrough

Relevant files
Enhancement
10 files
build_and_push.sh
Build and push app and migrator images                                     
+16/-0   
deploy_via_ssm.sh
Deploy application container via SSM                                         
+37/-0   
health_check.sh
Health check verification for deployed app                             
+29/-0   
migrate_via_ssm.sh
Execute database migrations via SSM                                           
+27/-0   
ssm_run_and_wait.sh
Generic SSM command execution with polling                             
+51/-0   
deploy.yml
Refactor workflow with migration stage                                     
+34/-104
start-env.yml
Manual workflow to start AWS resources                                     
+27/-0   
stop-env.yml
Manual workflow to stop AWS resources                                       
+27/-0   
Dockerfile.prod
Add migrator target with goose tool                                           
+15/-2   
00001_init_schema.sql
Add UUID default to users table id                                             
+1/-1     

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 4, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

@NickSavino NickSavino merged commit 31a2dcf into main Feb 4, 2026
1 check passed
@qodo-code-review
Copy link
Copy Markdown

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Supply chain risk

Description: The build installs github.com/pressly/goose/v3/cmd/goose@latest, which is a mutable
dependency reference and creates a supply-chain risk (a future compromised or breaking
latest release would be pulled during CI builds).
Dockerfile.prod [10-10]

Referred Code
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install github.com/pressly/goose/v3/cmd/goose@latest
Secret exposure risk

Description: Secrets fetched from SSM (DBURL, CLERK_SECRET) are injected into docker run environment
variables and can be exposed to other privileged users/processes on the EC2 host (e.g.,
via docker inspect, container metadata, or host-level logging), risking credential
disclosure if the instance is not tightly locked down.
deploy_via_ssm.sh [15-33]

Referred Code
COMMANDS_JSON=$(cat <<JSON
[
    "set -euo pipefail",
    "AWS_REGION=${AWS_REGION}",
    "APP_IMAGE_URI=${APP_IMAGE_URI}",
    "CONTAINER_NAME=${CONTAINER_NAME}",
    "echo APP_IMAGE_URI=$APP_IMAGE_URI",
    "if ! command -v docker >/dev/null 2>&1; then sudo dnf -y install docker; sudo systemctl enable --now docker; fi",
    "sudo usermod -aG docker ec2-user || true",
    "ECR_REGISTRY=\${APP_IMAGE_URI%%/*}",
    "aws ecr get-login-password --region $AWS_REGION | sudo docker login --username AWS --password-stdin \$ECR_REGISTRY",
    "DBURL=\$(aws ssm get-parameter --region $AWS_REGION --with-decryption --name /cgc-2026-prod/api/database_url --query Parameter.Value --output text)",
    "CLERK_SECRET=\$(aws ssm get-parameter --region $AWS_REGION --with-decryption --name /cgc-2026-prod/api/clerk_secret_key --query Parameter.Value --output text)",
    "sudo docker pull $APP_IMAGE_URI",
    "sudo docker rm -f $CONTAINER_NAME || true",
    "sudo docker run -d --restart unless-stopped --name $CONTAINER_NAME -p 8080:8080 -e PORT=8080 -e DATABASE_URL=\"\$DBURL\" -e CLERK_SECRET_KEY=\"\$CLERK_SECRET\" $APP_IMAGE_URI",
    "sleep 2",
    "curl -fsS http://localhost:8080/health"
]
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Unclear failure context: The script exits on a missing public IP using test without printing an actionable error
message, making failures harder to diagnose in CI logs.

Referred Code
PUBLIC_IP=$(aws ec2 describe-instances \
  --region "${AWS_REGION}" \
  --instance-ids "${EC2_INSTANCE_ID}" \
  --query 'Reservations[0].Instances[0].PublicIpAddress' \
  --output text)

  test "${PUBLIC_IP}" != "None"

URL="http://${PUBLIC_IP}:8080/health"

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Leaks internal errors: On SSM failures the workflow prints StandardErrorContent directly to CI output, which can
expose internal command details and potentially sensitive runtime information.

Referred Code
Failed|Cancelled|TimedOut)
  echo "SSM command failed with status: ${STATUS}"
  aws ssm get-command-invocation \
    --region "${AWS_REGION}" \
    --command-id "${COMMAND_ID}" \
    --instance-id "${EC2_INSTANCE_ID}" \
    --query 'StandardErrorContent' \
    --output text
  exit 1

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Sensitive log exposure: Dumping StandardErrorContent into GitHub Actions logs may inadvertently expose secrets or
connection details if remote commands fail and echo environment variables or command
output.

Referred Code
aws ssm get-command-invocation \
  --region "${AWS_REGION}" \
  --command-id "${COMMAND_ID}" \
  --instance-id "${EC2_INSTANCE_ID}" \
  --query 'StandardErrorContent' \
  --output text
exit 1

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit logging: The PR introduces migration and deployment execution paths but does not clearly establish
an auditable record (who/what/when/outcome) for these critical actions within the
application or a centralized audit log, which may exist outside the shown diff.

Referred Code
echo "Deploying image ${APP_IMAGE_URI} to instance"

COMMANDS_JSON=$(cat <<JSON
[
    "set -euo pipefail",
    "AWS_REGION=${AWS_REGION}",
    "APP_IMAGE_URI=${APP_IMAGE_URI}",
    "CONTAINER_NAME=${CONTAINER_NAME}",
    "echo APP_IMAGE_URI=$APP_IMAGE_URI",
    "if ! command -v docker >/dev/null 2>&1; then sudo dnf -y install docker; sudo systemctl enable --now docker; fi",
    "sudo usermod -aG docker ec2-user || true",
    "ECR_REGISTRY=\${APP_IMAGE_URI%%/*}",
    "aws ecr get-login-password --region $AWS_REGION | sudo docker login --username AWS --password-stdin \$ECR_REGISTRY",
    "DBURL=\$(aws ssm get-parameter --region $AWS_REGION --with-decryption --name /cgc-2026-prod/api/database_url --query Parameter.Value --output text)",
    "CLERK_SECRET=\$(aws ssm get-parameter --region $AWS_REGION --with-decryption --name /cgc-2026-prod/api/clerk_secret_key --query Parameter.Value --output text)",
    "sudo docker pull $APP_IMAGE_URI",
    "sudo docker rm -f $CONTAINER_NAME || true",
    "sudo docker run -d --restart unless-stopped --name $CONTAINER_NAME -p 8080:8080 -e PORT=8080 -e DATABASE_URL=\"\$DBURL\" -e CLERK_SECRET_KEY=\"\$CLERK_SECRET\" $APP_IMAGE_URI",
    "sleep 2",
    "curl -fsS http://localhost:8080/health"
]


 ... (clipped 4 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Secret handling in CI: Secrets are fetched via SSM and injected into container environment variables, and while
not explicitly logged in the diff, it is not verifiable here whether downstream tooling or
failure paths could emit these values to logs.

Referred Code
COMMANDS_JSON=$(cat <<JSON
[
    "set -euo pipefail",
    "AWS_REGION=${AWS_REGION}",
    "APP_IMAGE_URI=${APP_IMAGE_URI}",
    "CONTAINER_NAME=${CONTAINER_NAME}",
    "echo APP_IMAGE_URI=$APP_IMAGE_URI",
    "if ! command -v docker >/dev/null 2>&1; then sudo dnf -y install docker; sudo systemctl enable --now docker; fi",
    "sudo usermod -aG docker ec2-user || true",
    "ECR_REGISTRY=\${APP_IMAGE_URI%%/*}",
    "aws ecr get-login-password --region $AWS_REGION | sudo docker login --username AWS --password-stdin \$ECR_REGISTRY",
    "DBURL=\$(aws ssm get-parameter --region $AWS_REGION --with-decryption --name /cgc-2026-prod/api/database_url --query Parameter.Value --output text)",
    "CLERK_SECRET=\$(aws ssm get-parameter --region $AWS_REGION --with-decryption --name /cgc-2026-prod/api/clerk_secret_key --query Parameter.Value --output text)",
    "sudo docker pull $APP_IMAGE_URI",
    "sudo docker rm -f $CONTAINER_NAME || true",
    "sudo docker run -d --restart unless-stopped --name $CONTAINER_NAME -p 8080:8080 -e PORT=8080 -e DATABASE_URL=\"\$DBURL\" -e CLERK_SECRET_KEY=\"\$CLERK_SECRET\" $APP_IMAGE_URI",
    "sleep 2",
    "curl -fsS http://localhost:8080/health"
]

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Add missing environment variables

Add the missing AWS_REGION and ECR_REPO environment variables to the Build &
push images step in deploy.yml. The build_and_push.sh script requires them and
will fail without them.

.github/workflows/deploy.yml [34-37]

 - name: Build & push images
   env:
+    AWS_REGION: ${{ secrets.AWS_REGION }}
+    ECR_REPO: ${{ secrets.ECR_REPO }}
     GITHUB_SHA: ${{ github.sha }}
   run: bash ./scripts/ci/build_and_push.sh
  • Apply / Chat
Suggestion importance[1-10]: 10

__

Why: This suggestion identifies a critical bug where the build_and_push.sh script would fail due to missing mandatory environment variables (AWS_REGION, ECR_REPO), causing the entire workflow to fail.

High
Supply migration step parameters

Add the missing AWS_REGION, ECR_REPO, and EC2_INSTANCE_ID environment variables
to the Run Migrations via SSM step in deploy.yml. The migration script requires
them and will fail without them.

.github/workflows/deploy.yml [54-57]

 - name: Run Migrations via SSM
   env:
+    AWS_REGION: ${{ secrets.AWS_REGION }}
+    ECR_REPO: ${{ secrets.ECR_REPO }}
+    EC2_INSTANCE_ID: ${{ secrets.EC2_INSTANCE_ID }}
     GITHUB_SHA: ${{ github.sha }}
   run: bash ./scripts/ci/migrate_via_ssm.sh
  • Apply / Chat
Suggestion importance[1-10]: 10

__

Why: This suggestion identifies a critical bug where the migrate_via_ssm.sh script would fail due to missing mandatory environment variables (AWS_REGION, ECR_REPO, EC2_INSTANCE_ID), causing the migrate job to fail.

High
Define deploy environment variables

Add the missing AWS_REGION, ECR_REPO, EC2_INSTANCE_ID, and CONTAINER_NAME
environment variables to the Deploy to EC2 via SSM step in deploy.yml. The
deployment script requires them and will fail without them.

.github/workflows/deploy.yml [77-80]

 - name: Deploy to EC2 via SSM
   env:
+    AWS_REGION: ${{ secrets.AWS_REGION }}
+    ECR_REPO: ${{ secrets.ECR_REPO }}
+    EC2_INSTANCE_ID: ${{ secrets.EC2_INSTANCE_ID }}
+    CONTAINER_NAME: ${{ secrets.CONTAINER_NAME }}
     GITHUB_SHA: ${{ github.sha }}
   run: bash ./scripts/ci/deploy_via_ssm.sh
  • Apply / Chat
Suggestion importance[1-10]: 10

__

Why: This suggestion identifies a critical bug where the deploy_via_ssm.sh script would fail due to missing mandatory environment variables (AWS_REGION, ECR_REPO, EC2_INSTANCE_ID, CONTAINER_NAME), causing the deploy job to fail.

High
Add error handling for SSM command

Add an explicit check to ensure the aws ssm send-command returns a valid
COMMAND_ID. If the command fails and the ID is empty, exit with a clear error
message to improve script robustness.

scripts/ci/ssm_run_and_wait.sh [10-16]

 COMMAND_ID=$(aws ssm send-command \
             --region "${AWS_REGION}" \
             --instance-ids "${EC2_INSTANCE_ID}" \
             --document-name "AWS-RunShellScript" \
             --parameters "commands=${COMMANDS_JSON}" \
             --query 'Command.CommandId' \
             --output text)
 
+if [ -z "${COMMAND_ID}" ]; then
+  echo "Failed to send SSM command. Exiting." >&2
+  exit 1
+fi
+
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This suggestion correctly identifies a potential failure mode where COMMAND_ID could be empty, leading to confusing errors later. Adding an explicit check improves the script's robustness and makes debugging easier.

Medium
Prevent workflow failure on resource stop

Make the stop-env.yml workflow idempotent by checking if the RDS instance is in
an 'available' state before attempting to stop it. This prevents the workflow
from failing if the instance is already stopped.

.github/workflows/stop-env.yml [19-27]

 - name: Stop EC2 and RDS
   run: |
     set -euo pipefail
 
     aws ec2 stop-instances --instance-ids "${{ secrets.EC2_INSTANCE_ID }}"
     aws ec2 wait instance-stopped --instance-ids "${{ secrets.EC2_INSTANCE_ID }}"
 
-    aws rds stop-db-instance --db-instance-identifier "${{ secrets.RDS_INSTANCE_ID }}"
-    aws rds wait db-instance-stopped --db-instance-identifier "${{ secrets.RDS_INSTANCE_ID }}"
+    if [[ "$(aws rds describe-db-instances --db-instance-identifier "${{ secrets.RDS_INSTANCE_ID }}" --query 'DBInstances[0].DBInstanceStatus' --output text --no-cli-pager)" == "available" ]]; then
+      echo "RDS instance is available, stopping it..."
+      aws rds stop-db-instance --db-instance-identifier "${{ secrets.RDS_INSTANCE_ID }}"
+      aws rds wait db-instance-stopped --db-instance-identifier "${{ secrets.RDS_INSTANCE_ID }}"
+    else
+      echo "RDS instance is not in 'available' state, skipping stop command."
+    fi
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This suggestion correctly points out that the workflow will fail if the RDS instance is already stopped. Making the step idempotent by checking the instance status first is a significant improvement in robustness.

Medium
High-level
Run migrations from a dedicated task

Instead of running migrations on the application's EC2 instance via SSM, use a
dedicated, ephemeral environment like an AWS Fargate task. This improves
scalability, security, and separation of concerns.

Examples:

.github/workflows/deploy.yml [39-57]
  migrate:
    runs-on: ubuntu-latest
    needs: build
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@v4


 ... (clipped 9 lines)
scripts/ci/migrate_via_ssm.sh [1-27]
#!/usr/bin/env bash
set -euo pipefail
          
: "${AWS_REGION:?}"
: "${ECR_REPO:?}"
: "${GITHUB_SHA:?}"
: "${EC2_INSTANCE_ID:?}"

ECR_REGISTRY="$(aws sts get-caller-identity --query Account --output text).dkr.ecr.${AWS_REGION}.amazonaws.com"
MIGRATOR_IMAGE_URI="${ECR_REGISTRY}/${ECR_REPO}:${GITHUB_SHA}-migrate"

 ... (clipped 17 lines)

Solution Walkthrough:

Before:

# .github/workflows/deploy.yml
migrate:
  name: Run Migrations via SSM on EC2
  steps:
    - run: ./scripts/ci/migrate_via_ssm.sh

# scripts/ci/migrate_via_ssm.sh
COMMANDS_JSON=$(cat <<JSON
[
    "...",
    "DBURL=$(aws ssm get-parameter ...)",
    "sudo docker pull $MIGRATOR_IMAGE_URI",
    "sudo docker run --rm -e GOOSE_DBSTRING=\"\\$DBURL\" $MIGRATOR_IMAGE_URI up"
]
JSON
)
./scripts/ci/ssm_run_and_wait.sh "${COMMANDS_JSON}"

After:

# .github/workflows/deploy.yml
migrate:
  name: Run Migrations via dedicated task
  steps:
    - name: Run ECS Task for migrations
      run: |
        aws ecs run-task \
          --cluster <your-cluster> \
          --task-definition <migrator-task-def> \
          --launch-type FARGATE \
          --network-configuration ...
        # Wait for task to complete
        aws ecs wait tasks-stopped --tasks <task-id>
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a significant architectural weakness by coupling migrations with the application host, and proposes a more scalable and secure industry-standard alternative.

Medium
General
Name migration container for safer execution

Name the migration Docker container and explicitly remove any previous instances
before running. This prevents orphaned containers and makes the migration step
more robust and idempotent.

scripts/ci/migrate_via_ssm.sh [22]

-"sudo docker run --rm -e GOOSE_DRIVER=postgres -e GOOSE_DBSTRING=\"\\$DBURL\" $MIGRATOR_IMAGE_URI -dir ./db/migrations up"
+"sudo docker rm -f cgc-migrator || true",
+"sudo docker run --rm --name cgc-migrator -e GOOSE_DRIVER=postgres -e GOOSE_DBSTRING=\"\\$DBURL\" $MIGRATOR_IMAGE_URI -dir ./db/migrations up"
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: This is a good suggestion that improves the idempotency and reliability of the migration step by naming the container and ensuring old instances are removed, preventing orphaned containers and potential conflicts.

Low
  • More

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants