diff --git a/.github/workflows/atd_moped_database.yml b/.github/workflows/atd_moped_database.yml deleted file mode 100644 index 8cacfa06ec..0000000000 --- a/.github/workflows/atd_moped_database.yml +++ /dev/null @@ -1,50 +0,0 @@ -# -# Applies database migrations to staging (main) and production -# -name: "Applies the migrations to the database" - -on: - push: - branches: - - main - - production - paths: - - "moped-database/**" - - ".github/workflows/atd_moped_database.yml" - - ".github/workflows/aws-moped-migrations-helper.sh" - -jobs: - apply: - name: Apply Migrations - runs-on: ubuntu-24.04 - steps: - - uses: actions/setup-python@v4 - with: - python-version: "3.8" - architecture: "x64" - # Get the code first - - name: "Checkout" - uses: actions/checkout@v3 - # Then install the AWC CLI tools & boto3 - - name: "Install AWS Cli & Hasura CLI" - run: | - sudo apt-get install python3-setuptools - pip3 install awscli virtualenv - curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash - - # Run the shell commands using the AWS environment variables - - name: "Apply" - env: - AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_MOPED_HASURA_CONFIGURAITON_FILE_PRODUCTION: ${{ secrets.AWS_MOPED_HASURA_CONFIGURAITON_FILE_PRODUCTION }} - AWS_MOPED_HASURA_CONFIGURAITON_FILE_STAGING: ${{ secrets.AWS_MOPED_HASURA_CONFIGURAITON_FILE_STAGING }} - run: | - export BRANCH_NAME=${GITHUB_REF##*/} - echo "SHA: ${GITHUB_SHA}" - echo "ACTION/BRANCH_NAME: ${BRANCH_NAME}" - echo "GR: ${GITHUB_REF}" - echo "PWD: $(pwd)" - source $(pwd)/.github/workflows/aws-moped-migrations-helper.sh - run_migration_process diff --git a/.github/workflows/aws-moped-migrations-helper.sh b/.github/workflows/aws-moped-migrations-helper.sh deleted file mode 100644 index 442ebbeb3a..0000000000 --- a/.github/workflows/aws-moped-migrations-helper.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -case "${BRANCH_NAME}" in -"production") - export WORKING_STAGE="production" - ;; -*) - export WORKING_STAGE="staging" - ;; -esac - -echo "SOURCE -> BRANCH_NAME: ${BRANCH_NAME}" -echo "SOURCE -> WORKING_STAGE: ${WORKING_STAGE}" - -# -# Download the Zappa settings from the AWS Secrets Manager -# -function download_hasura_settings() { - echo "Downloading Hasura Settings: ${WORKING_STAGE}"; - - if [[ "${WORKING_STAGE}" == "production" ]]; then - export AWS_HASURA_CONFIGURATION="${AWS_MOPED_HASURA_CONFIGURAITON_FILE_PRODUCTION}"; - else - export AWS_HASURA_CONFIGURATION="${AWS_MOPED_HASURA_CONFIGURAITON_FILE_STAGING}"; - fi; - - aws secretsmanager get-secret-value \ - --secret-id "${AWS_HASURA_CONFIGURATION}" | \ - jq -rc ".SecretString" > config.yaml; -} - - -# -# Waits until the local hasura server is ready -# -function run_migration() { - echo "----- MIGRATIONS STARTED -----"; - hasura --skip-update-check version; - echo "Applying migration"; - hasura migrate apply \ - --skip-update-check \ - --disable-interactive \ - --database-name default; - echo "Applying metadata"; - hasura metadata apply \ - --skip-update-check; - echo "----- MIGRATIONS FINISHED -----"; -} - -# -# Controls the migration process -# -function run_migration_process() { - cd ./moped-database; - echo "Running migration process @ ${PWD}" - download_hasura_settings; - run_migration; -} diff --git a/.github/workflows/migrations-metadata-deployment-helper.sh b/.github/workflows/migrations-metadata-deployment-helper.sh new file mode 100644 index 0000000000..dea1855f56 --- /dev/null +++ b/.github/workflows/migrations-metadata-deployment-helper.sh @@ -0,0 +1,261 @@ +#!/usr/bin/env bash + +case "${BRANCH_NAME}" in +"production") + export WORKING_STAGE="production" + ;; +*) + export WORKING_STAGE="staging" + ;; +esac + +echo "SOURCE -> BRANCH_NAME: ${BRANCH_NAME}" +echo "SOURCE -> WORKING_STAGE: ${WORKING_STAGE}" + +################################################################################ +# JOB: apply-migrations-and-metadata +# Functions for downloading Hasura settings and applying migrations/metadata +################################################################################ + +# +# Download the Hasura settings from the AWS Secrets Manager +# +function download_hasura_settings() { + echo "Downloading Hasura Settings: ${WORKING_STAGE}"; + + if [[ "${WORKING_STAGE}" == "production" ]]; then + export AWS_HASURA_CONFIGURATION="${AWS_MOPED_HASURA_CONFIGURATION_FILE_PRODUCTION}"; + else + export AWS_HASURA_CONFIGURATION="${AWS_MOPED_HASURA_CONFIGURATION_FILE_STAGING}"; + fi; + + aws secretsmanager get-secret-value \ + --secret-id "${AWS_HASURA_CONFIGURATION}" | \ + jq -rc ".SecretString" > config.yaml; +} + +# +# Applies migrations and metadata to the Hasura instance +# +function run_migration() { + echo "----- MIGRATIONS STARTED -----"; + hasura --skip-update-check version; + echo "Applying migration"; + hasura migrate apply \ + --skip-update-check \ + --disable-interactive \ + --database-name default; + echo "Applying metadata"; + hasura metadata apply \ + --skip-update-check; + echo "----- MIGRATIONS FINISHED -----"; +} + +# +# Controls the migration process (main entry point for migrations job) +# +function run_migration_process() { + cd ./moped-database; + echo "Running migration process @ ${PWD}" + download_hasura_settings; + run_migration; +} + +################################################################################ +# JOB: update-ecs-task-deployment +# Functions for validating and deploying ECS task definitions +################################################################################ + +# +# Determines which ECS task definition file to use based on the branch +# Sets TD_FILE and ENVIRONMENT variables +# +function determine_task_definition_file() { + echo "Branch name: ${BRANCH_NAME}"; + + # Determine environment based on branch + if [ "${BRANCH_NAME}" = "production" ]; then + export ENVIRONMENT="production" + export TD_FILE="moped-database/ecs_task_definitions/production.graphql-engine.ecs-td.json" + export FAMILY="atd-moped-production" + export CLUSTER="atd-moped-cluster-production" + else + export ENVIRONMENT="staging" + export TD_FILE="moped-database/ecs_task_definitions/staging.graphql-engine.ecs-td.json" + export FAMILY="atd-moped-staging" + export CLUSTER="atd-moped-cluster-staging" + fi + + export SERVICE="graphql-engine" + + echo "Environment: ${ENVIRONMENT}"; + echo "Cluster: ${CLUSTER}"; + echo "Service: ${SERVICE}"; + echo "Family: ${FAMILY}"; + echo "Task definition file: ${TD_FILE}"; +} + +# +# Compares the local task definition file with the one currently in AWS +# Returns 0 if different (needs update), 1 if identical (no update needed) +# +function check_task_definition_differs() { + echo "Fetching current task definition from AWS for family: ${FAMILY}..."; + + # Describe the current task definition from AWS + # If this is the first task definition, the command will fail and we'll register it + if ! aws ecs describe-task-definition \ + --task-definition ${FAMILY} \ + --output json > /tmp/aws-task-def.json 2>/dev/null; then + echo "No existing task definition found in AWS, will register new one"; + return 0; + fi + + echo "Extracting task definition from AWS response..."; + + # Extract just the taskDefinition object and remove AWS-managed fields + # These fields are added by AWS and shouldn't be compared + jq --sort-keys '.taskDefinition | del(.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy, .deregisteredAt, .tags)' \ + /tmp/aws-task-def.json > /tmp/aws-task-def-normalized.json + + # Normalize the local file the same way (remove the same fields if present) + jq --sort-keys 'del(.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy, .deregisteredAt, .tags)' \ + ${TD_FILE} > /tmp/local-task-def-normalized.json + + echo "Comparing local task definition with AWS version..."; + + # Compare the normalized JSON files + if diff -q /tmp/aws-task-def-normalized.json /tmp/local-task-def-normalized.json > /dev/null; then + echo "🛑 Task definitions are identical, no update needed"; + return 1; + else + echo "✓ Task definitions differ, update needed"; + echo ""; + echo "========================================"; + echo "Differences (AWS version vs Local file):"; + echo "========================================"; + diff -u /tmp/aws-task-def-normalized.json /tmp/local-task-def-normalized.json || true + echo "========================================"; + echo ""; + return 0; + fi +} + +# +# Deregisters old task definitions, keeping only the last 3 active ones +# +function cleanup_old_task_definitions() { + echo "Cleaning up old task definitions for family: ${FAMILY}..."; + + # List all ACTIVE task definition revisions for this family, sorted by revision number + local active_revisions=$(aws ecs list-task-definitions \ + --family-prefix ${FAMILY} \ + --status ACTIVE \ + --sort DESC \ + --output json | jq -r '.taskDefinitionArns[]') + + if [ -z "$active_revisions" ]; then + echo "No active task definitions found to clean up"; + return 0; + fi + + # Count total active revisions + local total_count=$(echo "$active_revisions" | wc -l | tr -d ' ') + echo "Found ${total_count} active task definition(s)"; + + # If we have 3 or fewer, no cleanup needed + if [ "$total_count" -le 3 ]; then + echo "Only ${total_count} active revision(s), no cleanup needed"; + return 0; + fi + + # Skip the first 3 (most recent) and deregister the rest + local to_deregister=$(echo "$active_revisions" | tail -n +4) + local deregister_count=$(echo "$to_deregister" | wc -l | tr -d ' ') + + echo "Deregistering ${deregister_count} old revision(s), keeping the 3 most recent..."; + + while IFS= read -r task_def_arn; do + if [ -n "$task_def_arn" ]; then + echo "Deregistering: ${task_def_arn}"; + if aws ecs deregister-task-definition --task-definition "${task_def_arn}" > /dev/null; then + echo " ✓ Deregistered successfully"; + else + echo " ✗ Failed to deregister"; + fi + fi + done <<< "$to_deregister" + + echo "Cleanup complete!"; +} + +# +# Registers the ECS task definition using AWS CLI +# Returns 0 if successful, exits with error if registration fails or file not found +# Returns 1 if no registration was needed (file unchanged) +# +function register_task_definition() { + # Check if task definition file exists + if [ ! -f "${TD_FILE}" ]; then + echo "Task definition file not found: ${TD_FILE}"; + echo "Skipping ECS task definition update"; + return 1; + fi + + # Check if the task definition differs from what's in AWS + if ! check_task_definition_differs; then + echo "Skipping ECS task definition registration"; + return 1; + fi + + echo "Registering updated task definition..."; + + # Register the task definition using AWS CLI + if aws ecs register-task-definition \ + --family ${FAMILY} \ + --cli-input-json file://${TD_FILE}; then + echo "✓ Task definition registered successfully!"; + return 0; + else + echo "✗ Task definition registration failed!"; + exit 1; + fi +} + +# +# Updates the ECS service to use the latest task definition +# +function update_ecs_service() { + echo "Updating ECS service to use the new task definition..."; + echo "Cluster: ${CLUSTER}"; + echo "Service: ${SERVICE}"; + echo "Family: ${FAMILY}"; + + # Update the service to use the latest task definition from the family + if aws ecs update-service \ + --cluster ${CLUSTER} \ + --service ${SERVICE} \ + --task-definition ${FAMILY} \ + --force-new-deployment; then + echo "✓ ECS service updated successfully!"; + echo "The service will now use the new task definition"; + return 0; + else + echo "✗ Failed to update ECS service"; + exit 1; + fi +} + +# +# Main entry point for ECS task definition update process +# Determines the correct file, registers it if needed, and cleans up old revisions +# +function update_ecs_task_definition_process() { + determine_task_definition_file; + + # Only run cleanup and service update if we successfully registered a new task definition + if register_task_definition; then + cleanup_old_task_definitions; + update_ecs_service; + fi +} diff --git a/.github/workflows/migrations-metadata-deployment.yml b/.github/workflows/migrations-metadata-deployment.yml new file mode 100644 index 0000000000..a3c77d2ba8 --- /dev/null +++ b/.github/workflows/migrations-metadata-deployment.yml @@ -0,0 +1,80 @@ +name: "Applies the migrations to the database" + +on: + push: + branches: + - main + - production + - frank/version-control-ecs-task-definitions + paths: + - "moped-database/**" + - ".github/workflows/migrations-metadata-deployment.yml" + - ".github/workflows/migrations-metadata-deployment-helper.sh" + +jobs: + apply-migrations-and-metadata: + name: Apply Migrations and Metadata + runs-on: ubuntu-24.04 + steps: + - uses: actions/setup-python@v4 + name: "Setup Python" + with: + python-version: "3.8" + architecture: "x64" + # Get the code first + - name: "Checkout Code" + uses: actions/checkout@v3 + # Then install the AWC CLI tools & boto3 + - name: "Install AWS Cli & Hasura CLI" + run: | + sudo apt-get install python3-setuptools + # reminder; this is V1 of the AWS CLI + pip3 install awscli virtualenv + curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash + + # Run the shell commands using the AWS environment variables + - name: "Apply Migrations and Metadata" + env: + AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_MOPED_HASURA_CONFIGURATION_FILE_PRODUCTION: ${{ secrets.AWS_MOPED_HASURA_CONFIGURAITON_FILE_PRODUCTION }} + AWS_MOPED_HASURA_CONFIGURATION_FILE_STAGING: ${{ secrets.AWS_MOPED_HASURA_CONFIGURAITON_FILE_STAGING }} + run: | + export BRANCH_NAME=${GITHUB_REF##*/} + echo "SHA: ${GITHUB_SHA}" + echo "ACTION/BRANCH_NAME: ${BRANCH_NAME}" + echo "GR: ${GITHUB_REF}" + echo "PWD: $(pwd)" + source $(pwd)/.github/workflows/migrations-metadata-deployment-helper.sh + # run_migration_process + + update-ecs-task-deployment: + name: Check for updated ECS task definition and deploy if needed + runs-on: ubuntu-24.04 + needs: apply-migrations-and-metadata + if: always() + steps: + - name: "Checkout Code" + uses: actions/checkout@v3 + + - name: "Install AWS CLI v2" + run: | + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip awscliv2.zip + sudo ./aws/install --update + aws --version + + - name: "Install updated ECS task definition" + env: + AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + run: | + export BRANCH_NAME=${GITHUB_REF##*/} + echo "SHA: ${GITHUB_SHA}" + echo "ACTION/BRANCH_NAME: ${BRANCH_NAME}" + echo "GR: ${GITHUB_REF}" + echo "PWD: $(pwd)" + source $(pwd)/.github/workflows/migrations-metadata-deployment-helper.sh + update_ecs_task_definition_process diff --git a/moped-database/ecs_task_definitions/production.graphql-engine.ecs-td.json b/moped-database/ecs_task_definitions/production.graphql-engine.ecs-td.json new file mode 100644 index 0000000000..ae1f256f2c --- /dev/null +++ b/moped-database/ecs_task_definitions/production.graphql-engine.ecs-td.json @@ -0,0 +1,112 @@ +{ + "family": "atd-moped-production", + "containerDefinitions": [ + { + "cpu": 4096, + "environment": [ + { + "name": "HASURA_ENDPOINT", + "value": "https://moped-graphql.austinmobility.io/v1/graphql" + }, + { + "name": "HASURA_GRAPHQL_ENABLE_TELEMETRY", + "value": "false" + }, + { + "name": "HASURA_GRAPHQL_ENABLE_CONSOLE", + "value": "false" + } + ], + "essential": true, + "healthCheck": { + "command": [ + "CMD-SHELL", + "curl -fsS http://localhost:8080/healthz?strict=true || exit 1" + ], + "interval": 15, + "retries": 3, + "startPeriod": 15, + "timeout": 5 + }, + "image": "hasura/graphql-engine:v2.48.6", + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/aws/ecs/moped/graphql-api/production", + "awslogs-region": "us-east-1", + "awslogs-stream-prefix": "ecs" + } + }, + "memory": 8192, + "mountPoints": [ ], + "name": "atd-moped", + "portMappings": [ + { + "containerPort": 8080, + "hostPort": 8080, + "name": "atd-moped-8080-tcp", + "protocol": "tcp" + } + ], + "secrets": [ + { + "name": "ACTIVITY_LOG_API_SECRET", + "valueFrom": "arn:aws:ssm:us-east-1:295525487728:parameter/MOPED_PRODUCTION_FARGATE_HASURA_GRAPHQL_ADMIN_SECRET" + }, + { + "name": "HASURA_GRAPHQL_ADMIN_SECRET", + "valueFrom": "arn:aws:ssm:us-east-1:295525487728:parameter/MOPED_PRODUCTION_FARGATE_HASURA_GRAPHQL_ADMIN_SECRET" + }, + { + "name": "HASURA_GRAPHQL_DATABASE_URL", + "valueFrom": "arn:aws:ssm:us-east-1:295525487728:parameter/MOPED_PRODUCTION_FARGATE_HASURA_GRAPHQL_DATABASE_URL" + }, + { + "name": "HASURA_GRAPHQL_JWT_SECRET", + "valueFrom": "arn:aws:ssm:us-east-1:295525487728:parameter/MOPED_PRODUCTION_FARGATE_HASURA_GRAPHQL_JWT_SECRET" + }, + { + "name": "MOPED_API_ACTIONS_URL", + "valueFrom": "arn:aws:ssm:us-east-1:295525487728:parameter/MOPED_PRODUCTION_FARGATE_HASURA_ACTIONS_URL" + }, + { + "name": "MOPED_API_APIKEY", + "valueFrom": "arn:aws:ssm:us-east-1:295525487728:parameter/MOPED_PRODUCTION_FARGATE_HASURA_API_KEY" + } + ], + "systemControls": [ ], + "volumesFrom": [ ] + } + ], + "executionRoleArn": "arn:aws:iam::295525487728:role/atd-moped-ecs-execution-role-production", + "networkMode": "awsvpc", + "volumes": [ ], + "placementConstraints": [ ], + "requiresCompatibilities": [ + "FARGATE" + ], + "cpu": "4096", + "memory": "8192", + "runtimePlatform": { + "cpuArchitecture": "X86_64", + "operatingSystemFamily": "LINUX" + }, + "tags": [ + { + "key": "project", + "value": "atd-moped" + }, + { + "key": "environment", + "value": "production" + }, + { + "key": "organization", + "value": "atd" + }, + { + "key": "awsApplication", + "value": "arn:aws:resource-groups:us-east-1:295525487728:group/Moped/0d1wuyh5kinejubg588qch09jg" + } + ] +} diff --git a/moped-database/ecs_task_definitions/staging.graphql-engine.ecs-td.json b/moped-database/ecs_task_definitions/staging.graphql-engine.ecs-td.json new file mode 100644 index 0000000000..13f6433e54 --- /dev/null +++ b/moped-database/ecs_task_definitions/staging.graphql-engine.ecs-td.json @@ -0,0 +1,117 @@ +{ + "family": "atd-moped-staging", + "containerDefinitions": [ + { + "cpu": 256, + "environment": [ + { + "name": "HASURA_ENDPOINT", + "value": "https://moped-graphql-staging.austinmobility.io/v1/graphql" + }, + { + "name": "HASURA_GRAPHQL_ENABLE_TELEMETRY", + "value": "false" + }, + { + "name": "HASURA_GRAPHQL_ENABLE_CONSOLE", + "value": "false" + } + ], + "essential": true, + "healthCheck": { + "command": [ + "CMD-SHELL", + "curl -fsS http://localhost:8080/healthz?strict=true || exit 1" + ], + "interval": 15, + "retries": 3, + "startPeriod": 15, + "timeout": 5 + }, + "image": "hasura/graphql-engine:v2.48.6", + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/aws/ecs/moped/graphql-api/staging", + "awslogs-region": "us-east-1", + "awslogs-stream-prefix": "ecs" + } + }, + "memory": 2048, + "memoryReservation": 2048, + "mountPoints": [ ], + "name": "atd-moped", + "portMappings": [ + { + "containerPort": 8080, + "hostPort": 8080, + "name": "atd-moped-8080-tcp", + "protocol": "tcp" + } + ], + "secrets": [ + { + "name": "ACTIVITY_LOG_API_SECRET", + "valueFrom": "arn:aws:ssm:us-east-1:295525487728:parameter/MOPED_STAGING_FARGATE_HASURA_GRAPHQL_ADMIN_SECRET" + }, + { + "name": "HASURA_GRAPHQL_ADMIN_SECRET", + "valueFrom": "arn:aws:ssm:us-east-1:295525487728:parameter/MOPED_STAGING_FARGATE_HASURA_GRAPHQL_ADMIN_SECRET" + }, + { + "name": "HASURA_GRAPHQL_DATABASE_URL", + "valueFrom": "arn:aws:ssm:us-east-1:295525487728:parameter/MOPED_STAGING_FARGATE_HASURA_GRAPHQL_DATABASE_URL" + }, + { + "name": "HASURA_GRAPHQL_JWT_SECRET", + "valueFrom": "arn:aws:ssm:us-east-1:295525487728:parameter/MOPED_STAGING_FARGATE_HASURA_GRAPHQL_JWT_SECRET" + }, + { + "name": "MOPED_API_ACTIONS_URL", + "valueFrom": "arn:aws:ssm:us-east-1:295525487728:parameter/MOPED_STAGING_FARGATE_HASURA_ACTIONS_URL" + }, + { + "name": "MOPED_API_APIKEY", + "valueFrom": "arn:aws:ssm:us-east-1:295525487728:parameter/MOPED_STAGING_FARGATE_HASURA_API_KEY" + } + ], + "systemControls": [ ], + "volumesFrom": [ ] + } + ], + "executionRoleArn": "arn:aws:iam::295525487728:role/atd-moped-ecs-execution-role-staging", + "networkMode": "awsvpc", + "volumes": [ ], + "placementConstraints": [ ], + "requiresCompatibilities": [ + "FARGATE" + ], + "cpu": "256", + "memory": "2048", + "runtimePlatform": { + "cpuArchitecture": "X86_64", + "operatingSystemFamily": "LINUX" + }, + "tags": [ + { + "key": "project", + "value": "atd-moped" + }, + { + "key": "environment", + "value": "staging" + }, + { + "key": "ecs:taskDefinition:createdFrom", + "value": "ecs-console-v2" + }, + { + "key": "organization", + "value": "atd" + }, + { + "key": "awsApplication", + "value": "arn:aws:resource-groups:us-east-1:295525487728:group/Moped/0d1wuyh5kinejubg588qch09jg" + } + ] +}