はじめに
こんにちは、IT 本部 IT 基盤部第一グループの横田です。 IT 基盤部では、組織横断的に複数のプロダクトのインフラ運用を行っており、インフラ基盤の安定稼働やコスト削減に取り組んでいます。
私たちがインフラ運用を担当する Pococha では、元々 Amazon EC2 (以下、EC2)上で Ruby on Rails アプリケーションの個別開発環境を運用していました。 本記事では、その開発環境を Amazon EKS (以下、EKS)上で新たに構築し、開発のユースケースに合った運用しやすいコンテナベースの開発環境を整備した取り組みについて紹介します。
特に、 過去の記事 で紹介した GitOps ベースのデプロイフローを土台にしながら、開発のユースケースに応じて開発環境を 2 種類に分け、それぞれをどのように実現したかを説明します。
解決したかった課題
従来の EC2 ベースの個別開発環境には、いくつかの課題がありました。
- アプリケーションサーバーやジョブワーカーのメモリ使用量が増えると、環境自体が不安定になりやすい
- 開発者ごとに 1 台ずつ環境を持つため、環境数に比例してコストが増えやすい
- レビューや情報共有のために、変更内容に対応した検証環境を用意しにくい
そこで、EKS 上に日常的な実装や動作確認に使う環境と、レビューや共有のために使う環境を用意し、用途に応じて使い分けられるようにしました。
- ブランチ環境:特定の Git ブランチに push されたコードを自動でデプロイ
- Pull Request 検証環境:特定のラベルを付与した Pull Request に紐付く最新のコードを自動でデプロイ
本記事のポイント
- EKS 上で、開発のユースケースに応じて使い分け可能なコンテナベースの開発環境を整備し、動作検証のしやすさを向上
- 実行基盤を Spot インスタンスに切り替え、さらに複数環境の Pod を同一 Node に集約することで、インフラコストを削減
なぜ開発環境を見直したのか
元々、開発者ごとの個別開発環境として EC2 インスタンスを 1 人 1 台ずつ利用していました。 しかし、Ruby on Rails のアプリケーションは比較的メモリ使用量が大きく、開発段階では最適化されていない処理によって、アプリケーションサーバーやジョブワーカーのプロセスが急激に太ることがあります。Pococha でも、 Unicorn や Sidekiq のワーカーが肥大化し、一部の環境でメモリを使い切って API へのアクセスや SSH 接続ができなくなることがありました。
この構成では、開発者が実装中の変更をすぐに検証したくても、まず環境自体が健全に動いていることを確認しなければならず、環境の状態に作業が左右されやすいという問題がありました。 また、開発者ごとに EC2 インスタンスを持つ構成は分かりやすい一方で、環境の数に比例してコストが増えやすく、環境の安定性とコストの両面で見直しが必要な状態でした。
新しい開発環境の設計
Pococha の Kubernetes ベースのデプロイフローについては、過去の記事で EKS と Argo CD を用いた GitOps の仕組みを紹介しました。
今回、新たに設計した開発環境では、その考え方をベースにしつつ、開発のユースケースに合わせてデプロイの起点を 2 種類に分けました。
- ブランチ環境:特定の Git ブランチへの push を起点に更新される、継続利用向けの開発環境
- Pull Request 検証環境:特定のラベルが付与された Pull Request の最新コミットを起点に更新される、レビューや共有向けの検証環境
元々、既存の EC2 環境では、開発者がブランチを指定してデプロイを実行し、動作確認を行う運用をしていました。 そこで、従来の運用に近い形で、特定のブランチに push されたコードを自動でデプロイする環境を用意しました。 この従来の運用に近い開発環境を、本記事では「ブランチ環境」と呼びます。
設計する上で重要だったのは、単にコンテナ化して EKS 上に構築することではなく、開発者が普段行っている操作と、環境の作成・更新・削除を自然に結び付けることでした。
日常的な開発では、あらかじめ対応付けたブランチへの push を起点に環境が更新される方が扱いやすく、実装中の変更を継続的に確認しやすくなります。一方で、コードレビューや情報共有の文脈では、Pull Request 単位で環境が存在した方が、関係者に同じ環境を共有しやすく、「どの変更を確認しているのか」を揃えやすくなります。
例えば、ブランチ環境は developer1.xxxx.local や feature1.xxxx.local のように、開発者や作業ブランチに対応付けた環境として継続的に使うイメージです。一方、Pull Request 検証環境は pr-123.xxxx.local のように Pull Request 単位で用意される環境をイメージすると、レビュワーや関係者が同じ変更を確認しやすくなります。
そこで、ブランチ環境は特定のブランチに対応付けた継続利用向けの環境とし、Pull Request 検証環境は Pull Request の最新コミットに対応付けた共有しやすい検証環境とすることで、同じ GitOps の枠組みを使いながら、異なるユースケースに対応できるようにしました。
ブランチ環境のデプロイフロー
ブランチ環境は、特定の Git ブランチに push されたコードを自動でデプロイする開発環境です。 開発者は、普段通りブランチにコードを push するだけで、自動的に変更を反映できます。
この仕組みの狙いは、従来の「自分の環境に変更を反映して動作確認する」という開発体験を保ちながら、実行基盤を EC2 から EKS に置き換えることでした。 Kubernetes を利用することで、ヘルスチェックや Pod のライフサイクル管理を自動化できるようになり、問題が発生した場合も Pod の再作成によって復旧しやすい構成にできました。 また、複数環境の Pod を同一 Node に集約することができるため、インフラコストを削減することができました。
ブランチ環境のデプロイフロー
図の流れを概略化すると、次のようになります。 緑色の番号は開発チーム、紫色はインフラチームが管理・運用を担当します。また、デプロイ完了後の API アクセスを赤色で表しています。 図中のコンポーネントに関する説明は 過去の記事 をご覧ください。
| 各ステップの処理 | |
|---|---|
| (1) | 開発者が特定のブランチにコードを push します。 |
| (2) | GitHub Actions が特定のブランチの変更を検知し、自動的にワークフローを実行します。 |
| (3) | GitHub Actions がコンテナイメージを build します。 |
| (4) | GitHub Actions が build したコンテナイメージを Amazon ECR (以下、ECR)に push します。 |
| (5) | Argo CD Image Updater が ECR 上に新しいイメージが push されたことを自動的に検知します。 |
| (6) | Argo CD Image Updater がコンテナイメージの情報を環境ごとの Helm Values ファイル に書き込み、GitHub 上に push します。 |
| (7) | Argo CD が Helm Values ファイルの変更を自動的に検知します。 |
| (8) | Argo CD がアプリケーション Pod や Istio の VirtualService などの Kubernetes リソースを更新します。 |
| (9-1) | アプリケーション Pod を起動するため、ECR からコンテナイメージが pull されます。 |
| (9-2) | ExternalDNS が VirtualService リソースのアノテーションをもとに Route 53 の DNS レコードを作成します。 |
| (9-3) | Istio の Gateway や VirtualService リソースの設定をもとに Envoy (istio-proxy) のルーティングが動的に設定されます。 |
| (10) | クライアントから (9-2) で作成された DNS レコードに対して名前解決を行います。 |
| (11) | クライアントから AWS の Application Load Balancer (以下、ALB)に対して HTTPS でアクセスします。 |
| (12) | ALB がいずれかの Istio Ingress Gateway の Pod に対して HTTP でアクセスします。 |
| (13) | Istio Ingress Gateway の Pod がリクエストの Host ヘッダーに基づき、対応する環境のアプリケーション Pod に HTTP でアクセスします。 |
ブランチ環境のポイントは、「あらかじめ決められたブランチに紐付く継続利用向けの環境」であることです。 ブランチ環境を利用することで、開発者は日々の実装や動作確認を継続的に行いやすく、Pull Request を作る前の段階でも動作確認を行うことができます。
ステップ (9-2) と (9-3) では、アクセス経路の自動構成を行っています。 次に、その具体的な設定方法についてコードを交えて詳しく見ていきます。
DNS レコード作成とルーティングの動的設定
このデプロイフローでは、アプリケーション Pod の更新だけでなく、各環境へのアクセス経路も Kubernetes リソースの変更に追従して自動的に更新されます。 その中心となるのが、Istio の VirtualService リソースです。 VirtualService リソースの設定例を以下に示します。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
annotations:
external-dns.alpha.kubernetes.io/target: example-alb.ap-northeast-1.elb.amazonaws.com
external-dns.alpha.kubernetes.io/ttl: "300"
name: example-app
namespace: app
spec:
gateways:
- istio-system/example-gateway
hosts:
- example-app.dev.example.com
http:
- match:
- port: 8080
route:
- destination:
host: example-app
port:
number: 8080
この設定のうち、まず metadata.annotations の external-dns.alpha.kubernetes.io/target は、作成する DNS レコードの値(向き先)を示しています。
ExternalDNS
はこのアノテーションを監視し、spec.hosts に記述された各ホスト名に対応する DNS レコードを Route 53 に自動作成・更新します。
また、external-dns.alpha.kubernetes.io/ttl によって DNS レコードの TTL もあわせて制御できます。
spec.gateways と spec.hosts は、「どの Gateway で、どのホスト名へのリクエストを受け付けるか」を定義しています。
さらに、spec.http.route.destination には、受信したリクエストの転送先となる Kubernetes Service を記述します。
Istio は Gateway と VirtualService リソースの設定をもとに、
Envoy
(istio-proxy) のルーティング設定を動的に更新します。
そのため、新しい環境が作成された場合でも、個別にプロキシ設定を手動変更することなく、対応するホスト名から対象環境のアプリケーション Pod へリクエストを転送できます。
この仕組みによって、環境の追加・更新・削除に伴う DNS レコードとルーティング設定の変更を、Kubernetes リソースの管理に統合できます。 次に説明する Pull Request 検証環境のデプロイフローでは、環境の作成・更新・削除がブランチ環境よりも頻繁に発生するため、この自動化が環境のライフサイクル全体をシンプルに保つ上で重要となっています。
Pull Request 検証環境のデプロイフロー
ブランチ環境だけでも日常的な開発には対応できますが、コードレビューや情報共有の文脈では、Pull Request 単位で独立した環境が簡単に用意できると便利です。 そこで追加したのが Pull Request 検証環境です。
Pull Request 検証環境では、特定のラベルが付与された Pull Request に紐付く最新コミットからコンテナイメージを自動的にビルドし、個別の検証環境としてデプロイします。
Pull Request 検証環境のデプロイフロー
図の流れを概略化すると、次のようになります。
| 各ステップの処理 | |
|---|---|
| (1) | 開発者が対象となる Pull Request に特定のラベルを付与、もしくはその Pull Request の最新コミットを更新します。 |
| (2) | GitHub Actions が Pull Request への特定ラベルの付与、もしくはその Pull Request に紐付く最新コミットの変更を検知し、自動的にワークフローを実行します。 |
| (3) | GitHub Actions がコンテナイメージを build します。 |
| (4) | GitHub Actions が build したコンテナイメージを ECR に push します。 |
| 【以下、ブランチ環境と同様】 |
ラベルをトリガーにした理由は、全ての Pull Request に対して自動的に環境を作るのではなく、必要な場合のみ明示的に生成したかったためです。 これにより、運用する環境数を制御しやすくなり、コストや管理負荷を抑えながら個別の検証環境を提供できます。
Pull Request に追加で commit が push された場合は、その時点の最新コミットを継続的に反映できます。 そのため、レビュワーは常に最新の状態を確認でき、レビュー対象と環境の対応関係も明確になります。
また、Pull Request に付与されたデプロイ用のラベルが外されたり、Pull Request がクローズされた場合は、自動的に環境が削除されます。
ブランチ環境と Pull Request 検証環境をどう使い分けるか
2 種類の環境は、どちらも GitHub 上のイベントを起点にコンテナイメージをビルドし、Argo CD で同期するという点では共通しています。 しかし、環境をどの単位で持つか、どのタイミングで作成・更新するか、誰と共有するかという点で役割を分けています。
ブランチ環境は、特定のブランチに対応した継続利用向けの環境です。 元々の EC2 環境と同様に、開発者が指定したブランチを反映して継続的に動作確認するための環境であり、日々の実装や試行錯誤を進める際の基本となる環境です。 開発者は普段通りブランチへコードを push するだけで変更を反映できるため、従来の開発フローから大きく変えずに利用できます。
一方、Pull Request 検証環境は、Pull Request の最新コミットに対応した検証環境です。 特定のラベルが付与された Pull Request を起点に作成され、Pull Request の最新コミットに追従して環境も更新されます。 そのため、レビュー対象の変更だけを切り出して確認しやすく、レビュワーや関係者との間で「どの変更を見ているのか」を揃えやすくなります。
ブランチ環境は継続的に使うことを前提とした環境であるのに対し、Pull Request 検証環境はレビューや情報共有のため必要になったときに作成する環境です。 このように、前者は普段の開発における継続的な動作確認、後者はレビュー単位での確認や共有、という役割で使い分けています。
導入効果
開発体験の改善
開発体験の観点で最も大きかったのは、個別開発環境を EC2 インスタンスそのものとして持つのではなく、Kubernetes リソースとして扱えるようになったことです。 これにより、一部の環境で Ruby on Rails のワーカーが肥大化してメモリを使い切った場合でも、その影響が同じインスタンス上の他のアプリケーションやインスタンス全体の不安定化に波及しにくくなりました。 また、Kubernetes のヘルスチェックや Pod のライフサイクル管理によって、問題が発生した場合も復旧しやすい構成にできたため、環境自体の安定性を高めることができました。
加えて、開発のユースケースに応じてブランチ環境と Pull Request 検証環境を使い分けられるようになったことも大きな改善でした。 開発者は、実装中の変更を継続的に確認したい場合はブランチ環境を使い、レビューや情報共有のために変更を切り出して確認したい場合は Pull Request 検証環境を使う、という形で目的に応じて環境を選択できます。
さらに、環境の更新やアクセス経路の構成が GitHub 上の操作と Kubernetes リソースの管理に結び付いたことで、環境ごとに個別の作業を意識する場面が減りました。 その結果、開発者は「どの環境で何を確認すべきか」を理解しやすくなり、開発の流れに沿って自然に動作検証を進められるようになりました。
インフラコストの削減
インフラコストを評価するにあたっては、既存環境と新しい環境の運用条件およびリソース利用効率の差分を整理する必要があります。 既存の EC2 環境でも、既に毎日 22:00 から翌 09:00 までと、土日はインスタンスを自動停止する運用を行っていました。 つまり、その状態を前提に新しい環境との違いを考える必要があります。
この運用を前提にすると、1 週間 168 時間のうち稼働しているのは平日 5 日分の 13 時間、すなわち 65 時間です。
したがって、旧環境は常時稼働と比較して 1 - 65 / 168 = 約 61.3 % のコスト削減ができていたことになります。
新しい環境でも、この自動停止の運用自体は維持しています。 そのうえで、コスト削減に効いている要素は大きく 2 つあります。
1 つ目は、実行基盤を On-demand インスタンスから Spot インスタンスへ切り替えたことです。
話を単純化するために、新しい環境で主に使用している東京リージョン(ap-northeast-1)の r8g.xlarge を例にすると、2026/04/06 時点で On-demand インスタンスの単価は 0.28424 USD / hour、Spot インスタンスの単価は 0.0976 USD / hour です。
この前提では、1 時間あたりの単価は 1 - 0.0976 / 0.28424 = 約 65.7 % 削減できます。
なお、既存の EC2 環境では別のインスタンスタイプを利用していたため、実際の削減率はここで示した値とは異なります。
2 つ目は、EKS 上で複数の環境を Node に集約できるようになったことです。 EC2 ベースの個別開発環境では、1 環境につき 1 インスタンスを割り当てていたため、未使用の CPU やメモリリソースが環境ごとに分散していました。 一方、新しい環境では Karpenter を利用することで、必要な Node を動的に用意しながら、ブランチ環境や Pull Request 検証環境の Pod を同じ Node にスケジュールできます。 そのため、環境の数が増えてもリソースを共有しやすく、特に作成・削除が頻繁に発生する Pull Request 検証環境において、専用インスタンスを都度確保するよりも効率的に運用できます。
つまり、新しい環境のコスト削減は、旧環境から継続している自動停止運用を前提にしたうえで、Spot インスタンスによる単価の削減と、Node 集約によるリソース効率向上を上乗せしたものとして整理できます。
言い換えると、新しい環境のコスト削減は、以下の 3 つの要素の積み重ねです。
- 既存環境から継続している自動停止による削減
- On-demand インスタンスから Spot インスタンスへの切り替えによる単価の削減
- Node 集約によるリソース効率向上
特に 3 つ目は、ブランチ環境に加えて Pull Request 検証環境を動的に増減させる今回の構成において重要でした。 個別の開発環境を維持しながらも、環境ごとに固定のインスタンスを抱え込まない構成にできたことで、開発体験の改善とコスト削減を両立しやすくなりました。
おわりに
本記事では、EC2 ベースで運用していた Ruby on Rails アプリケーションの個別開発環境に代わる新しい開発環境を EKS 上に構築し、ブランチ環境と Pull Request 検証環境を使い分けられる仕組みを整備した取り組みを紹介しました。
今回のポイントは、単にコンテナベースの開発環境を用意したことではなく、開発のユースケースに合わせて環境の作成・更新の単位を設計し直したことにあります。 ブランチ環境ではブランチ単位で継続的に動作確認を行い、Pull Request 検証環境ではレビュー対象の変更を切り出して確認できるようになったことで、開発者やレビュワーが目的に応じて環境を使い分けられるようになりました。
また、Kubernetes によるヘルスチェックや Pod のライフサイクル管理、ExternalDNS や Istio によるアクセス経路の自動構成により、環境の運用を Kubernetes リソースの管理に統合できました。 さらに、既存の自動停止運用を活かしつつ、Spot インスタンスの活用と Karpenter による Node 集約を組み合わせることで、開発体験の改善とコスト削減を両立しやすい構成にできました。
同様に、既存の開発環境の運用やコスト、検証フローに課題を抱えている方にとって、本記事が参考になれば幸いです。
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。