Repository navigation
361 lines (314 loc) · 15.2 KB
/
Copy pathdeploy.yml
File metadata and controls
361 lines (314 loc) · 15.2 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
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
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
name: Deploy to Amazon ECS
env:
aws_region: eu-west-2
webservers_cluster_name: webservers
sidekiq_cluster_name: sidekiq
website_task_definition_name: website
website_service_name: website
api_task_definition_name: api
api_service_name: api
anycable_task_definition_name: anycable
anycable_service_name: anycable
sidekiq_task_definition_name: sidekiq
sidekiq_service_name: sidekiq
ecr_repository_rails: "webserver-rails"
dockerfile_rails: "docker/rails.Dockerfile"
ecr_repository_nginx: "webserver-nginx"
dockerfile_nginx: "docker/nginx.Dockerfile"
ecr_repository_anycable_go: "anycable-go"
dockerfile_anycable_go: "docker/anycable-go.Dockerfile"
# TODO: (Required) Move this to be a secret
s3_bucket: "exercism-v3-assets"
# The only config the Rails image build needs: both are compiled into the
# JS, which every browser downloads. The build boots the app without AWS
# (see config/image_build.rb), so it has no route to any other setting.
website_assets_host: "https://assets.exercism.org"
sentry_js_dsn: ${{ vars.SENTRY_JS_DSN }}
on:
push:
branches: [main]
workflow_dispatch:
schedule:
- cron: 0 */2 * * *
# Nothing in this job reads or writes the repository through the Actions
# token: AWS and Cloudflare are reached with their own credentials.
permissions:
contents: read
# A deploy rolls four ECS services and then uploads the Worker, so two runs
# overlapping would race each other onto the cluster. Runs queue instead of
# cancelling: a half-finished rollout is worse than a slow one.
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
deploy:
# Forks of this repository get the workflow file along with everything
# else, and org secrets are not shared with them. This keeps the job to
# the one repository that is meant to deploy.
if: github.repository == 'exercism/website'
name: Upload images to ECR, assets to S3, and definition to ECS
runs-on: ubuntu-latest
steps:
###############
###############
# Setup Steps #
###############
###############
- name: Checkout
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
# The Rails image, its build stage and the BuildKit cache together need
# more than a hosted runner has spare, so the preinstalled toolchains we
# never touch are deleted first. This buys about 20GB in under a minute.
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \
/usr/local/.ghcup /opt/hostedtoolcache/CodeQL
df -h /
# The gha cache backend below is unavailable to the default docker
# driver, so the builds run on a docker-container builder.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e1e17a757e536f70e52b5a12b2e8d1d1c60e04ef
with:
aws-access-key-id: ${{ secrets.AWS_DEPLOY_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_DEPLOY_SECRET_ACCESS_KEY }}
aws-region: ${{ env.aws_region }}
- name: Login to Amazon ECR
id: login_to_ecr
uses: aws-actions/amazon-ecr-login@2f9f10ea3fa2eed41ac443fee8bfbd059af2d0a4
#########################
#########################
# Build the Rails image #
#########################
#########################
# The bundler version is computed from the checkout, so it is resolved
# here and handed to both of the builds below unchanged: the assets
# export only reuses the build stage's cache if its arguments match the
# push build's byte for byte.
- name: Resolve image tags and build arguments
id: images
env:
ECR_REGISTRY: ${{ steps.login_to_ecr.outputs.registry }}
run: |
{
echo "rails=$ECR_REGISTRY/${{ env.ecr_repository_rails }}:${{ github.sha }}"
echo "nginx=$ECR_REGISTRY/${{ env.ecr_repository_nginx }}:${{ github.sha }}"
echo "bundler_version=$(awk '/BUNDLED WITH/ {getline;$1=$1;print}' Gemfile.lock)"
} >> "$GITHUB_OUTPUT"
# mode=max so the build stage is cached too. Everything slow (bundle
# install, yarn install, assets:precompile) happens there, and a cache of
# the shipped image alone would still have to rebuild all of it.
#
# provenance is off so ECR keeps receiving a plain image manifest, which
# is what the task definitions below point at.
#
# This repository is public, and a pull request from a fork can restore
# caches written on main. The build takes no secrets and no AWS
# credentials, so these layers hold nothing beyond the repository and
# the two public values above.
- name: Build and Push Rails Docker
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc
with:
context: .
file: ${{ env.dockerfile_rails }}
target: runtime
push: true
provenance: false
tags: |
${{ steps.images.outputs.rails }}
${{ steps.login_to_ecr.outputs.registry }}/${{ env.ecr_repository_rails }}:latest
build-args: |
BUNDLER_VERSION=${{ steps.images.outputs.bundler_version }}
WEBSITE_ASSETS_HOST=${{ env.website_assets_host }}
SENTRY_JS_DSN=${{ env.sentry_js_dsn }}
cache-from: type=gha,scope=rails
cache-to: type=gha,mode=max,scope=rails,timeout=30m
##################################
##################################
# Extact and upload assets to s3 #
##################################
##################################
# The build stage is still in the builder's cache from the step above, so
# this resolves in seconds and writes out only the compiled assets.
- name: Copy assets from Dockerfile
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc
with:
context: .
file: ${{ env.dockerfile_rails }}
target: assets
outputs: type=local,dest=assets
build-args: |
BUNDLER_VERSION=${{ steps.images.outputs.bundler_version }}
WEBSITE_ASSETS_HOST=${{ env.website_assets_host }}
SENTRY_JS_DSN=${{ env.sentry_js_dsn }}
cache-from: type=gha,scope=rails
- name: Upload assets to s3
run: |
aws s3 sync assets s3://${{ env.s3_bucket }}/assets --acl public-read --no-progress --cache-control max-age=31536000
#########################
#########################
# Build the Nginx image #
#########################
#########################
# No layer cache: this is a base image and one COPY, so restoring a cache
# would cost more than the build it saves.
- name: Build and Push Nginx Docker
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc
with:
context: .
file: ${{ env.dockerfile_nginx }}
push: true
provenance: false
tags: |
${{ steps.images.outputs.nginx }}
${{ steps.login_to_ecr.outputs.registry }}/${{ env.ecr_repository_nginx }}:latest
##################################
##################################
# Update website Task definition #
##################################
##################################
# We get the existing task definition to update
# Note that this is passed into the first update action
# but then chained from action to action for the others.
- name: Download website task definition
run: |
aws ecs describe-task-definition --task-definition ${{ env.website_task_definition_name }} --query taskDefinition > website-task-definition.json
- name: Update website | puma container
id: update_definition_for_website_puma
uses: aws-actions/amazon-ecs-render-task-definition@61b0c00c3743b70987a73a1faf577f0d167d1574
with:
task-definition: website-task-definition.json
container-name: puma
image: "${{ steps.images.outputs.rails }}"
- name: Update website | nginx container
id: update_definition_for_website_nginx
uses: aws-actions/amazon-ecs-render-task-definition@61b0c00c3743b70987a73a1faf577f0d167d1574
with:
task-definition: ${{ steps.update_definition_for_website_puma.outputs.task-definition }}
container-name: nginx
image: "${{ steps.images.outputs.nginx }}"
# Push the new definition up to ECS and deploy the new service from it
- name: Deploy Website
uses: aws-actions/amazon-ecs-deploy-task-definition@df9643053eda01f169e64a0e60233aacca83799a
with:
task-definition: ${{ steps.update_definition_for_website_nginx.outputs.task-definition }}
service: ${{ env.website_service_name }}
cluster: ${{ env.webservers_cluster_name }}
wait-for-service-stability: false
##############################
##############################
# Update api Task definition #
##############################
##############################
# We get the existing task definition to update
# Note that this is passed into the first update action
# but then chained from action to action for the others.
- name: Download api task definition
run: |
aws ecs describe-task-definition --task-definition ${{ env.api_task_definition_name }} --query taskDefinition > api-task-definition.json
- name: Update api | puma container
id: update_definition_for_api_puma
uses: aws-actions/amazon-ecs-render-task-definition@61b0c00c3743b70987a73a1faf577f0d167d1574
with:
task-definition: api-task-definition.json
container-name: puma
image: "${{ steps.images.outputs.rails }}"
- name: Update api | nginx container
id: update_definition_for_api_nginx
uses: aws-actions/amazon-ecs-render-task-definition@61b0c00c3743b70987a73a1faf577f0d167d1574
with:
task-definition: ${{ steps.update_definition_for_api_puma.outputs.task-definition }}
container-name: nginx
image: "${{ steps.images.outputs.nginx }}"
# Push the new definition up to ECS and deploy the new service from it
- name: Deploy API
uses: aws-actions/amazon-ecs-deploy-task-definition@df9643053eda01f169e64a0e60233aacca83799a
with:
task-definition: ${{ steps.update_definition_for_api_nginx.outputs.task-definition }}
service: ${{ env.api_service_name }}
cluster: ${{ env.webservers_cluster_name }}
wait-for-service-stability: false
##################################
##################################
# Update anycable Task definition #
##################################
##################################
# We get the existing task definition to update
# Note that this is passed into the first update action
# but then chained from action to action for the others.
- name: Download anycable task definition
run: |
aws ecs describe-task-definition --task-definition ${{ env.anycable_task_definition_name }} --query taskDefinition > anycable-task-definition.json
- name: Update anycable | anycable_ruby container
id: update_definition_for_anycable_ruby
uses: aws-actions/amazon-ecs-render-task-definition@61b0c00c3743b70987a73a1faf577f0d167d1574
with:
task-definition: anycable-task-definition.json
container-name: anycable_ruby
image: "${{ steps.images.outputs.rails }}"
- name: Update anycable | anycable_go container
id: update_definition_for_anycable_go
uses: aws-actions/amazon-ecs-render-task-definition@61b0c00c3743b70987a73a1faf577f0d167d1574
with:
task-definition: ${{ steps.update_definition_for_anycable_ruby.outputs.task-definition }}
container-name: anycable_go
image: "${{ steps.login_to_ecr.outputs.registry}}/anycable-go-pro:latest"
- name: Update anycable | nginx container
id: update_definition_for_anycable_nginx
uses: aws-actions/amazon-ecs-render-task-definition@61b0c00c3743b70987a73a1faf577f0d167d1574
with:
task-definition: ${{ steps.update_definition_for_anycable_go.outputs.task-definition }}
container-name: nginx
image: "${{ steps.images.outputs.nginx }}"
# Push the new definition up to ECS and deploy the new service from it
- name: Deploy Anycable
uses: aws-actions/amazon-ecs-deploy-task-definition@df9643053eda01f169e64a0e60233aacca83799a
with:
task-definition: ${{ steps.update_definition_for_anycable_nginx.outputs.task-definition }}
service: ${{ env.anycable_service_name }}
cluster: ${{ env.webservers_cluster_name }}
wait-for-service-stability: false
##################################
##################################
# Update Sidekiq Task definition #
##################################
##################################
# We get the existing task definition to update
# Note that this is passed into the first update action
# but then chained from action to action for the others.
- name: Download sidekiq task definition
run: |
aws ecs describe-task-definition --task-definition ${{ env.sidekiq_task_definition_name }} --query taskDefinition > sidekiq-task-definition.json
# Set puma to rails
- name: Update the sidekiq definition
id: update_definition_for_sidekiq
uses: aws-actions/amazon-ecs-render-task-definition@61b0c00c3743b70987a73a1faf577f0d167d1574
with:
task-definition: sidekiq-task-definition.json
container-name: sidekiq
image: "${{ steps.images.outputs.rails }}"
# Push the new definition up to ECS and deploy a new service from it.
- name: Deploy Sidekiq
uses: aws-actions/amazon-ecs-deploy-task-definition@df9643053eda01f169e64a0e60233aacca83799a
with:
# Ensure this is the final step in the daisy-chain above
task-definition: ${{ steps.update_definition_for_sidekiq.outputs.task-definition }}
service: ${{ env.sidekiq_service_name }}
cluster: ${{ env.sidekiq_cluster_name }}
wait-for-service-stability: true
################################
################################
# Deploy the Cloudflare Worker #
################################
################################
# Last in the job, so the Worker only starts sending visitors to a locale
# prefix once the web servers above have rolled out and can serve it. It
# reads config/i18n.json, the same file Rails reads, so both go out from
# one commit. Routes belong to exercism/terraform; this uploads the script.
- name: Deploy locale-redirect Worker
working-directory: cloudflare/locale-redirect
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_WORKERS_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: npx --yes wrangler@4.136.0 deploy