-
Notifications
You must be signed in to change notification settings - Fork 0
181 lines (168 loc) · 8.74 KB
/
Copy pathdeploy.yml
File metadata and controls
181 lines (168 loc) · 8.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# =============================================================================
# hasna self-hosted deploy pipeline — drop this file in .github/workflows/deploy.yml
#
# The ONLY per-repo edit is the `APP` env var below. Everything else is derived
# from AWS at run time via the /hasna/deploy/<APP> SSM manifest published by the
# hasna-app Terraform module (modules/deploy-oidc-role).
#
# What it does:
# 1. Assumes the per-repo least-priv OIDC role (no long-lived keys).
# 2. Builds a NATIVE arm64 image on a GitHub arm64 runner (no QEMU emulation).
# 3. Pushes it to the app's ECR repo, tagged with the commit SHA.
# 4. Runs the one-shot DB migration task and fails hard on non-zero exit.
# 5. Registers a new web task-def revision and updates the ECS service.
# 6. Waits for the service to reach steady state and asserts the deployment
# circuit breaker reported COMPLETED (rollback => job fails).
#
# Prereqs (created by the hasna-app module): ECR repo, ECS cluster/service,
# web + migration task-def families, task/execution roles, and the SSM manifest.
# The role name is <APP>-prod-gha-deploy; its trust is pinned to
# repo:hasna/<APP>:environment:production, so this job MUST run in the
# `production` GitHub Environment (set below).
# =============================================================================
name: deploy
on:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch: {}
# Serialize production deploys; never cancel an in-flight one mid-rollout.
concurrency:
group: deploy-production
cancel-in-progress: false
permissions:
contents: read
id-token: write # required to mint the GitHub OIDC token
env:
# >>> THE ONLY LINE EACH REPO CHANGES <<<
APP: domains
# Locked platform defaults.
AWS_REGION: us-east-1
AWS_ACCOUNT_ID: "789877399345"
jobs:
deploy:
name: build + migrate + deploy
runs-on: ubuntu-24.04-arm # native arm64 runner — image is built without QEMU
environment: production # MUST match the OIDC subject repo:hasna/<APP>:environment:production
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials (GitHub OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/${{ env.APP }}-prod-gha-deploy
aws-region: ${{ env.AWS_REGION }}
role-session-name: gha-deploy-${{ env.APP }}-${{ github.run_id }}
- name: Load deploy manifest
id: m
run: |
set -euo pipefail
M="$(aws ssm get-parameter --name "/hasna/deploy/${APP}" --query Parameter.Value --output text)"
get() { jq -er ".$1" <<<"$M"; }
{
echo "cluster=$(get cluster)"
echo "service=$(get service)"
echo "web_family=$(get web_task_family)"
echo "web_container=$(get web_container)"
echo "mig_family=$(get migration_task_family)"
echo "mig_container=$(get migration_container)"
echo "ecr_url=$(get ecr_repository_url)"
echo "assign_public_ip=$(get assign_public_ip)"
echo "subnets=$(jq -er '.subnets | join(",")' <<<"$M")"
echo "sgs=$(jq -er '.security_groups | join(",")' <<<"$M")"
} >> "$GITHUB_OUTPUT"
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push native arm64 image
id: build
env:
ECR_URL: ${{ steps.m.outputs.ecr_url }}
run: |
set -euo pipefail
IMAGE="${ECR_URL}:${GITHUB_SHA}"
# Runner is arm64, so linux/arm64 builds natively — no emulation.
# ECR repo uses IMMUTABLE tags, so we push ONLY the unique SHA tag —
# a moving ":latest" cannot be overwritten and would fail the push.
docker buildx build \
--platform linux/arm64 \
--provenance=false \
--cache-from "type=gha" \
--cache-to "type=gha,mode=max" \
--tag "${IMAGE}" \
--push \
.
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
- name: Run one-shot migration task
env:
IMAGE: ${{ steps.build.outputs.image }}
CLUSTER: ${{ steps.m.outputs.cluster }}
MIG_FAMILY: ${{ steps.m.outputs.mig_family }}
MIG_CONTAINER: ${{ steps.m.outputs.mig_container }}
SUBNETS: ${{ steps.m.outputs.subnets }}
SGS: ${{ steps.m.outputs.sgs }}
ASSIGN: ${{ steps.m.outputs.assign_public_ip }}
run: |
set -euo pipefail
# Register a migration revision pinned to the new image.
NEW_TD="$(aws ecs describe-task-definition --task-definition "$MIG_FAMILY" \
--query taskDefinition | jq --arg img "$IMAGE" --arg c "$MIG_CONTAINER" '
.containerDefinitions |= map(if .name==$c then .image=$img else . end)
| del(.taskDefinitionArn,.revision,.status,.requiresAttributes,.compatibilities,.registeredAt,.registeredBy,.deregisteredAt)')"
MIG_ARN="$(aws ecs register-task-definition --cli-input-json "$NEW_TD" \
--query taskDefinition.taskDefinitionArn --output text)"
echo "Running migration task def: $MIG_ARN"
TASK_ARN="$(aws ecs run-task --cluster "$CLUSTER" --task-definition "$MIG_ARN" \
--launch-type FARGATE --count 1 \
--started-by "gha-migrate-${GITHUB_RUN_ID}" \
--network-configuration "awsvpcConfiguration={subnets=[$SUBNETS],securityGroups=[$SGS],assignPublicIp=$ASSIGN}" \
--query 'tasks[0].taskArn' --output text)"
if [ -z "$TASK_ARN" ] || [ "$TASK_ARN" = "None" ]; then
echo "::error::migration task failed to start"; exit 1
fi
echo "Waiting for migration task to stop: $TASK_ARN"
aws ecs wait tasks-stopped --cluster "$CLUSTER" --tasks "$TASK_ARN"
DESC="$(aws ecs describe-tasks --cluster "$CLUSTER" --tasks "$TASK_ARN")"
EXIT="$(jq -r ".tasks[0].containers[] | select(.name==\"$MIG_CONTAINER\") | .exitCode // \"null\"" <<<"$DESC")"
REASON="$(jq -r '.tasks[0].stoppedReason // ""' <<<"$DESC")"
echo "migration exitCode=$EXIT stoppedReason=$REASON"
if [ "$EXIT" != "0" ]; then
echo "::error::migration task did not exit 0 (exit=$EXIT, reason=$REASON)"; exit 1
fi
- name: Deploy service (new revision) and wait for stable
env:
IMAGE: ${{ steps.build.outputs.image }}
CLUSTER: ${{ steps.m.outputs.cluster }}
SERVICE: ${{ steps.m.outputs.service }}
WEB_FAMILY: ${{ steps.m.outputs.web_family }}
WEB_CONTAINER: ${{ steps.m.outputs.web_container }}
run: |
set -euo pipefail
NEW_TD="$(aws ecs describe-task-definition --task-definition "$WEB_FAMILY" \
--query taskDefinition | jq --arg img "$IMAGE" --arg c "$WEB_CONTAINER" '
.containerDefinitions |= map(if .name==$c then .image=$img else . end)
| del(.taskDefinitionArn,.revision,.status,.requiresAttributes,.compatibilities,.registeredAt,.registeredBy,.deregisteredAt)')"
WEB_ARN="$(aws ecs register-task-definition --cli-input-json "$NEW_TD" \
--query taskDefinition.taskDefinitionArn --output text)"
echo "Updating $SERVICE -> $WEB_ARN"
aws ecs update-service --cluster "$CLUSTER" --service "$SERVICE" \
--task-definition "$WEB_ARN" >/dev/null
echo "Waiting for service to reach steady state..."
aws ecs wait services-stable --cluster "$CLUSTER" --services "$SERVICE"
# Deployment circuit breaker: a rolled-back deploy is a FAILED deploy.
# After a rollback the PRIMARY deployment can still report COMPLETED (the
# *rollback* completed) while running the OLD task def, so assert BOTH the
# rollout state AND that the live PRIMARY task def is the one we deployed.
SVC="$(aws ecs describe-services --cluster "$CLUSTER" --services "$SERVICE")"
RS="$(jq -r '.services[0].deployments[] | select(.status=="PRIMARY") | .rolloutState' <<<"$SVC")"
LIVE_TD="$(jq -r '.services[0].deployments[] | select(.status=="PRIMARY") | .taskDefinition' <<<"$SVC")"
echo "primary rolloutState=$RS liveTaskDef=$LIVE_TD deployed=$WEB_ARN"
if [ "$RS" != "COMPLETED" ]; then
echo "::error::deployment did not complete (rolloutState=$RS) — likely circuit-breaker rollback"; exit 1
fi
if [ "$LIVE_TD" != "$WEB_ARN" ]; then
echo "::error::live task def ($LIVE_TD) != deployed ($WEB_ARN) — deployment was rolled back"; exit 1
fi
echo "Deploy of ${APP} @ ${GITHUB_SHA} succeeded."