Repository navigation
Deploy to Amazon ECS #14888
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |