-
Notifications
You must be signed in to change notification settings - Fork 0
302 lines (263 loc) · 11.3 KB
/
_function_app_deploy.yml
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
on:
workflow_call:
inputs:
workspace_name:
type: string
required: true
description: The name of the workspace to create the artifact for.
environment:
description: Environment where the artifact will be deployed.
type: string
required: true
resource_group_name:
description: Function App resource group name.
type: string
required: true
function_app_name:
description: Function App name.
type: string
required: true
health_check_path:
description: The health probe path exposed by the Function App.
type: string
required: true
use_staging_slot:
description: True if artifact should be deployed to staging slot
type: boolean
required: false
default: true
use_private_agent:
description: Use a private agent to deploy the built artifact.
type: boolean
required: false
default: true
use_canary_deploy:
description: Use the automated canary deploy.
type: boolean
required: false
default: true
log_analytics_workspace_id:
description: Log Analitycs Workspace ID for canary query checks
type: string
required: false
canary_increment_step:
description: Every step increase the canary traffic by this percentage
type: number
required: false
default: 10
canary_next_step_after_ms:
description: Time between canary traffic increment steps
type: number
required: false
default: 300000
concurrency:
group: ${{ github.workflow }}-cd
cancel-in-progress: true
env:
BUNDLE_NAME: bundle
jobs:
build:
name: Build Artifact
runs-on: ubuntu-20.04
env:
WORKSPACE: ${{ inputs.workspace_name }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
name: Checkout
- name: Prune
run: npx [email protected] prune --scope ${{ env.WORKSPACE }}
- name: Setup Node.js
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version-file: ".node-version"
cache: "yarn"
cache-dependency-path: "./out/yarn.lock"
- name: Install dependencies
run: yarn install --immutable
working-directory: ./out
- name: Restore turbo cache
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
with:
path: ./out/node_modules/.cache/turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo
- name: Build
run: yarn build
working-directory: ./out
- name: Build the Function App Artifact
id: make-function-app-artifact
run: |
npm pkg set --json "bundledDependencies"=true
npm pkg set --json "files"='["**/function.json", "dist", "host.json","extensions.csproj", ".node-version"]'
npx npm-pack-zip
mv $(jq -r .name package.json).zip ${{ env.BUNDLE_NAME }}.zip
echo "artifact-path=$(realpath ${{ env.BUNDLE_NAME }}.zip)" >> "$GITHUB_OUTPUT"
working-directory: ./out/apps/${{ env.WORKSPACE }}
- name: Upload Artifact
uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0
with:
name: ${{ env.BUNDLE_NAME }}
path: ${{ steps.make-function-app-artifact.outputs.artifact-path }}
if-no-files-found: error
retention-days: 7
deploy:
name: Deploy
if: ${{ !github.event.act }}
needs: [build]
runs-on: ${{ inputs.use_private_agent == true && 'self-hosted' || 'ubuntu-20.04' }}
environment: ${{ inputs.environment }}-cd
permissions:
id-token: write
contents: read
env:
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
ARM_USE_OIDC: true
RESOURCE_GROUP_NAME: ${{ inputs.resource_group_name }}
FUNCTION_APP_NAME: ${{ inputs.function_app_name}}
HEALTH_CHECK_PATH: ${{ inputs.health_check_path }}
USE_STAGING_SLOT: ${{ inputs.use_staging_slot }}
USE_CANARY_DEPLOY: ${{ inputs.use_canary_deploy }}
CANARY_INCREMENT_STEP: ${{ inputs.canary_increment_step }}
CANARY_NEXT_STEP_AFTER_MS: ${{ inputs.canary_next_step_after_ms }}
LOG_ANALITYCS_WORKSPACE_ID: ${{ inputs.log_analytics_workspace_id }}
steps:
- name: Download Artifact
uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1
with:
name: ${{ env.BUNDLE_NAME }}
- name: Azure Login
uses: azure/login@v2 # v2.0.0
with:
client-id: ${{ env.ARM_CLIENT_ID }}
tenant-id: ${{ env.ARM_TENANT_ID }}
subscription-id: ${{ env.ARM_SUBSCRIPTION_ID }}
- name: Deploy
uses: azure/webapps-deploy@v2
if: ${{ env.USE_STAGING_SLOT == 'false' }}
with:
resource-group-name: ${{ env.RESOURCE_GROUP_NAME }}
app-name: ${{ env.FUNCTION_APP_NAME }}
package: ${{ env.BUNDLE_NAME }}.zip
- name: Deploy to Staging Slot
uses: azure/webapps-deploy@v2
if: ${{ env.USE_STAGING_SLOT == 'true' }}
with:
resource-group-name: ${{ env.RESOURCE_GROUP_NAME }}
app-name: ${{ env.FUNCTION_APP_NAME }}
slot-name: staging
package: ${{ env.BUNDLE_NAME }}.zip
- name: Ping Staging Health
if: ${{ env.USE_STAGING_SLOT == 'true' }}
run: |
curl \
--retry 5 \
--retry-max-time 120 \
--retry-all-errors \
-f 'https://${{ env.FUNCTION_APP_NAME }}-staging.azurewebsites.net${{ env.HEALTH_CHECK_PATH }}'
- name: Extract actifact bundle code
if: ${{ env.USE_STAGING_SLOT == 'true' && env.USE_CANARY_DEPLOY == 'true' }}
run: |
unzip -qq ${{ env.BUNDLE_NAME }}.zip -d .
- name: Setup Node.js
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
if: ${{ env.USE_STAGING_SLOT == 'true' && env.USE_CANARY_DEPLOY == 'true' }}
with:
node-version-file: ".node-version"
- name: Canary traffic increase step
id: canary
continue-on-error: true
if: ${{ env.USE_STAGING_SLOT == 'true' && env.USE_CANARY_DEPLOY == 'true' }}
run: |
#!/usr/bin/env bash
set -e
currentPercentage=0
echo "=============================="
echo "Starting Canary Deployment..."
echo "=============================="
while true; do
echo "------------------------------------------------"
echo "Current Percentage of Traffic: $currentPercentage%"
echo "------------------------------------------------"
# Run the script that calculates the next traffic percentage
# and the wait interval before the next increment
output="$(npm run -s canary $currentPercentage)"
exit_code=$?
echo "Script Output: $output"
# If the script exits with a non-zero code, perform a rollback
# and fail the pipeline.
if [ $exit_code -ne 0 ]; then
echo "Monitoring script exited with error."
exit 1
fi
# Parse JSON's output using jq.
swap=$(echo "$output" | jq -r '.swap // empty')
if [ "$swap" == "true" ]; then
echo "Swap requested. Reached 100% or threshold."
echo "Exiting the loop to proceed with final swap."
break
fi
nextIncrementPercentage=$(echo "$output" | jq -r '.nextIncrementPercentage')
afterMs=$(echo "$output" | jq -r '.afterMs')
# Validate that the fields are present
if [ -z "$nextIncrementPercentage" ] || [ -z "$afterMs" ]; then
echo "Invalid output from script (missing nextIncrementPercentage or afterMs)."
exit 1
fi
echo "Next Increment: $nextIncrementPercentage%"
echo "Waiting interval: $afterMs ms"
# Set the new traffic percentage for the staging slot
# The rest goes to production
az webapp traffic-routing set \
--name ${{env.FUNCTION_APP_NAME}} \
--resource-group ${{env.RESOURCE_GROUP_NAME}} \
--distribution staging=$nextIncrementPercentage
# Update the currentPercentage variable
currentPercentage=$nextIncrementPercentage
# Wait the specified time before the next iteration
delaySeconds=$((afterMs / 1000))
echo "Waiting for $delaySeconds seconds..."
while [ "$delaySeconds" -gt 0 ]; do
if [ "$delaySeconds" -gt 10 ]; then
sleep 10
delaySeconds=$((delaySeconds - 10))
echo "Waiting for $delaySeconds seconds..."
else
sleep "$delaySeconds"
delaySeconds=0
fi
done
# If nextIncrementPercentage reach 100
# the loop is interrupted
if [ "$nextIncrementPercentage" -ge 100 ]; then
echo "Reached 100% traffic on staging. Exiting loop."
break
fi
done
echo "======================================================"
echo "Canary deployment validation completed."
echo "======================================================"
echo "CANARY_SUCCESS=true" >> "$GITHUB_OUTPUT"
- name: Restore staging slot to 0% traffic
if: ${{ env.USE_STAGING_SLOT == 'true' && env.USE_CANARY_DEPLOY == 'true' }}
run: |
az webapp traffic-routing clear \
-g ${{ env.RESOURCE_GROUP_NAME }} \
-n ${{ env.FUNCTION_APP_NAME }}
- name: Canary step is failed
if: ${{ env.USE_STAGING_SLOT == 'true' && env.USE_CANARY_DEPLOY == 'true' && steps.canary.outputs.CANARY_SUCCESS != 'true' }}
run: |
echo "=========================================================="
echo "Canary deployment failed. The pipeline will be terminated."
echo "=========================================================="
exit 1
- name: Swap Staging and Production Slots
if: ${{ env.USE_STAGING_SLOT == 'true' }}
run: |
az webapp deployment slot swap \
-g ${{ env.RESOURCE_GROUP_NAME }} \
-n ${{ env.FUNCTION_APP_NAME }} \
--slot staging \
--target-slot production