こんにちは。
ブックウォーカー開発本部 サービス開発部 Webサービス開発セクションのフサギコ(髙﨑)です。 Ruby on Railsによるバックエンドの実装運用とAWSによるサービスインフラの設計構築を主に担当するテックリードとして、ニコニコ漫画を開発しています。
本記事では、ニコニコ漫画新バックエンドのCPUアーキテクチャをx86_64からaarch64に移行して費用を削減した事例を通じて、
- GitHub Actionsでx86_64(AMD64) / aarch64(ARM64)向けのマルチアーキテクチャDockerイメージをビルドする方法
- ecspressoでFargateタスクのCPUアーキテクチャを切り替える方法
についてお話します。
ECS Fargateの費用を削減したい
今回取り上げるニコニコ漫画新バックエンドは、2020年5月ごろから開発しているRails製サーバアプリケーションで、現行PHP製サーバのビジネスロジック部のリプレイスを目的としています。 dockerイメージとしてbuildしてECS Fargate上で起動、運用しています。
開発から5年半以上が経過し、新バックエンドが担う機能が増えてきたことで、必要となるAPIサーバや非同期ジョブワーカーのタスク数も増えてきました。
AWSのCompute SavingsPlanは既に活用していましたが、更に費用を削減する方法についての検討に入りました。
aarch64で起動するECS Fargateへの移行
スポットフリートによるECS on EC2構成への移行も含めて検討しましたが、下記のような観点から、まずはaarch64アーキテクチャのECS Fargateへ移行することに決めました。
aarch64で起動するECS Fargateはx86_64のそれに比べて20%安い
CPUアーキテクチャを変更するだけ*1で20%もの費用が削減できるのは破格でしょう。
aarch64用のdockerイメージを用意するのに少々手間がかかりますが、20%もの費用削減はその手間を大きく超えます。
Rubyはaarch64でも十分にテストされている
新バックエンドが利用しているRubyとRuby on Railsはaarch64でも十分にテストされており、またCPUアーキテクチャによってバグが出がちな、画像などのバイナリを直接操作するライブラリにも依存していないため移行できると判断しました。
一方でバイナリを直接操作するような、特にただでさえバージョン間で差異が大きくバグを生みがちなImageMagickなどに依存している場合には慎重になる必要があると考えており、実際に現行PHPはそのImageMagickを利用しているため、今回は移行対象外としました。
切り戻しが容易
万が一CPUアーキテクチャの違いによるバグが出てもdockerイメージを並行してbuildしていれば、デプロイに使用しているecspressoの設定を変えるだけですぐに切り戻しできることから、リスクは低いと判断しました。
GitHub Actionsを使ってx86_64とaarch64のマルチアーキテクチャなdockerイメージをbuildする
新バックエンドはそのCI/CDとしてGitHub Actionsを使っており、dockerイメージもそこでビルドしています。
具体的にはon: workflow_callで動作するdocker_build.ymlをcicd.ymlからリポジトリ内usesで呼び出すという構成を採ることで、ステージング環境への手動デプロイやmainブランチにマージした後のステージング環境→本番環境デプロイまで、一貫して同じ手順でdocker build、およびAWS ECRリポジトリへのpushを行えるようにしています。
さて、aarch64用のdockerイメージを作ろうとするとQEMUによるCPUエミュレーションで長い時間待つか、aarch64ネイティブの環境でdockerイメージをbuildする必要があります。
GitHub Actionsでは割と最近までlinuxかつaarch64のランナーを起動するにはカスタムランナーの設定を行わなければなりませんでした…が!
なんとつい先月末、linuxかつaarch64のランナーがGitHub Actions標準として利用できるようになりました!!
これにより、aarch64のランナーを起動するためにGitHub Organizationの設定を手動で変える必要がなくなり、より再利用性の高いワークフローを組めるようになりました。 それに加え、GitHubのTeamプランや、GitHub Enterpriseの無料枠に含まれるようになったということでもあります。
x86_64のみをbuildしていたときのdocker_build.ymlワークフローがこちら。
name: new_backend_docker_build_x86_64 on: workflow_call: inputs: aws_iam_role: required: true type: string image_tag: required: true type: string jobs: build: runs-on: ubuntu-latest permissions: id-token: write contents: read steps: - name: configure aws credentials uses: aws-actions/configure-aws-credentials@v4 with: aws-region: ap-northeast-1 role-session-name: new_backend_docker_build.yml-via-github-actions role-to-assume: ${{ inputs.aws_iam_role }} - name: login to ecr id: login-ecr uses: aws-actions/amazon-ecr-login@v2 - name: Checkout uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: build and push docker image uses: docker/build-push-action@v6 with: context: . push: true tags: ${{ steps.login-ecr.outputs.registry }}/new_backend:${{ inputs.image_tag }}
aarch64と並行でbuildするようになったワークフローがこちら。
name: new_backend_docker_build_both_arch on: workflow_call: inputs: aws_iam_role: required: true type: string image_tag: required: true type: string jobs: build_x86_64: runs-on: ubuntu-latest permissions: id-token: write contents: read outputs: image_digest: ${{ steps.build-push.outputs.digest }} steps: - name: configure aws credentials uses: aws-actions/configure-aws-credentials@v4 with: aws-region: ap-northeast-1 role-session-name: new_backend_docker_build.yml-via-github-actions role-to-assume: ${{ inputs.aws_iam_role }} - name: login to ecr id: login-ecr uses: aws-actions/amazon-ecr-login@v2 - name: Checkout uses: actions/checkout@v4 - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: ${{ steps.login-ecr.outputs.registry }}/new_backend - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: build and push docker image uses: docker/build-push-action@v6 id: build-push with: context: . platforms: linux/amd64 labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=${{ steps.login-ecr.outputs.registry }}/new_backend,push-by-digest=true,name-canonical=true,push=true build_arm64: runs-on: ubuntu-24.04-arm permissions: id-token: write contents: read outputs: image_digest: ${{ steps.build-push.outputs.digest }} steps: - name: configure aws credentials uses: aws-actions/configure-aws-credentials@v4 with: aws-region: ap-northeast-1 role-session-name: new_backend_docker_build.yml-via-github-actions role-to-assume: ${{ inputs.aws_iam_role }} - name: login to ecr id: login-ecr uses: aws-actions/amazon-ecr-login@v2 - name: Checkout uses: actions/checkout@v4 - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: ${{ steps.login-ecr.outputs.registry }}/new_backend - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: build and push docker image uses: docker/build-push-action@v6 id: build-push with: context: . platforms: linux/arm64 labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=${{ steps.login-ecr.outputs.registry }}/new_backend,push-by-digest=true,name-canonical=true,push=true build_manifest: runs-on: ubuntu-latest needs: - build_x86_64 - build_arm64 permissions: id-token: write contents: read steps: - name: configure aws credentials uses: aws-actions/configure-aws-credentials@v4 with: aws-region: ap-northeast-1 role-session-name: new_backend_docker_build_manifest.yml-via-github-actions role-to-assume: ${{ inputs.aws_iam_role }} - name: login to ecr id: login-ecr uses: aws-actions/amazon-ecr-login@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Create manifest list and push run: | docker buildx imagetools create \ --tag ${{ steps.login-ecr.outputs.registry }}/new_backend:${{ inputs.image_tag }} \ '${{ steps.login-ecr.outputs.registry }}/new_backend@${{ needs.build_x86_64.outputs.image_digest }}' \ '${{ steps.login-ecr.outputs.registry }}/new_backend@${{ needs.build_arm64.outputs.image_digest }}' - name: Inspect image run: | docker buildx imagetools inspect ${{ steps.login-ecr.outputs.registry }}/new_backend:${{ inputs.image_tag }}
元々は直接イメージタグを付けてpushしていたところを、各CPUアーキテクチャのbuildではSHA256ダイジェスト値でpushし、それらSHA256ダイジェスト値を含むマニフェストを作成してpushすることで同じイメージタグでx86_64とaarch64両方のCPUアーキテクチャに同一のイメージタグで対応できるdockerイメージを構築できるようになりました。
このワークフローを使えば、GitHub Actionsの標準ランナー(ubuntu-latest / ubuntu-24.04-arm)だけで完結し、self-hostedランナーもカスタムランナーも必要ありません。
ecspressoでaarch64のFargateを指定して起動する
前段のマルチアーキテクチャ対応dockerイメージがECRへpushできさえすれば、ecspressoの起動時にどちらのCPUアーキテクチャのFargateを起動するか指定するのはとても簡単で、タスク定義のjsonに下記のようにruntimePlatformを加えるだけです。
{ "family": "nicomanga_{{ must_env `DEPLOY_TARGET_ENV` }}_new_backend", "runtimePlatform": { "operatingSystemFamily": "LINUX", "cpuArchitecture": "{{ env `DEPLOY_CPU_ARCH` `X86_64` }}" }, "cpu": 以降略
あとはDEPLOY_CPU_ARCH環境変数に ARM64 または X86_64を指定すれば、そのCPUアーキテクチャで起動してくれます。
少ない台数から徐々に切り替え、現在では全てのタスクがaarch64で稼働しています。
まとめ
本記事では、ニコニコ漫画新バックエンドのCPUアーキテクチャをx86_64からaarch64に移行して費用を削減した事例を通じて、
- GitHub Actionsでx86_64(AMD64) / aarch64(ARM64)向けのマルチアーキテクチャDockerイメージをビルドする方法
- ecspressoでFargateタスクのCPUアーキテクチャを切り替える方法
についてお話しました。
具体的な金額については伏せますが、単価が20%安い、という事実だけでインパクトが大きいことは十分に感じていただけると思います。
また、このCPUアーキテクチャ移行をするタイミングで偶然にもaarch64で起動するGitHub Actionsの標準ランナーがリリースされたりなどの幸運もあり、非常にシンプルな形で実現できました。
起動するCPUアーキテクチャの切り替えもタスク定義の中のruntimePlatformでcpuArchitectureを指定するだけだったため、非常に簡単かつ切り戻ししやすい形で移行できました。
ドワンゴでは物理・電子・Web連載問わず漫画や本が好き、あるいは長年運用されてきたWebサービスを紐解き、より良い形に作り替えていくことに興味があるWebアプリケーションエンジニアを募集しています。
もし興味がありましたらぜひ、採用情報ページからご応募ください。 カジュアル面談のご相談も歓迎します。
*1:だけ、というにはいささか根本の変更すぎる気もしますが少なくとも表面上の差分としては小さい