カミナシ エンジニアブログ

株式会社カミナシのエンジニアが色々書くブログです

円安を乗り越えるための Arm アーキテクチャへの移行が完了! そのプロセスを公開します

こんにちは。ソフトウェアエンジニアの坂井 (@manabusakai) です。

カミナシでは、クラウドインフラストラクチャに AWS を採用していますが、昨今の円安を受けて円換算での請求額は右肩上がりで増え続けています。サービスの規模や特性に関わらず、パブリッククラウドを利用する多くの日本企業で頭痛の種になっているのではないでしょうか。

円安になる前から継続的にコスト最適化には取り組んできましたが、クイックウィンで実施できるものはやり尽くしており手詰まり感がありました。しかし、我々スタートアップにおいて適正なコストに抑えることはランウェイ(キャッシュ不足に陥るまでの残存期間)を伸ばす意味でも重要なため、現状に甘んじることなく次の最適化ポイントを探していました。

Arm アーキテクチャ移行によるコスト最適化への期待値

AWS は Arm ベースの Graviton プロセッサを開発しており、執筆時点で Graviton4 がリリースされています。 Graviton プロセッサのコストパフォーマンスの良さはよく知られており、x86_64 ベースのインスタンスと比べて 10 〜 20 % ほど低コストです。

カミナシではマルチプロダクトに向けて複数の開発が進んでいますが、新規プロダクトについては原則としてすべて Arm アーキテクチャを採用しています。1

しかし、最初に作られた「カミナシ レポート」については x86_64 アーキテクチャのままでした。そして、このアカウントが全体の中でも最も大きなウエイトを占めています。

このアカウントのコストの内訳を調べてみると、Amazon RDS と Amazon ECS がコストの半分以上を占めています。 RDS と ECS は基本的に常時起動しているため、コストに占める割合が大きいです。

コストの内訳

この部分を Graviton プロセッサに置き換えると月々のコストをグッと押し下げることができるため、今年の 1 月頃から RDS と ECS の Arm アーキテクチャへの移行を始めました。具体的には、Graviton プロセッサを採用したコンピューティングリソースに置き換えること、またアプリケーションを Arm アーキテクチャに対応させることの 2 つを指しています。

専任チームが存在しないカミナシでどうやってインフラの改善を進めているのか?」にも書きましたが、カミナシにはインフラの専任チームは存在しません。そのため、今回も有志で集まったメンバーが協力し合い、プロジェクトチームを編成して進めていきました。

そして先日、すべてのリソースが Graviton プロセッサを採用した Arm アーキテクチャに置き換わりました 🎉

同じような悩みを抱えている方に向けて何かの参考になればと思い、今回はどのように進めてきたのかをご紹介します。

フェーズ 1 : RDS

まずはマネージドサービスである RDS から着手することにしました。

ちょうどリザーブドインスタンスが更新を迎えるタイミングだったのと、インフラの変更作業だけで済ませることができるため、最初のステップにちょうど良さそうだと判断しました。

前提として、今回は T3 を T4g に R5 を R6g に移行します。データベースエンジンは Aurora MySQL を使っていて、read heavy な特性があるため複数の Reader インスタンスが存在しています。2

マネージドサービスの場合、異なる CPU アーキテクチャに変更してもアプリケーションから見たときの挙動は何も変わらないため、まずは開発環境の RDS のインスタンスタイプを T3 から T4g にインプレースで変更しました。

本番環境の Writer インスタンスを変更するには failover が発生するためメンテナンスが必要ですが、その前に Reader インスタンスの 1 台を R6g に入れ替えて 1 週間ほど様子を見ることにしました。こうすることで CPU アーキテクチャによって、パフォーマンス特性に違いがないか確認できます。

我々の環境では R6g に変更したことで、僅差ですがパフォーマンスの改善が見られました。下のグラフはそのときの様子ですが、緑の R5 より赤の R6g のほうがトラフィックが集中したときの負荷が緩やかになっていることがわかります。

R5 と R6g の CPU 使用率の比較

1 週間ほど様子を見て問題がないことを確認したので、メンテナンスを実施して Writer インスタンスもインプレースで変更しました。作業自体は 15 分程度で完了しました。

フェーズ 2 : ECS

次はアプリケーションが実行されている ECS に着手しました。なお、カミナシでは全面的に Fargate を採用しています。

今回移行したサービスは API サーバを中心に、複数のバッチ処理から構成されています。バッチ処理は、ECS をベースにした 2 つのタイプに分けられます。

  • 時刻をトリガーに起動するタイプ: EventBridge スケジューラを使用した ECS タスク
  • キューをトリガーに起動するタイプ: AWS Batch on AWS Fargate

まずはリトライがしやすいバッチ処理から移行してノウハウを貯めることにしました。

また、カミナシではサーバサイドに Go 言語を採用しています。 Go はクロスコンパイルが言語レベルでサポートされているため、アプリケーションコードの修正やモジュールの変更は必要ありませんでした。詳しくは後述しますが、今回も Dockerfile とビルドパイプラインの変更だけで対応できました。

ECS で実行されるタスクを移行する大まかなステップは次のとおりです。

  1. multi-platform image を作るために Dockerfile を修正する
  2. ビルドパイプラインで multi-platform image をビルドし ECR にプッシュする
  3. ECS タスク定義で cpuArchitecture を変更する

それぞれのステップを詳しく見ていきましょう。

multi-platform image を作るために Dockerfile を修正する

まず Dockerfile の修正です。

カミナシでは Docker image のビルドを GitHub Actions で行っています。標準的な runner (e.g. ubuntu-latest) を選んだ場合は、x86_64 アーキテクチャの CPU で実行されます。3

実行環境と異なる CPU アーキテクチャの Docker image を作るには Buildx を使います。

たとえば、docker buildx コマンドで異なるアーキテクチャの Docker image をビルドするには --platform で明示します。カンマで区切れば同時に複数のビルドを行なってくれます。

$ docker buildx build --platform=linux/amd64,linux/arm64 .

ここで問題になるのがビルドの遅さです。実行環境と異なるアーキテクチャは QEMU でエミュレートして実行されるため、通常と比べて数倍の時間がかかります(実測値で 10 倍くらい遅くなったケースもありました)。

そこで BuildKit と multi-stage build を用いて、エミュレートされる部分を減らし高速化します(詳しくは "Faster Multi-Platform Builds: Dockerfile Cross-Compilation Guide" で紹介されています)。

具体的には、BuildKit が自動で設定する BUILDPLATFORMTARGETARCH という ARGS を活用します。

FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS build
WORKDIR /src
COPY . .
ARG TARGETARCH
RUN GOARCH=$TARGETARCH go build -o /out/myapp .

FROM alpine
COPY --from=build /out/myapp /bin

この Dockerfile を GitHub Actions 上でビルドすると、BUILDPLATFORM には linux/amd64TARGETARCH には arm64 がセットされます。

これによって最初のステージは x86_64 アーキテクチャで実行され、Go のクロスコンパイルによって /out/myapp は Arm アーキテクチャのバイナリが作られます(Go のクロスコンパイルには QEMU は使われていません)。

次のステージは BUILDPLATFORM を書いていないのでエミュレートされますが、やっていることは単純なファイルコピーだけなので、ビルド時間にはほぼ影響ありません。

こうすることで、multi-platform image を高速にビルドすることができます。

ビルドパイプラインで multi-platform image をビルドし ECR にプッシュする

GitHub Actions 上で multi-platform image をビルドするのは簡単です。 docker/build-push-action を使っているなら platforms パラメータを追加するだけです。

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.xxxxxx }}
          aws-region: ap-northeast-1

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Setup Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build image and push
        uses: docker/build-push-action@v6
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        with:
          platforms: linux/amd64,linux/arm64
          provenance: false
          push: true
          tags: ${{ env.ECR_REGISTRY }}/xxxxxx:${{ env.IMAGE_TAG }} 

これで amd64 と arm64 の Docker image と Image index がプッシュされます。

Docker image と Image index

コンテナレジストリに ECR を使う場合は provenance: false も追加で指定しておくと良いです。

Buildx v0.10.0 で Provenance attestation という機能が追加され、docker/build-push-action@v4 以降はデフォルトで有効になっています。 Provenance attestation はビルドプロセスに関する詳細な情報を記録するためのもので、この情報は Attestation Storage という領域に保存されています。

しかし、ECR はこの Attestation Storage に対応しておらず、結果この領域が 0.00 MB の Docker image として表示されています。この経緯を知らない人には認知負荷が高いので、ECR が対応するまでは provenance: false を指定して Provenance attestation の情報を記録しないようにしています。

ECS タスク定義で cpuArchitecture を変更する

Dockerfile を修正して ECR に Arm アーキテクチャの Docker image も push できたら、最後にタスク定義を修正するだけです。

ECS で Arm アーキテクチャのタスクを実行する際の考慮事項は "Amazon ECS task definitions for 64-bit ARM workloads" にまとまっています。 Fargate の場合に気にすべき点は次の 2 つです。

  • Fargate プラットフォームバージョンが 1.4.0 以降である
  • Fargate Spot はサポートされていない

この点がクリアになれば、あとはタスク定義に cpuArchitecture を明示するだけです。

{
    "runtimePlatform": {
        "operatingSystemFamily": "LINUX",
        "cpuArchitecture": "ARM64"
    },
}

ECR には multi-platform image として amd64 と arm64 の Docker image が存在しているので、cpuArchitecture に合ったものが pull されます。なお、sidecar がある場合はそれらについても arm64 の Docker image が存在することをあらかじめ確認しておきましょう。

新しいタスクが Arm アーキテクチャで動いているかは AWS CLI で確認できます。

$ aws ecs describe-task-definition --task-definition (family:revision) --query "taskDefinition.runtimePlatform"
{
    "cpuArchitecture": "ARM64",
    "operatingSystemFamily": "LINUX"
}

まとめ

以上のようにして RDS と ECS を Arm アーキテクチャに移行しました。

移行が完了してから数か月が経ちましたが、前後で比較しても主要なメトリクスに変化はなく、Graviton プロセッサのコストパフォーマンスの良さを感じています。

当初の目的であったコスト最適化についても、おおよその想定通り RDS と ECS は 10 % ほどコストを下げることができました。このプロジェクトを始めた当初は 1 ドル 140 円台でしたが、その後は一時 160 円を超える場面もあったので、たとえ 10% の削減でも大きな意味を持ちます。

長い目で見たときにこの改善が大きな差を生むことはわかっていましたが、緊急ではないのでなかなか着手できずにいました。そうした中で、緊急ではないが重要なことをサービス開発チームでやり遂げることができたのは大きな成果でした。

カミナシではノンデスクワーカーの様々な課題を解決するためにマルチプロダクト化を加速させていきます。それに伴い、これまで以上に AWS のコスト管理が難しくなってきますが、専任チームを持たない組織でも最適な形を模索していきたいと思っています!

prtimes.jp

最後に宣伝です 📣 カミナシでは絶賛採用中です! 一緒に強いチームを作っていく仲間を募集しています!

herp.careers


  1. 機械学習などの特定の用途においては x86_64 アーキテクチャを採用しているケースもあります。
  2. 残念ながら、作業時点では Aurora MySQL は R7g に対応していませんでした。
  3. 今年の 7 月に Arm アーキテクチャの runner も発表されました (Arm64 on GitHub Actions: Powering faster, more efficient build systems) が、すべての Actions が動作するかわからないためカミナシでは採用していません。