はじめに
こんにちは。雑食系エンジニアの勝又です。
今回は、私が2年ほど参画させていただいた大規模サービスのインフラやDevOps周りを全面的にリプレイスしたお話について簡単にご紹介させていただきます。(内容に関しては事前に参画先企業様に確認していただいております)
サービス概要
詳細な内容は伏せますが、メインとなるテーブルのレコード数が数十億件、スパイク時には数万〜数十万のユーザーが一斉にアクセスする大規模サービスです。
技術的負債
長く運用されてきたサービスのあるあるですが、新機能の追加が最優先されてきたことにより、こちらのサービスにも下記のような技術的負債が大量に積み上がっていました。
- RubyやRailsやMySQLのバージョンがかなり古い
- インフラの構成がコードではなくドキュメントで管理されている
- アプリケーションの構成管理がおこなわれていない
- CI/CDパイプラインが構築されていない
- デプロイ手順が複雑で「デプロイ職人」的な存在が必要になってしまう
- DBのマイグレーションが手動で実行されている
- マイグレーションファイルの一部が紛失している
- 事前準備が大変なため本番環境へのリリースが月1回しかおこなえない
- アプリケーションのスケールアウト/スケールインが手動で実施されている
- AWSアカウントが開発用/検証用/本番用など適切に分割されていない
- モニタリングやログ収集の仕組みが整備されていない
こういった技術的負債を放置していると「開発効率やリードタイムの低下」「オペレーションミスの増加」「技術やノウハウの属人化」「不具合発生率や障害発生率の増加」「セキュリティリスクの増加」などの問題が深刻化していきます。
さらには開発者体験(DevEX)の悪化により優秀なエンジニアの定着率の低下を招き、リクルーティングにも大きな悪影響が発生します。優秀なエンジニアを採用できなくなると「サービスを安定運用しながら多種多様で複雑な機能要件や非機能要件を迅速に実現してお客様に高い価値を提供し続ける」ことができなくなり、サービスの競争力が失われてしまいます。
インフラ/DevOps/セキュリティ周りの改善
先述のような技術的負債に対して、私が参画した2年ほど前から本格的な返済作業に着手しました。今回はその中で取り組んできたインフラ/DevOps/セキュリティ周りの改善に関してざっとご紹介してみたいと思います。
ECS(Fargate)の導入
リプレイス前の当サービスはEC2上で動作していましたが、本体のアプリケーションも非同期処理もバッチ処理も全てFargateに移行しました。
コンテナの方が全ての面でVMよりも優れているというわけではありませんが「ローカル環境や本番環境の差異/開発者間の環境の差異を吸収してくれる」「構成管理が容易になる」「デプロイやロールバックやオートスケールが容易になる」といったメリットを考慮すると、ある程度以上の規模のサービス開発においては大抵の場合はコンテナを使用するメリットの方が大きくなります。
さらにFargateの場合は基盤となるEC2インスタンスの構成管理も不要となりますので、保守の手間が大幅に削減できるというメリットもあります。
それ以外にCodeDeployのBLUE/GREENデプロイ機能を活用したデプロイ安全性の向上、SOCIによる起動時間の短縮、Fargate SPOTの活用やスケジュール化されたスケールイン/スケールアウトによるコスト削減等の改善も導入済みです。
保守作業の際に必要となる管理用のコンテナもFargateで管理しており、専用のスクリプトで起動&ECS Execでログインして作業が完了した後は一定時間が経過すると自動的に終了するような機能を組み込んでいます。
Aurora MySQL バージョン3(MySQL8.0)への移行
データベースはRDS for MySQL 5.7からAurora MySQL バージョン3(MySQL8.0系)へ移行しました。
標準的なMySQLと比較した場合、Aurora MySQLは「スループットの向上」「ストレージのオートスケール」「可用性と耐久性の向上」「フェイルオーバー時間の短縮」といったメリットがあります。一定規模以上のサービスで使用するAWSのRDBとしてはAuroraがベターな選択肢と考えて差し支えないと思います。
前述したように「一部のマイグレーションファイルの紛失」という問題があったり、移行の準備でAWSアカウントをまたがるレプリケーション設定が必要だったりとそれなりの手間を要しましたが、幸いなことに5.7でDeprecatedになるような特殊な機能を使っている箇所は無かったため、比較的スムーズに移行が完了しました。
Auroraへの移行後はさらにProvisioned AuroraからAurora Serverless V2への移行も実施済みです。
Terraformによるインフラのコード化
当サービスではFargate、Lambda、Aurora、ElastiCache、S3、CloudFront、CloudWatch、Route53、ALB、WAFなど多数のAWSサービスを使用していますが、移行の際にこれらは全てTerraformを使ってコード化しました。
インフラをコードで管理することで「手順書の維持管理が不要になる」「複数環境の構築が容易になる」「GitHub上で変更をレビューできる」「オペレーションミスのリスクや属人性が減少する」等の大きなメリットを得ることができます。
コード化の際にはFargateやAuroraなど適切なユニット分けをした上で、専用のスクリプトを作ってユニット単位でplan/apply/destroyを実行できるようにしています。
Terraformの悩みどころである複数環境管理に関してはworkspaceやmoduleではなくシェルスクリプトの定数ファイルを使用したプレースホルダ置換で対応しています。「重複コードが最小限になる」「AWS CLIなど他のツールとTerraformを併用する場合に設定値を共通化できる」「sedコマンドがインストールされていればどこでも使える」等のメリットがあり、メンテナンスが容易で環境が新しく追加される際の作業も最低限で済むため、モノリポで複数ツールが混在するケースにおいては総合的にベターな手法かなと考えています。
CI/CDパイプラインの導入
CI/CDパイプラインはGitHub Actionsで構築しました。
フロントエンド/バックエンド/インフラそれぞれに対してワークフローを作成し、実行時間の長いバックエンドのRSpecのテストはマトリックス戦略とparallel_testsにより多重並列で実行しています。split-testとRSpecの実行時間ログを使用したテスト分割の最適化にも対応しています。
CDに関しては環境ごとに「dev」「stg」「prod」等のリリースブランチを用意し、それらのブランチに対してプルリクのマージやpushが実行された際にデプロイが実行されるような方式を採用しています。これによりそのブランチに対して権限を付与されているエンジニアであればGitHubのUI上で簡単にデプロイが実行できるようになったため、セキュリティを担保しながら「属人化の防止」「開発効率アップ」「リリース手順の大幅な簡易化」「リリース頻度の向上」を実現することができました。
また、AWSアカウントに対する権限を付与されているエンジニアであればCIを経由せずにローカル環境から専用のスクリプトを使って各環境へのデプロイが実行可能な方式にしています(ローカル環境とCI環境で同じスクリプトを共用しています)。Terraformの各ユニットのplanやapply、コンテナイメージのビルドとプッシュ、複数のFargateアプリケーションのデプロイ、データベースのマイグレーション等がそれぞれ個別に実行できるため、作業時に必要なデプロイ処理だけを効率よく実行することができます。さらにFargateアプリケーションのデプロイに関しては独自の工夫により並列化しています。
CI/CDを実行する際には専用のカスタムイメージを使用しています。デプロイ後などに環境依存の問題が顕在化する可能性を極小化するために、このCI用のイメージはアプリケーション用のイメージと同じBaseイメージから生成しています。(imagemagick等の共通で使用する必須のライブラリはBaseイメージでインストールして、CI用イメージやアプリケーション用イメージはそれぞれの用途で必要なパッケージだけをインストールする方式にしています)
(補足:Baseイメージを共用する方式においては、何も対策をしないとそのBaseイメージは「グローバル変数」のような存在になってしまうということに注意が必要です。つまり何らかの不具合がBaseイメージに含まれている状態でビルドを実施してそれがコンテナレジストリに同じバージョンタグでアップロードされてしまうと、そのバージョンのBaseイメージを参照している子イメージ全てが次回のビルド時に影響を受けることになります。そのためBaseイメージを同じバージョンタグで更新することを許可しない仕組みを導入して「グローバル変数」ではなく「グローバル定数」として扱えるようにする必要があります。当サービスではBaseイメージのビルド&プッシュ用のスクリプトにその制御用のコードを実装して対応しています)
データベースマイグレーションの自動化
移行前に手動で実行していたデータベースのマイグレーションはスクリプトとLambdaを使ってコマンドで実行できるようにした上で、CI/CDパイプラインに組み込みました。
DBマイグレーションに関してはFargateタスクを使用する方法もありますが、LambdaはFargateタスクと同じく「常時起動しておく必要がないためコスパが良い」「アプリケーション本体と同じコンテナを使用できるため新しくコードを書く必要がない」「VPC内のリソースにアクセスできる(VPC Lambdaの場合)」という利点があることに加えて「起動が高速」という大きなメリットがあります。最長実行時間も15分あるので一般的なマイグレーションに関しては問題ありません。
重たいALTER TABLE系の処理だと15分で終わらない可能性がありますが、そういったDDLは「手動で適用 => マイグレーション管理用テーブルにレコードを追加してマイグレーションが1つ進んだように偽装する => マイグレーションファイルを追加」というフローが必要なのでそもそも自動化の対象にはなりません。よって自動化の用途においてはLambdaで十分であると判断しています。
AWS Control Towerの導入
移行前からAWS Organizationsは使用していたのですが、AWSのマルチアカウント管理の効率化のためにAWS Control Towerを導入しました。
AWS Control Towerを有効化すると、新規に作成されるAWSアカウントではCloudTrailとAWS Configが自動的に有効化されて、証跡やログがLog ArchiveアカウントのS3バケットに自動的に集約されるようになります。こういったセキュリティ周りの設定が自動化されるだけでも大きなメリットがあります。リージョン拒否設定により未使用リージョンのサービスへのアクセスを拒否することもできます。
まだ導入段階のため十分に使いこなせているとは言えない状態ですが、AWSアカウントが新規に追加された際のイベントを検知して自動的に実行されるLambdaを作成して、アカウントのSecurity Hubを有効化したりOrganizationのSecurity Hubのメンバーに追加する等の最低限の自動化処理をおこなっています。
今後はガードレールの整備を進めたりSecurity HubとSlackを連携する等して、セキュリティリスクの事前検知や早期発見に役立てていく予定です。
IAM Identity Center(旧SSO)の導入とコード化
インフラ移行後は各AWSアカウント上にIAM Userは極力作成せず、IAM Identity Center(旧SSO)で統一的な権限管理をおこなう方式を採用しています。またIAM Identity Centerのユーザー、グループ、許可セットはTerraformでコード化して管理しています。
これにより、社内の誰がどのAWSアカウントに対してどのような権限を保持しているかが容易に把握できるようになりました。
またAWSの「最小権限の原則」に可能な限り準拠して、前述した「管理用の一時コンテナへのアクセス」等に関しても専用の許可セットとグループを作成して権限を制御しています。
サードパーティサービスとの連携など、どうしても必要な場合があるためIAM Userを完全に廃止することは中々難しいというのが現実です。しかしIAM Identity Centerによる統一管理を導入したことにより、IAMアクセスキーの漏出というAWSで最も発生頻度の高いセキュリティリスクを最小限に抑えることができるようになりました。
まとめ
これ以外にもフロントエンドやバックエンドで大小様々な課題への取り組みを現在も実施中ですが、今回は私が関与した改善を中心に紹介させていただきました。
インフラ周りやDevOps周りに限りませんが、何かの課題を解決するための技術やツールや手法の選択に関して「絶対の正解」は存在しません。複数の打ち手を考案して十分に調査した上でメリットとデメリットを総合的に比較して意思決定していく必要があります。
私の場合、各種の技術やツールや手法の選定に関しては
- 保守性
- 一般性
- 汎用性
- 性能
- 学習コスト
- セキュリティ
- 健康寿命
といった辺りを評価軸として重視しておりますが、今回のリプレイス作業においてはかなり妥当な判断ができたのではないかと思います。
世の中は不確実でありIT業界の技術の進歩は非常に速いため、1年後にはこの記事で紹介したものとはまた別の技術やツールがスタンダードになる可能性はあります。そういう場合でも結果論で考えずに「あの時点で入手可能だった情報から総合的に判断するならあれがベターな選択肢だった」と思えるような後悔の少ない意思決定を継続していくことが重要だと考えております。