はてなブックマークのステージング環境を支える技術

id:cohalzです。この記事ははてなエンジニア Advent Calendar 2023 の29日目の記事です。

28日目の記事は id:SlashNephy さんの おうち Kubernetes クラスタ運用記 ~2023~ でした。

はてなブックマークにおけるステージング環境について紹介します。

はてなブックマークでは現在インフラをAWS上に構築しており、ECSやAurora MySQLのサービスを利用しています。

本番環境と同様にステージング環境も用意していますが、より良いステージング環境(例えば本番環境に近く、変更がすぐ試せて、費用が安い構成)にすることを目指し、いくつか工夫した点があるのでそれらを紹介します。

AWSアカウントの分離

はてなでは複数のサービスを運用していますが、はてなブックマーク単体でAWSアカウントを分けて他のサービスとリソースが同居しないようにしています。

その中でもさらに用途別に3つのAWSアカウントを利用しているのでそれらを紹介します。

  • production
  • staging
  • dev

productionおよびstagingではAWS CDKでインフラを管理しています。

ステージング環境はデフォルトブランチに変更があった際にGitHub Actionsで自動でcdk deployが行われるようになっています。

AWS CDKの設定もスタックの実装には環境ごとに場合分けはできるだけせず、envfileのような設定置き場のファイルを作ってそこで差分を吸収するようにしています。

デプロイ時には環境名を入力するプロンプトがあり、間違ったものが入力された場合にはデプロイが止まるような仕組みも入れています。

$ make deploy env=production app=bookmark stacks='*'
Please type environment name to continue:

devのアカウントに関してはIaC管理せず、いつでもまっさらな状態に作り直して良いという運用で検証用に使っています。

こういったアカウント分離をすることでCLIなどでオペレーションをする対象を間違えてしまうみたいなことを防ぎやすい効果があります。

とはいえ後述する本番環境のデータを参照する仕組みをするなど権限周りの設定が難しいことがあるのでそこだけ注意が必要です。

ステージング環境は本番環境のスナップショット

はてなブックマークではデータストア層にElastiCache for Redis/Aurora MySQL/OpenSearchを利用しています。

ステージング環境においてこれらのデータストアは全てある時点での本番環境のスナップショットとなるように構築しています。

具体的には毎日の朝に本番環境のデータをコピーしてステージング環境のデータストアを作り直すという処理がStep FunctionsとEventBridgeで自動で動くように構築しています。

これによりダミーデータを用意する必要がなくなったという利点のほか、不具合の修正に関しても本番環境とデータがほぼ同じことでステージング環境で再現させることや変更の動作確認がしやすいメリットがあります。

また細かいメリットとしてステージング環境を気軽に壊せるようになるというのがありました。

開発の際にステージング環境に対して間違った設定の変更をしてしまったり、変なデータを入れてしまったりしたとしても翌日には本番同等の設定とデータで作り直されます。

そのため、ステージング環境に対して本番環境との差分があるかもしれないといった意識をほとんどしなくて済むようになりました。

また、今年はAurora MySQLをv2からv3に移行する作業をいくつか行いましたが、その際にもこの本番のデータを使う仕組みが役に立ちました。

ステージング環境を作り直す際にアップグレードする処理を追加することで、本番はv2でステージング環境はv3として本番環境のデータを元に動作確認も簡単にできるようになりました。

しかもそのアップグレードする処理は毎日実行しているため、いざ本番をアップグレードする際の手順で失敗してしまったなんてことが起こりにくくなりました。

前述の通りproductionとstagingでAWSアカウントが分かれているのでアカウントを跨いでデータをコピーできるようにするにはいくつか追加の手順が必要なのに注意してください。

  • Aurora MySQLはResource Access Managerを使いステージング環境から参照できるようにする
  • ElastiCache for Redisは自動バックアップをS3にアップロードする
  • OpenSearchはLambdaで定期的に手動スナップショットを取得してS3にアップロードする

こういった作り直す実装はStep FunctionsとそのAWS SDK統合を利用して作成しています。下の図はAuroraのクラスタを作り直して必要に応じてアップグレードを行うStep Functionsのワークフローです。

Auroraクラスタを作り直すStep Functions

こういったコピーする実装は元々Lambdaで作成していましたが、Step Funcstionsでは時間制限もなくステップ数による課金なのでこういった処理の費用をより安く作成できます。

上の図で言うとAuroraクラスタを削除するリクエストを実行した後に作成できるまでに数分待つといった処理をしていますが、場合によっては数分待ってもまだ実行できないことがあります。

その際に組み込みのリトライ処理を使ってExponential Backoffで待ち時間を増やすといったこともやっています。

Step Functionsで実装するメリットとして他には処理の流れや失敗した部分の確認もコンソールから確認しやすいというのもメリットの一つかなと思います。

またLambdaではどの言語で書いたとしても将来的にランタイムの更新を考える必要が出てきます。そういったものの管理が不要になるのもメリットです。

夜間休日にステージング環境を止める

はてなブックマークでは複数のマイクロサービスが存在しており、ステージング環境でも同様に各サービスを動かしています。 スペックは必要に応じて下げているものもありますが、サービスの数だけECSサービスやデータストアが必要になってくるため、それなりに費用がかかっていました。

ステージング環境は基本的に開発をする日中だけ動かしておければいいので、それ以外のタイミングでは止めるといった仕組みをStep FunctionsやGitHub Actionsを使って実現しました。

例えば定時後にAuroraクラスタを全て削除するStep Functionsはこのようになっています。

Auroraクラスタを全て削除するStep Functions

Step FunctionsでDescribe*のAPIを実行し、そこで帰ってきた複数の結果をMapを使って順番に削除するといった処理になっています。

AWSアカウントが分かれていることで、AWSアカウント全てのデータストアを止めるみたいな単純な実装で済むというのもありがたいポイントでした。もしアカウントが分かれていない場合はタグなどでフィルターする感じになると思います。

停止する処理を追加したところで今度は起動する処理を追加する必要がありますが、こちらはブックマークチームでは必要ありませんでした。

というのも先に紹介した通り、ステージング環境にデータストアをコピーする実装が既に動いていたためです。

データストアに関してはこれでやりたいことを実現できたので今度はECSのことを考えます。

はてなブックマークではECSは全てFargateで動かしておりEC2はありません。そのため起動しているタスクを全て0台にする処理を実行するだけでよい状態でした。

それを実現するためにGitHub Actionsで定期的に0台にするのと元の台数にする仕組みを入れるようにしました。

こちらも他に合わせてStep Functionsで実行しても良かったですが、はてなブックマークではecspressoをGitHub Actionsから利用する仕組みが既に整っていたのと、管理しているECSを落とす仕組みは社内でも特に需要があったので真似しやすい形にしたというのが理由です。

実装に関しては非常に簡単で ecspresso scale --tasks=0 をステージング環境の全ECSサービスに対して実行するだけで実現できます。

営業時間になって動かしたくなった場合は、現在の設定ファイルを元にただecspresso deploy --skip-task-definition を実行することで本来設定していた台数に自動で戻るようになっています。

ちなみにFargate Spotも利用しているため、Fargateをそのまま24時間動かすのに比べて0.3(Fargate Spot) * 0.25(起動時間) = 7.5%の費用で済むようになっています。

ECSのデプロイ

ステージング環境の運用を軽く紹介したところで、ECSのデプロイについても軽く紹介します。

はてなブックマークのリポジトリ構成はこのようになっています。

  • 各アプリケーションのリポジトリ(App Repo)
  • CDKのリポジトリ(CDK Repo)
  • ecspressoのリポジトリ(Release Repo)

各アプリケーションのリポジトリmainブランチに変更をマージするとrepository dispatch経由でイメージのタグが更新される仕組みになっており、ステージング環境に自動で最新の変更がデプロイされるようになっています。

こちらも各サービスでデプロイ方法が統一されており、図にするとこんな感じです。

Release Repo(ecspressoのリポジトリ)のディレクトリ構成はこんな感じになっています。

core
└── main
    ├── base
    │   ├── container
    │   │   └── app.libsonnet
    │   ├── service-def.libsonnet
    │   └── task-def.libsonnet
    ├── production
    │   ├── deploy-config.json
    │   ├── ecspresso.jsonnet
    │   ├── image
    │   │   └── app.libsonnet
    │   ├── service-def.jsonnet
    │   └── task-def.jsonnet
    └── staging
        ├── deploy-config.json
        ├── ecspresso.jsonnet
        ├── image
        │   └── app.libsonnet
        ├── service-def.jsonnet
        └── task-def.jsonnet

ECSのクラスタ名/ECSのサービス名/環境名 という階層にしています。この例ではproductionとstagingのAWSアカウントにcoreというECSクラスタがあり、その中にmainというサービスがあるという感じです。

baseでは環境で変わらないような設定(例えば platformFamilynetworkMode など)を記述して、各環境のディレクトリでは環境ごとに変わる設定のみを記述するようにしています。

deploy-config.json というファイルを独自に追加しています。これはGitHub Actionsから読むファイルで、デプロイ時にMackerelにアノテーションするサービス名だったり、デプロイの依存関係(AをデプロイしたらBもデプロイする)などの設定をJSON形式で記述し処理を実行しています。

GitHub Actionsで deploy-config.json を見てMackerelのサービスにアノテーションする設定の例はこんな感じです。

- id: get-mackerel-service
  if: ${{ hashFiles(format('{0}/deploy-config.json', github.event.client_payload.environment)) != '' }}
  run: |
    echo "service=$(jq -r '.mackerel.service // empty' ${{ github.workspace }}/${{ github.event.client_payload.environment }}/deploy-config.json)" >> "$GITHUB_OUTPUT"

- name: Create Graph Annotation to ${{ steps.get-mackerel-service.outputs.service }}
  uses: cohalz/post-mackerel-annotation@v1
  if: ${{ steps.get-mackerel-service.outputs.service != '' }}
  with:
    api-key: ${{ secrets.MACKEREL_APIKEY }}
    title: deploy ${{ github.event.client_payload.environment }}
    description: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
    service: ${{ steps.get-mackerel-service.outputs.service }}

ちなみにMackerelのサービス名に関しても本番環境とステージング環境で分けており、ステージング環境においてもメトリックの変化をアノテーションと合わせて確認できるようになっています。

ステージング環境でMackerelの様子を見ることは実際はあまりありませんが、こういったアプリケーション以外の環境も分かれている事によってCI/CD自体の設定を変更した際に安全に動作確認ができるようにしているというメリットがあります。

ちなみに設定を変更するプルリクエストを作成した場合は影響のあるECSサービスをうまく列挙し、それに対し ecspresso diffおよび ecspresso verify を行うようになっています。

ステージング環境だけ変更した時のCI

これによってステージング環境だけを変更したいのに本番環境が変更されてしまった、みたいな事故を防ぎやすくなっています。

おわり

はてなブックマークのステージング環境を中心にインフラを紹介しました。ステージング環境をより良く運用することで本番環境を安心して変更できるようになると思っているので参考にしてもらえると嬉しいです。

明日の記事は id:stefafafan さんの担当です。