Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

実運用からKubernetesならではの恩恵を考えてみる

こんにちは、広告技術部の平尾です。

本日12月15日から12月26日までの期間、Gunosy Tech Blog Festa を開催します。

期間中は平日に毎日、リレー形式で記事を公開していきます。

ぜひ最後までお楽しみください!

どのようなケースでKubernetes(以下K8s)を採用すべきか?

これは、頻繁に交わされる議論の一つです。 私たちは、現在K8s(Amazon EKS)をメインの基盤として採用し、運用しています。

K8sには数多くのエコシステムやメリットがありますが、AWS環境においては Amazon ECS というシンプルかつ強力な選択肢があります。

本記事では、当初の選定理由そのものというよりは、実際に運用を続ける中で見えてきた、ECSと比較した場合のK8sの恩恵について解説します。 もしECSを採用していた場合、実現が難しかったり、運用コストが跳ね上がっていたであろうポイントを振り返りながら、現場の視点でまとめました。

システムの概要

私たちは、グノシーなどのアプリに配信される広告の開発や保守運用を行っています。 広告配信、管理画面、スコアリングなど複数のマイクロサービスで構成されています。

StatefulSet

広告配信は、入札、表示、クリック、コンバージョンといった大量のトラフィックを処理しています。これらのログは単なるデバッグ情報ではなく、広告主様への課金根拠となるクリティカルなデータです。そのため、ログの取り扱いについてはとても慎重です。

ログ収集ツールのfluentdをStatefulSetとして定義し、EBS(Elastic Block Store)を中間バッファとしてS3へ転送するアーキテクチャを採用しています。

S3に直接書き込まず、EBSを中間バッファとして使っている理由は下記のとおりです。

信頼性の確保

S3の一時的なダウンやネットワーク障害時もログをロストせず、Pod再起動時も、未送信のログが保持されます。

コストの最適化

S3へのPUTリクエスト回数を大幅に削減(1秒毎の書き込み vs まとめて書き込み)することで、コスト削減効果を出しています。

パフォーマンス

小さなリクエストを大量に送るよりも、まとめてアップロードする方が効率的です。

つまり、EBSバッファは単なる永続化ではなく、信頼性の高いログバッファリング機構として機能しています。

ECSでもEBSは使えます。しかし、複数台にスケールアウトする際の運用負荷に大きな違いがあります。 ECSの仕様上、単一のサービスでタスク数を増やす(スケールアウトする)ことは可能です。 しかし、StatefulSetのように、各タスクが自分専用の永続ボリュームを保持したままスケールアウト・インする仕組みは、ECSには標準では備わっていません。 そのため、この構成を単一のサービスで実現しようとすると、起動エラーが発生します。そのため、それぞれに個別のEBSを持たせたままスケールするには、1台ごとに個別のECSサービスを作成するという力技が必要になります。

例えば、3台から10台に増やしたい場合、以下のような作業が発生します。

  1. 新しいEBSボリュームを7つ手動(またはTerraform)で作成

  2. タスク定義を7つ新規作成(それぞれ異なるボリュームをマウントするよう記述)

  3. ECSサービスを7つ新規作成

つまり、Terraformなどのコード上で7セット分の設定をコピー&ペーストし、個別にデプロイするという膨大な手間が発生します。これは急増するトラフィックに対応する運用として現実的ではありません。

では、K8sではどうかというと、ここでStatefulSetが役立ちます。StatefulSetを使うと、Podの増減に合わせてEBSの準備を自動化できます。 例えば、3台のfluentd + EBSが稼働しているとして、

  • fluentd-0 → volume-0 (100GB)

  • fluentd-1 → volume-1 (100GB)

  • fluentd-2 → volume-2 (100GB)

  apiVersion: apps/v1
  kind: StatefulSet
  metadata:
    name: fluentd-ap-northeast-1a
  spec:
    replicas: 3
    template:
      spec:
        containers:
          - name: fluentd
            volumeMounts:
              - name: fluent-log-volume
                mountPath: /mnt/fluentd

    # 各Pod専用のEBSを自動生成
    volumeClaimTemplates:
      - metadata:
          name: fluent-log-volume
        spec:
          accessModes: [ "ReadWriteOnce" ]
          storageClassName: gp3
          resources:
            requests:
              storage: 100Gi

HPA(Horizontal Pod Autoscaler)により台数が10台に増えた場合、自動的に fluentd-3 〜 fluentd-9 とそれらに紐づく専用のEBSが作成されます。 また、fluentd-0 のPodが落ちたあとに再開した場合には元の volume-0 を再接続します。これにより未送信ログから処理を継続できます。

このPodと専用ディスクのペアを自動管理する仕組みが標準で備わっている点は、運用コストを下げる上で非常に大きなメリットです。

Argo Rollouts

万が一、デプロイ時にバグが混入していたり、予期せぬ障害が発生した場合でも、影響範囲を最低限に抑える必要があります。私たちのチームではArgo Rolloutsを採用し、開発者が安心してデプロイできる環境を整えています。

  apiVersion: argoproj.io/v1alpha1
  kind: Rollout
  metadata:
    name: ad-server-1a  # AZ 1a専用
  spec:
    replicas: 100
    strategy:
      canary:
        steps:
          - setWeight: 1     # まず1台だけ新バージョン
          - pause: {}        # 手動承認待ち
          - setWeight: 10
          - pause:
              duration: 5m
          - setWeight: 50
          - setWeight: 100

ECSとCodeDeployの組み合わせでもカナリアリリースは可能ですが、上記のような柔軟な運用は難しいです。

  • CodeDeployはタイマー進行が基本ですが、Argo Rolloutsなら例えば、1台で一時停止しログに異常がないか確認できるまで無期限待機する、といった柔軟な制御が設定一つで可能です。

  • ECSで、例えばAZの1aだけ先行デプロイ、を行うには、サービスやターゲットグループを物理的に分割・管理する必要があり、構成管理コストが跳ね上がります。

  • Argo Rolloutsはネットワーク設定の変更だけでトラフィックを旧バージョンに戻せるため、コンテナの入れ替えを待つ必要がなく、即座にロールバックできます。

Argo Rolloutsのおかげで、「まずは1台だけ試す」といった判断が可能になり、「デプロイ可能な時間帯」という制約に縛られず、必要なタイミングでリリースできる体制になっています。

Namespace

私たちが管理しているクラスタでは複数のサービスが稼働しています。もしこれをECSで運用しようとすると、以下の二択を迫られます。

  • サービス毎にECSクラスタを作る。管理コストが増え、(特にEC2起動タイプを利用する場合)リソース(EC2)の余剰も大量に出るため非効率。

  • 単一ECSクラスタに詰め込む。サービス間の分離(権限、リソース制限)が難しい。

K8sにはNamespaceという強力な論理分離機能があります。私たちは単一のEKSクラスタ上で、NamespaceごとにLimitRangeを設定して、コンテナのデフォルトリソース制限を設定しています。

resource "kubernetes_limit_range" "ad-management-system" {
    metadata {
      name      = "ad-management-system"
      namespace = "ad-management-system"
    }
    spec {
      limit {
        type = "Container"
        default = {
          cpu    = "100m"
          memory = "256Mi"
        }
      }
    }
  }

これにより、単一のクラスタでリソース効率を最大化しつつ、サービスごとの独立性と安全性を担保できています。

PodDisruptionBudget(PDB)

インフラ運用において、ノード(サーバー)のメンテナンスや入れ替えは避けられない作業です。しかし、何の対策もしていないと、インフラ側の都合で、本来稼働していなければならないPodが一度に複数停止され、サービスダウンを招くリスクがあります。

ECSにはデプロイ時に minimumHealthyPercent を設定する機能はありますが、これはあくまで「デプロイ中」に新しいタスクが起動するまで古いタスクを残す設定です。

ECSにもドレイニング機能はありますが、K8sのPDBのようにサービスの稼働率を維持できない場合は、インフラ側の停止操作自体をブロックするといった強制力のある制御まではできません。そのため、タイミングが悪ければ必要なタスク数割れを許容してしまい、障害につながる可能性があります。

対して、PDBを使うと、最低でもこれだけのPod数は確保しなさい、というルールをクラスタレベルで強制できます。

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: ad-server
spec:
  maxUnavailable: 20%  # いかなる理由があっても、同時に停止できるのは20%まで

この設定が存在する限り、もしインフラ側の操作でPodを停止しようとしても、それがPDBのルール(例: 稼働率80%維持)に違反する場合は、K8sがその停止操作自体をブロックします。

PDBにより、大規模な運用においても、インフラ起因のサービスダウンを未然に、かつ確実に防ぐことができるのです。

まとめ

本記事では、実運用を通して見えてきた、ECSでは実現が難しい、または高い運用コストを伴う、私たちが Kubernetes を選ぶ4つの理由について解説しました。

  • StatefulSet: 課金に関わるログを絶対にロストさせないため(EBSの自動管理)

  • Argo Rollouts: 安全なデプロイを実現するため

  • Namespace: 多数のサービスを効率的かつ安全に同居させるため

  • PDB: インフラメンテナンス時のサービスダウンを強制的に防ぐため

また、本記事では触れられませんでしたが、Istio を用いたサービスメッシュの構築や、Skaffold による開発者体験の向上など、これら以外にもKubernetesの豊富なエコシステムから多大な恩恵を受けています。

ステートレスな構成や、シンプルな運用が求められる環境であれば、ECSは非常に優れた選択肢です。

一方で、今回のようにインフラ側でアプリケーションの挙動を細かく制御したいという要件が積み重なる場合、Kubernetesの表現力が不可欠になります。初期の学習コストこそかかりますが、長期的な運用の安定性と拡張性を担保する上では、我々にとって、Kubernetesは必要不可欠なサービスとなっています。