Kubernetes導入で爆速進化!AI/ML基盤、自社サービスの移行の裏側とコスト削減術を大公開!!

はじめに

こんにちは、レバレジーズテクノロジー戦略室SREチームのLEEです。

テクノロジー戦略室では部署横断的な技術に関するプロジェクトを推進しており、私が担当しているのはKubernetes(クバネティス)の導入と将来的にプラットフォーム化していくプロジェクトです。

ところでみなさん、Kubernetes(以下K8s)について最近気になっていませんか?
気になりますよね?
そう、気になるんですよ。

そんなみなさんに本記事ではレバレジーズテクノロジー戦略室が推進するK8s化プロジェクトの導入事例やメリットについてご紹介いたします。K8sについてまだよくわからない方もご安心ください、簡単に説明させていただきます。

K8sのよさ

K8sって何?

K8sは、コンテナ化されたアプリのデプロイ、スケーリング、管理を自動化するためのオープンソースのプラットフォームです。コンテナオーケストレーションツールとして広く利用されており、複雑なアプリケーションの運用を大幅に簡素化できます。
コンテナ化されたアプリを運用するのに最適なツールです。比較的大規模かつ可用性を必要とするシステムに向いていると言えますね。

K8s、特にEKSのよさ

EKSとは?

EKS

EKS(Amazon Elastic Kubernetes Service)は、AWSが提供するマネージドK8sサービスです。AWSの他のサービスと簡単に連携できたり、メンテナンスを一部AWS側でサポートしてくれます。

一番便利だと感じたのはAWS側がコントロールプレーンを管理してくれるところですね。K8s導入のネックと言われる頻繁なバージョンアップデートも、EKSならAWS側のサポートありで対応できます。イメージとしてはAurora RDSと同じですね。マネージドノードグループを使うことで、バージョンアップグレードも思ったより簡単にテストできて便利です。より詳しいバージョンアップ戦略についてはまた別の機会に紹介します。 バージョンアップに関する公式のドキュメントはこちら
コントロールプレーンって何?

K8sのメリット、デメリット

K8sってどんなメリットがあるの?デメリットは何?って思いはじめた頃なんじゃないでしょうか。
それでは以下をご覧ください。

メリット
  • 高い柔軟性と拡張性: K8sは豊富な機能と拡張性を備えています。あらゆる規模のアプリケーションに対応でき、親和性の高いOSSツールも多く、高度なデプロイ戦略やワークフローの実装が可能です。
  • 高い可用性と信頼性: アプリケーションの自動的な再起動や配置により、ダウンタイムを最小限に抑え、高い可用性を実現します。
  • 活発なOSSコミュニティ: K8sはGoogleで開発され、今や世界中の開発者から支援されているプロジェクトであり、活発なコミュニティが存在します。技術的なサポートや情報が豊富で、公式ドキュメントも優れています。
  • 効率的なリソース管理: コンテナ化されたアプリケーションを効率的に管理し、リソースの利用率を最適化できます。
    デメリット
  • 複雑性: 導入するまでの敷居が高く、運用が少し複雑で、高度な知識やスキルが必要です。
  • 学習コスト: 習得に時間がかかり、学習コストが高い方です。
  • リソース消費: K8s自体が一定のリソースを消費します。中規模以上のプロジェクトでは気にならない程度ですが、小規模なアプリケーションや環境では、オーバーヘッドが大きくなる場合があります。

もしK8sを導入したいと思う方がいらっしゃいましたら、上記のメリットやデメリットを考慮しておくと良いと思います。

ArgoCDを使ってもっと便利にしたい

ArgoCDとは?

ArgoCDとは、GitOpsの原則に基づいたCD(継続的デリバリー)ツールです。Gitリポジトリで管理されたアプリケーションの設定を自動的にK8sクラスタに適用し、デプロイ作業を効率化します。

ArgoCDはArgoProjectというOSSコミュニティによって開発されているツールで、K8sとの親和性が特に高く、K8s内部を可視化してくれるダッシュボードがとても優れています。

ArgoCDのダッシュボード

Gitリポジトリを更新したら自動で変更がデプロイされるところが特に便利ですし、ダッシュボードでは権限を持っているユーザーがpodを再起動させたり、kubectlを使わずとも内部構成が見れたりするので、K8sに詳しくないメンバーとの連携もサポートしてくれるツールだなと感じました。

まとめると...

  • ダッシュボードが使いやすい
  • 自動デプロイが便利
  • K8s知らない人にもわかりやすい

です!

Argo Rolloutsで Blue/Greenデプロイしたい

Argo Rolloutsとは?

Argo Rolloutsとは、Blue/Greenデプロイ、カナリアデプロイなど高度なデプロイ戦略をK8s上で実現するためのツールです。デプロイ中のダウンタイムを最小限に抑えつつ、安全にアプリケーションをリリースできるようにしてくれます。

Argo RolloutsもArgoProjectで管理されているOSSで、同じくダッシュボードが提供されます。 こちらの公式ドキュメントを参照してテストを行い、values.yamlファイルをカスタマイズしてデプロイしました。

専用のダッシュボードはデフォルトでは有効化されてないので、Helm チャートで有効化しましょう。

Blue/Green デプロイの導入

今回のK8s移行プロジェクトでは、レバレジーズ社製サービスであるteratail(テラテイル)のFrontEndアプリにBlue/Greenデプロイを導入しました。 自動でデプロイされた本番同様のプレビュー環境で開発者がUIなどを確認し、確認が終わったらArgo Rolloutsのダッシュボードで昇格(プロモート)できるようにしました!

下記のような流れで、安全な本番環境リリース作業を実現させました。

  1. 開発者がGithubリポジトリを更新する
  2. ArgoCDがGithubの更新を検知し、自動でプレビュー環境をデプロイ
  3. 開発者がプレビュー環境を確認する
  4. 問題なければ昇格(プロモート)!問題があったらリリース作業を中断!
  5. 昇格(プロモート)後はプレビュー環境が自動削除される

Blue/Green デプロイの注意点

ALBを残さない!

今回導入したプレビュー環境は独自のDNSやALBを持つようにしました。
プレビュー環境での確認が終わると、ArgoRolloutsが自動的にプレビュー環境のpodを削除してくれます。ですが、ALBは削除してくれないので、小さいながらALB分の稼働コストが無駄になってしまいます!
これはいけない。そこで今回はAnalysisTemplateを使ってALBも自動で削除するように設定しました。

AnalysisTemplateはArgoRolloutsのロールアウトが完了した時点でトリガーされるタスクです。 今回はプレビュー環境のALBと紐づいてるingressを自動で削除することで、プレビュー環境が不要な時に無駄なALBのコストが発生しないようにしました。

AnalysisTemplateはK8sのJobと同じく、クラスター内での権限を設定してあげる必要があります。 ServiceAccountを作成し、適切なロールと権限を付与してあげましょう。

※下記コンポーネントたちは同じNamespaceに属する必要があります。

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: <your_custom_name>
  namespace: <namespace>
spec:
  metrics:
    - name: <job_name>
      provider:
        job:
          spec:
            ttlSecondsAfterFinished: 30
            template:
              spec:
                serviceAccountName: analysistemplate
                containers:
                - name: <your_container_name>
                  image: bitnami/kubectl:latest
                  command: ["sh", "-c", "kubectl -n <namespace> delete ingress <your_ingress_name>"]
                restartPolicy: OnFailure
—
apiVersion: v1
kind: ServiceAccount
metadata:
  name: analysistemplate
  namespace: <namespace>
—
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: ingress-deleter
  namespace: <namespace>
rules:
- apiGroups: ["networking.k8s.io"]
  resources: ["ingresses"]
  verbs:
    - get
    - list
    - delete
—
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ingress-deleter-binding
  namespace: <namespace>
subjects:
- kind: ServiceAccount
  name: analysistemplate
  namespace: <namespace>
roleRef:
  kind: Role
  name: ingress-deleter
  apiGroup: rbac.authorization.k8s.io

Blue/Greenデプロイのやり方は色々あると思いますが、やっぱり無駄なコストを削るというのが重要なポイントだと思います。
運用効率化とコスト削減の二兎をうまく捕まえられるかどうかが鍵となります!

Prometheus + Grafana + ElasticSearchでもっと可視化したい

Prometheus / Grafana / ElasticSearch

K8sの監視ツールと言えばやっぱりPrometheus+Grafanaを思い浮かべる方が多いんじゃないでしょうか。また、ログ収集と検索ではElasticSearchも外せないですよね。K8sにおいてこれらのツールは定番で、OSSなので別途の費用負担なく導入できるのが最大のメリットです。

まずは各ツールについて簡単に説明させていただきます。

  • Prometheus :コンテナのメトリクス収集・監視ツール
  • Grafana:Prometheus、ElasticSearch等で収集したデータを可視化するためのダッシュボードツール
  • ElasticSearch:ログ収集・検索用データベース

これらのツールは突き詰めると奥が深いので詳しい説明はまた今度の機会とさせてください!
ここはまず導入時の試行錯誤をいくつかご紹介いたします。

アラートをカスタマイズしたい!

監視ツールを使う上で可視化と同じように重要なのが適切なアラートを飛ばすことだと思うんですよね。今回私たちが導入した Prometheus + Grafanaは kube-prometheus-stack というHelmチャートを使っており、PrometheusとGrafanaをセットでデプロイしてくれる便利なチャートです。

このチャートではデフォルトで設定されているPrometheusのアラートルールがあり、チャート上で使いたいルールを選択して適用しました。

他にはPrometheus Blackbox Exporterを使用してサイトの死活監視を設定しました。
Blackbox Exporterはまた別のHelmチャートでデプロイしてあげる必要があります。 kube-prometheus-stack Helmチャートでの設定値は以下の通りです。

prometheus:
  prometheusSpec:
    additionalScrapeConfigs:
      - job_name: 'external-targets'
        metrics_path: /probe
        params:
          module: [http_2xx]
        static_configs:
          - targets:
              - <TARGET_URL>
        relabel_configs:
          - source_labels: [__address__]
            target_label: __param_target
          - source_labels: [__param_target]
            target_label: instance
          - target_label: __address__
            replacement: prometheus-blackbox-exporter.monitoring.svc.cluster.local:9115
    retention: 30d
additionalPrometheusRulesMap:
  - name: external-monitoring-alerts
    groups:
      - name: external-targets-alerts
        rules:
          - alert: ExternalTargetWarning
            expr: probe_success == 0
            for: 30s
            labels:
              severity: warning
            annotations:
              summary: "External target {{ $labels.instance }} might be down"
              description: "The external target {{ $labels.instance }} has intermittent issues."
          - alert: ExternalTargetCritical
            expr: probe_success == 0
            for: 5m
            labels:
              severity: critical
            annotations:
              summary: "External target {{ $labels.instance }} is down"
              description: "The external target {{ $labels.instance }} is unreachable for 5 minutes."

他にもGrafanaのコンソール上でアラートを設定したり、additionalPrometheusRulesMap配下で追加のPrometheusのルールを追加することもできます!

ログをS3にも格納したい

ElasticSearchは単体ではログを収集できないので、必ずログを集めてきてくれるパートナーが必要です。
そこでまず最初に検討したパートナーがElasticSearchと同じくElastic社が開発した「Filebeat」です。Filebeatはとても軽量なログ収集ツールで、ECK-StackというHelmチャートにも含まれていてElasticSearchとの親和性も高くて良さげに見えました。

しかし、私たちが使っているのはOSS版。OSS版のFilebeat単体ではS3にログをアウトプットする機能がなく、同じくElastic社のLogStashが必須でしたがLogStashはリソース消費量の多いアプリなのでどうしたものか...
そこで採用したのが「Fluentbit」です。FluentbitはFilebeatと同じく軽量でありながらOSSコミュニティの開発も活発で、単体でS3へのアウトプット機能も備えていて最適なツールでした。Fluentbitは別途のHelmチャートを使ってデプロイし、アウトプットの設定はこのようにしました。

  outputs: |
    [OUTPUT]
        Name s3
        Match kube.*
        region ap-northeast-1
        bucket <YOUR_BUCKET_NAME>
        storage_class STANDARD
        total_file_size 100M

このようにOSSツールをフル活用してマイクロサービスやクラスターの監視体系を構築しました!
K8s内にホスティングさせている分リソースは食いますが、OSSなのでランニングコストが無料なのが最大のメリットです!
カスタマイズもし放題ですしね。

アプリケーションを乗せよう

使用した技術スタック

さて、ある程度ツールの話を進めたところで今回のプロジェクトで使用した技術スタックをまとめてみたいと思います。 今回のK8s移行では、以下の技術スタックを使用しました。

K8s (EKS):
  - version: v1.31
APP:
  - TypeScript
CI/CD:
  - ArgoCD v2.13.0
  - Argo Rollouts v1.7.2
  - Github Actions
監視:
  - Prometheus v0.81.0
  - Prometheus-blackbox-exporter v0.25.0
  - Grafana v11.6.0
  - ElasticSearch v9.0.0
  - Kibana v9.0.0
  - Fluentbit v3.2.0
  - CloudWatch
DB:
  - MySQL v8.x
  - PostgreSQL v16.x
ネットワーク:
  - gRPC
  - Envoy

Dify, Airflowを乗せる

K8s導入プロジェクトの最初の一歩として、OSSアプリケーションをK8s上に乗せました。 誰でも簡単にコンソール上でAI/LLMアプリが作れる「Dify」と、ワークフロー自動化ツールである「Apache Airflow」です。

Dify / Airflow

こちらのHelm Chart(Dify, Airflow)を用いてデプロイし、テスト段階ではコスト削減のためEKS内部でPostgreSQL DBを作ってテストし、本番運用開始後はAurora RDS PostgreSQLに移行させました。

後述しますが、Airflowを運用するEKSクラスターではコスト削減のためにSpotインスタンスを導入しています!

レバレジーズのサービスを移行させる

ここまでは主にOSSツールのデプロイについて話してきましたが、この章では自社製アプリケーションのデプロイについて説明します! 今回はレバレジーズ社製サービスである「teratail(テラテイル)」をECS on FargateからEKS on EC2に移行させました!

teratailとは?

teratail(テラテイル)」は、2014年7月にオープンしたエンジニアの問題解決プラットフォームです。学生やプログラミング初心者から第一線で活躍する方々まで、幅広いエンジニアの問題解決をサポートするサービスです。

コスト削減と可用性

主な目標は「コスト削減」と「可用性の向上」でした。
K8sクラスター本体の維持コストが月$75ほどかかってしまうのはありますが、EC2のリソースを共有しながら有効活用できる+ALBの数を減らせたので結果的には約1割ほどのコストダウンに成功しました。

また、可用性向上のため水平Pod自動スケーリング(HPA)Cluster Autoscalerを導入しています。可用性を測るための負荷試験ではk6というツールを利用しました。

そろそろみなさんもだいぶK8sの良さがわかって来たんじゃないでしょうか! 次の章では私がK8sの構築中に踏んだいくつかの罠の事例をご紹介いたします。

K8sで注意したいこと

gRPCの罠

gRPCは、Google社が開発した高性能なオープンソースのRemote Procedure Call(RPC)フレームワークです。
HTTP/2を通信プロトコルとして使用しているのが大きな特徴であり、K8sのクラスター内部通信は基本的にHTTP/1.1であるため、K8sでgRPCを使うには専用の設定をいろいろしてあげないといけませんでした。

コンテナのgRPCヘルスチェック

gRPCを使っているマイクロサービスならコンテナのヘルスチェックもgRPCでやりたいですよね?
そんなあなたにピッタリなのが gRPC probe(プローブ)です。
K8sにはコンテナを監視する様々な種類のprobeがあり、それぞれのprobeは決められた通信方法でコンテナを監視します。
K8s v1.27からはこのprobeの通信方法にgRPCが追加され、特別なプラグインとかがなくてもK8sの機能でgRPCヘルスチェックを使うことができます。

※用意するもの
ヘルスチェック用のgRPCサービス
gRPC probe

👇K8sでの設定ではgrpc.service名を指定してあげるのがポイントです(livenessでもreadinessでも可)

livenessProbe:
  grpc:
    port: 50051
    service: "Health"

負荷分散しない問題

gRPCを使う場合、負荷が高くなってpodの数が増えても一般的な K8s Serviceオブジェクトでは適切に増えたpodに負荷分散されない問題があります。詳しい説明は割愛させていただきますが、これはgRPCがコネクションを使い回す性質が原因であり、Serviceの裏にあるpodが増えても既存のpodのIPを使い回しているのです。

ここで登場するのが「Headless Service」で、簡単に説明するとHeadless Serviceは裏のpodの仮想IPを返すのではなく、DNSで名前解決しているような挙動をすることでgRPCが特定のIPと紐づかなくなります。

まだこれで終わりではありません、K8s内でgRPCを適切に扱うにはさらに「Envoy」コンテナが必要になります。

Envoyを活用しよう

Envoyは主にプロキシとして使われることが多いサービスですが、今回はHTTP/1.1をHTTP/2に変換する用途で使用します。 本来の構成ではBFFサービスが各バックエンドサービスへの振り分けを行っていましたが、バックエンドとBFFの間にEnvoyコンテナを下の図のように入れる形にしています。

こうすることで追加するEnvoyコンテナは最低限にしつつ、gRPC通信をK8s上で適切に使うことができました。
Envoyコンテナの数が増えるとその分リソースも使いますしね。

メモリーリークをなんとかしたい

今回K8sに移行したteratailサービスには、フロントエンドコンテナでメモリリークが起きるというバグがありました。
K8sではコンテナごとにリソースの上限を割り当てていて、上限に達すると強制的にOOMによるKillが発生し、podが再起動されます。しかし、自動的に再起動されるとはいえOOMが発生してしまうとわずかながらフロントエンドにアクセスできなくなる時間が出てしまいます。
これをK8sのレイヤーで解決するために、OOMが起きる前に定期的にフロントエンドサービスを再起動させる仕組みを導入しました。

CronJobを活用しよう

CronJobは時間を決めて自動で指定したタスクを実行するK8sのオブジェクトです。 今回はOOMが発生するのがおおよそ4時間ごとだったため、このCronJobでは3時間ごとにフロントエンドサービスをダウンタイムなしで再起動させるようにしました。
CronJob内で実行したいコマンドは事前に用意しましょう。
今回はArgo-Rollouts用の「rollout」というオブジェクトを再起動させるため、kubectlとArgo-Rolloutsのプラグインをインストールし、再起動コマンドを実行させています。

apiVersion: batch/v1
kind: CronJob
metadata:
  name: front-auto-restart
  namespace: <NAMESPACE>
spec:
  successfulJobsHistoryLimit: 1
  schedule: 10 */3 * * *
  jobTemplate:
    spec:
      ttlSecondsAfterFinished: 3600
      template:
        spec:
          serviceAccountName: front
          restartPolicy: OnFailure
          containers:
          - name: alpine
            image: public.ecr.aws/docker/library/alpine:3.21.3
            imagePullPolicy: IfNotPresent
            command:
              - /bin/sh
              - "-c"
            args:
              - set -e;
                echo "Installing dependencies...";
                apk add --no-cache curl bash;
                echo "Download, Installing kubectl...";
                curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl";
                chmod +x kubectl;
                mv kubectl /usr/local/bin/;
                echo "Download, Installing kubectl argo-rollouts plugin...";
                curl -LO "https://github.com/argoproj/argo-rollouts/releases/download/v1.8.0/kubectl-argo-rollouts-linux-amd64";
                chmod +x kubectl-argo-rollouts-linux-amd64;
                mv kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts;
                echo "Restarting rollout...";
                kubectl argo rollouts restart front-rollout -n <NAMESPACE>;
                echo "Restart complete"

次はこのCronJobに必要な権限とServiceAccountを用意すればOKです。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: front
  namespace: <NAMESPACE>
automountServiceAccountToken: true
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: front-auto-restart-role
  namespace: <NAMESPACE>
rules:
  - apiGroups: ["argoproj.io"]
    resources: ["rollouts"]
    verbs: ["get", "list", "patch", "update"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: front-auto-restart-rolebinding
  namespace: <NAMESPACE>
subjects:
  - kind: ServiceAccount
    name: front
    namespace: <NAMESPACE>
roleRef:
  kind: Role
  name: front-auto-restart-role
  apiGroup: rbac.authorization.k8s.io

これらはほんの一例に過ぎないものたちでしたが、結構頻繁に遭遇する罠だと思いますので気をつけたいですね。

コスト削減したい

Spotインスタンスを活用しよう

EKS on EC2ではノードグループを複数に分けることで、一部だけEC2 Spotインスタンスを使うことができます。
Spotインスタンスは格安な代わりに突然使えなくなることがあるようなEC2です。そのため、Spotインスタンスでは「いつ突然再起動されても大丈夫なもの」だけ乗せたいですね。
具体的には定期実行のジョブやバッチなどです。

今回の移行においてこのSpotインスタンスの活用にピッタリなものがApache Airflowでした。Airflowでは定期的に実行されるワークフローを組めるサービスですが、このワークフローを実行するpodだけをSpotインスタンスに乗せたいということです。この方法はAWSの公式ドキュメントでも推奨されており、このドキュメントに沿って設定を行ったら簡単にできました。

Apache Airflowの公式Helmチャートを使う場合は以下のような設定が必要です。

workers:
  # EC2 Spot Instanceを使うための設定
  tolerations:
    - key: "spotInstance"
      operator: "Equal"
      value: "true"
      effect: "PreferNoSchedule"
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: "lifecycle"
                operator: "In"
                values:
                - "Ec2Spot"

Savings PlanとRIを活用しよう

EKS on EC2を使う最大のメリットと言っても過言ではないのがAWSのSavings PlanReserved Instanceの恩恵を受けられることだと思います。
もし使う予定のEC2のスペックやファミリーが1年以上変わらないのであれば、RI全額前払い購入が断然お得です。
私たちはこれからも新規サービスを開発してどんどんK8s上に乗せる予定があったため、EC2のスペックやファミリーの指定がない Compute Savings Planで購入しました。
これだけでも約50%ほどのAWSコストを削減できます!

今後の計画

プラットフォーム化

レバレジーズテクノロジー戦略室では事業にとらわれず全社横断のプロジェクトを推進しています。将来的には社内で統一されたK8sのプラットフォームを構築し、社内で簡単に使えるプラットフォームとして提供できることが目標です。
現在はAWSを主軸としていますが、ゆくゆくはマルチクラウド化することも視野に入れています。

監視の集約

プラットフォーム化が進められれば、コンテナを監視するツールであるPrometheus+Grafanaや、ログ収集のElasticSearchなどのツールを1つのクラスターに集約し、全社の各クラスターを監視する監視基盤を構築することも計画しています。
また、今回はまだ導入できていませんが、OpenTelemetryも導入を検討しています。

終わりに

K8s移行プロジェクトは最初の敷居こそ高いものの、移行後のアプリケーションの運用や実装はとてもスピーディーになるんですよね。新しいアプリケーションを乗せたい時も、コンテナさえあればデプロイとテストが非常にスムーズに行えます。
これでみなさんもK8sに移行してみたくなったんじゃないでしょうか!?

長い文章にお付き合いいただきありがとうございました。

弊社にご興味のある方へ

レバレジーズでは、一緒に働く仲間を募集しています! 少しでも興味を持っていただけたら、ぜひカジュアル面談にご応募ください! hrmos.co