Skip to content

Commit b21459b

Browse files
authored
Merge pull request #6195 from NHSDigital/add-data-migration-gem
Automate post-deployment steps (Feature Flags & Data Migrations)
2 parents 5bd35e1 + 7274b76 commit b21459b

9 files changed

Lines changed: 216 additions & 100 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 140 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,10 @@ jobs:
181181
steps:
182182
- run: echo "Proceeding with deployment to $environment"
183183

184-
run-migrations:
185-
name: Run migrations
184+
pre-deploy:
185+
name: Pre deploy
186186
runs-on: ubuntu-latest
187187
needs: approve-deployments
188-
if: ${{ inputs.server_types == 'all' || inputs.server_types == 'ops' }}
189188
permissions:
190189
id-token: write
191190
steps:
@@ -201,10 +200,10 @@ jobs:
201200
with:
202201
path: ${{ runner.temp }}
203202
name: ${{ inputs.environment }}-ops-task-definition
204-
- name: Register migration task definition
205-
id: register-migration-task-definition
203+
- name: Register pre deploy task definition
204+
id: register-pre-deploy-task-definition
206205
run: |
207-
family_name="mavis-migration-task-definition-$environment"
206+
family_name="mavis-pre-deploy-task-definition-$environment"
208207
file_path="${{ runner.temp }}/migration-task-definition.json"
209208
jq --arg f "$family_name" '.family = $f' \
210209
"${{ runner.temp }}/ops-task-definition.json" > "$file_path"
@@ -220,7 +219,7 @@ jobs:
220219
SLACK_MAVIS_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_MAVIS_RELEASES_WEBHOOK_URL }}
221220
# yamllint disable rule:line-length
222221
run: |
223-
TASK_DEFINITION_ARN=${{ steps.register-migration-task-definition.outputs.task_definition_arn }}
222+
PRE_DEPLOY_TASK_DEFINITION_ARN=${{ steps.register-pre-deploy-task-definition.outputs.task_definition_arn }}
224223
SUBNET_ID=$(aws ec2 describe-subnets --filters "Name=tag:Name,Values=private-subnet-$environment-a" --query 'Subnets[0].SubnetId' --output text)
225224
SECURITY_GROUP_ID=$(aws ec2 describe-security-groups --filters "Name=group-name,Values=ops-service-$environment" --query 'SecurityGroups[0].GroupId' --output text)
226225
@@ -230,7 +229,7 @@ jobs:
230229
while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
231230
TASK_ARN=$(aws ecs run-task \
232231
--cluster "$cluster_name" \
233-
--task-definition "$TASK_DEFINITION_ARN" \
232+
--task-definition "$PRE_DEPLOY_TASK_DEFINITION_ARN" \
234233
--launch-type FARGATE \
235234
--network-configuration "awsvpcConfiguration={subnets=[$SUBNET_ID],securityGroups=[$SECURITY_GROUP_ID]}" \
236235
--overrides '{
@@ -314,8 +313,8 @@ jobs:
314313
deploy-service:
315314
name: Deploy service
316315
runs-on: ubuntu-latest
317-
needs: [prepare-deployment, approve-deployments, run-migrations]
318-
if: ${{ !cancelled() && needs.run-migrations.result != 'failure' }}
316+
needs: [prepare-deployment, approve-deployments, pre-deploy]
317+
if: ${{ !cancelled() }}
319318
permissions:
320319
id-token: write
321320
strategy:
@@ -362,14 +361,141 @@ jobs:
362361
exit 1
363362
fi
364363
# yamllint enable rule:line-length
364+
post-deploy:
365+
name: Post deploy
366+
runs-on: ubuntu-latest
367+
needs: [approve-deployments, deploy-service]
368+
if: ${{ !cancelled() && needs.deploy-service.result != 'failure' }}
369+
permissions:
370+
id-token: write
371+
steps:
372+
- name: Checkout code
373+
uses: actions/checkout@v6
374+
- name: Configure AWS Credentials
375+
uses: aws-actions/configure-aws-credentials@v6
376+
with:
377+
role-to-assume: arn:aws:iam::${{ env.aws_account_id }}:role/GithubDeployECSService
378+
aws-region: eu-west-2
379+
- name: Register post deploy task definition
380+
id: register-post-deploy-task-definition
381+
run: |
382+
family_name="mavis-post-deploy-task-definition-$environment"
383+
file_path="${{ runner.temp }}/migration-task-definition.json"
384+
jq --arg f "$family_name" '.family = $f' \
385+
"${{ runner.temp }}/ops-task-definition.json" > "$file_path"
386+
task_definition_arn=$(aws ecs register-task-definition \
387+
--cli-input-json file://$file_path \
388+
--query 'taskDefinition.taskDefinitionArn' \
389+
--output text
390+
)
391+
echo "task_definition_arn=$task_definition_arn" >> "$GITHUB_OUTPUT"
392+
- name: Run data migrations
393+
id: run-data-migrations
394+
env:
395+
SLACK_MAVIS_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_MAVIS_RELEASES_WEBHOOK_URL }}
396+
# yamllint disable rule:line-length
397+
run: |
398+
POST_DEPLOY_TASK_DEFINITION_ARN=${{ steps.register-post-deploy-task-definition.outputs.task_definition_arn }}
399+
SUBNET_ID=$(aws ec2 describe-subnets --filters "Name=tag:Name,Values=private-subnet-$environment-a" --query 'Subnets[0].SubnetId' --output text)
400+
SECURITY_GROUP_ID=$(aws ec2 describe-security-groups --filters "Name=group-name,Values=ops-service-$environment" --query 'SecurityGroups[0].GroupId' --output text)
401+
402+
MAX_ATTEMPTS=3
403+
ATTEMPT=1
404+
405+
while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
406+
TASK_ARN=$(aws ecs run-task \
407+
--cluster "$cluster_name" \
408+
--task-definition "$POST_DEPLOY_TASK_DEFINITION_ARN" \
409+
--launch-type FARGATE \
410+
--network-configuration "awsvpcConfiguration={subnets=[$SUBNET_ID],securityGroups=[$SECURITY_GROUP_ID]}" \
411+
--overrides '{
412+
"containerOverrides": [{
413+
"name": "application",
414+
"command": ["bin/rails","data:migrate"]
415+
}]
416+
}' \
417+
--query 'tasks[0].taskArn' \
418+
--output text)
419+
420+
echo "Waiting for task to complete: $TASK_ARN"
421+
422+
# shellcheck disable=SC2001
423+
TASK_ID=$(sed 's:^.*/::' <<< "$TASK_ARN")
424+
AWS_CONSOLE_URL="https://eu-west-2.console.aws.amazon.com/ecs/v2/clusters/$cluster_name/tasks/$TASK_ID/logs"
425+
426+
echo "View logs in AWS Console: $AWS_CONSOLE_URL"
427+
if [ "$environment" = 'production' ]; then
428+
./.github/send_slack_notification.sh "${{ secrets.SLACK_MAVIS_RELEASES_WEBHOOK_URL }}" "$AWS_CONSOLE_URL" "Running data migrations attempt $ATTEMPT/$MAX_ATTEMPTS"
429+
fi
430+
431+
MAX_WAIT_TIME=3600
432+
POLL_INTERVAL=10 # Poll every 10 seconds
433+
ELAPSED=0
434+
435+
while [ "$ELAPSED" -lt "$MAX_WAIT_TIME" ]; do
436+
TASK_STATUS=$(aws ecs describe-tasks \
437+
--cluster "$cluster_name" \
438+
--tasks "$TASK_ID" \
439+
--query 'tasks[0].lastStatus' \
440+
--output text)
441+
442+
if [ "$TASK_STATUS" = "STOPPED" ]; then
443+
echo "Task has stopped"
444+
break
445+
fi
446+
447+
sleep "$POLL_INTERVAL"
448+
ELAPSED=$((ELAPSED + POLL_INTERVAL))
449+
done
450+
451+
if [ "$ELAPSED" -ge "$MAX_WAIT_TIME" ]; then
452+
echo "ERROR: Data migration task did not complete within $MAX_WAIT_TIME seconds."
453+
exit 1
454+
fi
455+
456+
EXIT_CODE=$(aws ecs describe-tasks \
457+
--cluster "$cluster_name" \
458+
--tasks "$TASK_ARN" \
459+
--query 'tasks[0].containers[0].exitCode' \
460+
--output text)
461+
462+
echo "Container exit code: $EXIT_CODE"
463+
464+
if [ "$EXIT_CODE" = "0" ]; then
465+
echo "Data migrations completed"
466+
break
467+
else
468+
echo "ECS task failed with exit code: $EXIT_CODE"
469+
if [ "$ATTEMPT" = "$MAX_ATTEMPTS" ]; then
470+
exit 1
471+
fi
472+
ATTEMPT=$((ATTEMPT+1))
473+
fi
474+
done
475+
# yamllint enable rule:line-length
476+
- name: Notify data migrations completed
477+
if: ${{ env.environment == 'production' }}
478+
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a
479+
with:
480+
webhook: ${{ secrets.SLACK_MAVIS_RELEASES_WEBHOOK_URL }}
481+
webhook-type: incoming-webhook
482+
payload: |
483+
text: "Data migrations finished successfully :white_check_mark:"
484+
blocks:
485+
- type: "section"
486+
text:
487+
type: "mrkdwn"
488+
text: "Data migrations finished successfully :white_check_mark:"
365489
notify-deployment-complete:
366490
name: Notify deployment status
367491
runs-on: ubuntu-latest
368-
needs: [deploy-service, run-migrations]
492+
needs: [pre-deploy, deploy-service, post-deploy]
369493
if: ${{ !cancelled() && inputs.environment == 'production' }}
370494
steps:
371495
- name: Notify deployment success
372-
if: ${{ needs.deploy-service.result == 'success' }}
496+
if: >-
497+
${{ needs.deploy-service.result == 'success' ||
498+
needs.post-deploy.result == 'success' }}
373499
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a
374500
with:
375501
webhook: ${{ secrets.SLACK_MAVIS_RELEASES_WEBHOOK_URL }}
@@ -384,7 +510,7 @@ jobs:
384510
- name: Notify deployment failure
385511
if: >-
386512
${{ needs.deploy-service.result == 'failure' ||
387-
needs.run-migrations.result == 'failure' }}
513+
needs.pre-deploy.result == 'failure' }}
388514
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a
389515
with:
390516
webhook: ${{ secrets.SLACK_MAVIS_RELEASES_WEBHOOK_URL }}
@@ -400,7 +526,7 @@ jobs:
400526
- type: "section"
401527
fields:
402528
- type: "mrkdwn"
403-
text: "*Failed job:*\n${{ needs.deploy-service.result == 'failure' && 'deploy-service' || 'run-migrations' }}"
529+
text: "*Failed job:*\n${{ needs.deploy-service.result == 'failure' && 'deploy-service' || 'pre-deploy' || 'post-deploy' }}"
404530
- type: "mrkdwn"
405531
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View workflow run>"
406532
# yamllint enable rule:line-length

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ gem "caxlsx"
3131
gem "charlock_holmes"
3232
gem "config"
3333
gem "csv"
34+
gem "data_migrate"
3435
gem "devise"
3536
gem "discard"
3637
gem "dry-cli"

Gemfile.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,9 @@ GEM
275275
cuprite (0.17)
276276
capybara (~> 3.0)
277277
ferrum (~> 0.17.0)
278+
data_migrate (11.3.1)
279+
activerecord (>= 6.1)
280+
railties (>= 6.1)
278281
database_cleaner-active_record (2.2.2)
279282
activerecord (>= 5.a)
280283
database_cleaner-core (~> 2.0)
@@ -960,6 +963,7 @@ DEPENDENCIES
960963
cssbundling-rails
961964
csv
962965
cuprite
966+
data_migrate
963967
database_cleaner-active_record
964968
debug
965969
devise

app/lib/data_migration/backfill_notify_log_entries.rb

Lines changed: 0 additions & 49 deletions
This file was deleted.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# frozen_string_literal: true
2+
3+
class BackfillPurposeForNotifyLogEntries < ActiveRecord::Migration[8.1]
4+
def up
5+
migration = self.class.name
6+
started_at = Time.zone.now
7+
8+
batch_size = 1000
9+
total_records = NotifyLogEntry.count
10+
total_batches = (total_records / batch_size.to_f).ceil
11+
12+
batch_processed = 0
13+
records_updated = 0
14+
15+
# Helps us monitor progress in CloudWatch
16+
Rails.logger.info(event: "data_migration_start", migration:, total_records:, total_batches:)
17+
18+
NotifyLogEntry.find_in_batches(batch_size:) do |notify_log_entries|
19+
notify_log_entries.filter_map do |notify_log_entry|
20+
template_name = NotifyTemplate.find_by_id(
21+
notify_log_entry.template_id,
22+
channel: notify_log_entry.type.to_sym
23+
)&.name
24+
25+
next unless template_name
26+
27+
purpose = NotifyLogEntry.purpose_for_template_name(template_name)
28+
29+
next unless purpose
30+
31+
notify_log_entry.update_column(
32+
:purpose,
33+
NotifyLogEntry.purposes.fetch(purpose)
34+
)
35+
36+
records_updated += 1
37+
end
38+
39+
batch_processed += 1
40+
41+
Rails.logger.info(
42+
event: "data_migration_batch",
43+
migration:,
44+
records_updated:,
45+
batch_size:,
46+
batch_processed:,
47+
total_batches:,
48+
)
49+
end
50+
51+
duration_minutes = ((Time.zone.now - started_at) / 60.0).round
52+
53+
Rails.logger.info(
54+
event: "data_migration_finish",
55+
migration:,
56+
duration_minutes:,
57+
total_records:,
58+
records_updated:,
59+
)
60+
end
61+
62+
def down
63+
raise ActiveRecord::IrreversibleMigration
64+
end
65+
end

db/data_schema.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# frozen_string_literal: true
2+
3+
DataMigrate::Data.define(version: 20_260_304_173_751)

db/schema.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,9 @@
311311
t.index ["team_id"], name: "index_consents_on_team_id"
312312
end
313313

314+
create_table "data_migrations", primary_key: "version", id: :string, force: :cascade do |t|
315+
end
316+
314317
create_table "flipper_features", force: :cascade do |t|
315318
t.datetime "created_at", null: false
316319
t.string "key", null: false

lib/tasks/data_migration.rake

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)