2020-05-15

EKSを用いたマイクロサービスシステムの構築と運用

taxagawa

はじめに

こんにちは、エンジニアリング事業本部の@taxagawaです。 昨年新卒としてスマートショッピングに入社して以来、SREとして既存と新規を問わずシステム全般に関わる構築・運用などの業務を行なっています。

弊社ではSaaS事業プロダクトとして、IoTを用いた在庫管理・発注自動化ソリューションを実現するスマートマットを提供していますが、この度既存システムの大規模なバージョンアップを敢行し3月初めに無事リリースすることができました。今回のバージョンアップでは一部の例外を除きシステム全体においてマイクロサービスアーキテクチャを採用し、それをKubernetes(以下k8s)上で稼働させています。

本投稿では現在稼働中のk8sクラスタ周りのシステム構成やCI/CDの仕組み、また運用開始から約2か月経過した時点で積み重なってきたノウハウや課題について記述していきます。拙い文章ではありますが、これからk8s(特にEKS)を使用したシステムを構築する予定の方、あるいは既に運用されている方の助けになれば幸いです。

なお、k8sに関する用語や基本的な使い方についての説明の大半は省略させていただきます。

目次

  • はじめに
  • 目次
  • システムの全体構成
    • 使用技術スタック
      • Managed Node Groups
      • Ambassador
      • AWS ALB Ingress Controller
      • k8s-cloudwatch-adapter
      • Cluster Autoscaler
  • CI/CDの構築
    • Argo CD
    • 本番デプロイまでの具体的なフロー
  • jobの実行方法
    • CronJob
    • kube-airflow
    • CronJobに移行した理由
  • Datadogを用いた監視・ログ収集基盤の構築
    • エージェントの導入
    • HPAの設定
  • その他のTips
    • 開発環境の自動起動・破壊
    • Argo CDの設定のバックアップ
    • デプロイ時の瞬断への対策
  • 現在の課題
    • YAMLやterraformの管理
    • 本番環境でのスポットインスタンスの利用
  • まとめ

システムの全体構成

スマートマットを支える基盤システムは、AWSが提供するフルマネージド型のk8sサービスであるEKS上で稼働しています。現時点では開発環境と本番環境およびQA環境の3種類のクラスタが複数のAWSアカウントにおいて運用されており、本番環境では17のマイクロサービス(以下MS)・最大で約200個のPodが動いています。

以下の図は、EKSとその周辺AWSサービスによるシステム全体の大まかな構成図となります。

cluster structure

EKSのノードは複数のプライベートサブネットに跨って稼働しており、これらとは異なるVPC内に位置するRDSやRedisとの通信にはVPCピアリングを、SQSやSNSといった他のAWSサービスとの通信にはVPCエンドポイントを使用しています。またインバウンドの通信はすべてCDNやWAFを通すことでセキュリティを担保しています。画像内には描かれていませんが、ログの収集・監視はDatadogで行なっており、Datadogで補いきれない部分をCloudWatchによって監視しています。

使用技術スタック

以下に現時点で使用している技術スタックを列挙します。

AWS関連のサービスを含めたインフラに関するもの

  • AWS
    • EKS(Managed Node Groups)
    • ECR
    • RDS
    • Elasticache(Redis)
    • SNS
    • SQS
    • S3
    • CloudFront(画像配信のみ)
    • WAF
    • X-Ray
  • GitHub Actions
  • Argo CD
  • Cloudflare
  • Datadog
  • Ambassador

監視・ログ収集

  • Sentry
  • Terraform

k8sのアドオン

  • AWS ALB Ingress Controller
  • k8s-cloudwatch-adapter
  • Cluster Autoscaler
  • Metrics Server

開発やデバッグに使用するツール

  • stern
  • Telepresence
  • k9s

ここでは上で挙げたものの中で特に目新しいものや、私が構築時に調査した中で情報が少ないと感じたものについていくつか簡単に説明していきます。なおTelepresenceに関しては本ブログのこちらの記事にて既に詳しく説明しておりますので今回は触れないこととします。またArgo CDの運用やDatadogの構築に関しては別の章にて詳しく説明します。

Managed Node Groups

Managed Node GroupsはEKSで稼働するワーカーノードのプロビジョニングとライフサイクル管理を行ってくれる機能で、2019年11月に発表されました。これは丁度私たちがEKSの構築を始めたころであり、ワーカーノードの管理に関する諸々の煩わしさから解放されることを見込んだ上で試してみようということになりました。

さて実際に構築・運用をしてみて感じたManaged Node Groupsの良かった点・改善されると良い点を以下にまとめてみます。

良かった点

  • ほとんどノードの管理が不要

この点については期待通りで、運用開始から2か月以上経過した現在でもノードに対して手動で何らかの操作を行うことはほとんどありませんでした。もともと急なアクセス増加が起こりにくい(あるいは事前に想定しやすい)サービスではありますが、後述するCluster Autoscalerと併せて使用することで、ノードのリソースの状態に機敏に反応して適切にスケールイン/アウトを行なってくれています。 また止むを得ずノードを終了・更新する必要がある場合も、従来のようなPodの退避やスケジューリングを意識することなく実行できるため、たとえばノードのリタイアメントに対応する際にわざわざkubectlコマンドを叩く必要がなくなります。

  • ノード作成時の煩わしい設定が不要

AWSリソースはTerraformで管理していますが、非マネージドなものと比べてAuto Scalingグループやノード起動時のスクリプトの設定が不要となり、下記の例のように記述量はかなり少なくなります。

resource "aws_eks_node_group" "eks_managed_node_group" {
  cluster_name    = local.cluster_name
  node_group_name = "${local.cluster_name}-node-group"
  node_role_arn   = local.node_role
  instance_types  = ["t3.large"]
  disk_size       = 50
  subnet_ids = [
    aws_subnet.eks_private_01.id,
    aws_subnet.eks_private_02.id
  ]

  scaling_config {
    desired_size = 10
    max_size     = 20
    min_size     = 5
  }
  depends_on = [aws_eks_cluster.eks_cluster]
}

改善されると良い点

  • スポットインスタンスが利用できない

この記事の執筆時点(2020年5月)においてはManaged Node Groupsではノードとしてスポットインスタンスを利用できません。AWSのcontainer-roadmapには既にissueが存在しており近いうちに実装される予定となっているようですが、しばらくはオンデマンドインスタンスで運用するか、通常のオートスケーリンググループと併用することになると思います。 私たちの場合はコストの観点から、開発環境やQA環境のノードはすべてスポットインスタンスで稼働させたいという要望があるため、本番環境はManaged Node Groupsである一方、それ以外の環境は非マネージドなノードで稼働しているといった齟齬が発生してしまっています。

Ambassador

AmbassadorはMSのためのサイドカープロキシであるEnvoy専用のAPI Gatewayで、k8sクラスタ内に入ってくるトラフィックを転送およびフィルタリングするコントロールプレーンです。導入するとクラスタへのWebリクエストをAmbassadorを経由させてk8s内のPod(あるいはService)に転送できます。

Ambassadorの特徴としては以下のものが挙げられます。 - ルーティングとスケーリングがEnvoyとk8sに依存しているので展開と操作が簡単 - Istioと連携してサービスメッシュ化が可能 - 細かなルーティング制御、正規表現ベースのルーティング、ホストルーティングなどが可能 - gRPC、HTTP/2のサポート - Traffic Shadowing(Ambassadorに転送されたトラフィックを非同期にコピーし、違うServiceにトラフィックを送信する機能)

私たちのサービスでは、Webリクエストを受け付けるフロントエンドやBFFなどのMSの前にAmbassadorのPodとServiceを設置し、後述のAWS ALB Ingress Controllerと組み合わせることで次のようにトラフィックを処理しています。

リクエスト -> ALB(Ingress) -> Service(Ambassador) -> Pod(Ambassador) -> Service(MS) -> Pod(MS)

AWS ALB Ingress Controller

マニフェストによってALBを宣言的に管理することができ、Ambassadorとの連携も容易に行うことができます。AmbassadorのPodに障害が発生し再作成が実行された場合、ALBのターゲットグループに対する新たなPodの登録、不要なPodの削除を自動で行ってくれるため、冗長性を確保しておけばAmbassadorに起因するようなサービスの停止は避けることができます。

k8s-cloudwatch-adapter

k8s-cloudwatch-adapterはCloudWatchのメトリクスを取得するための、k8sのCustom Metrics APIおよびExternal Metrics APIであり、AWSが公式に提供するアドオンです。これを利用することで、Horizontal Pod Autoscaler(以下HPA)のスケール条件にCloudWatchのメトリクスを使うことができるようになります。

CloudWatchのメトリクスでHPAを実現させるだけであれば、DatadogでAWSアカウントと連携し希望するサービスのメトリクス収集を有効にするだけでも可能です。しかしこちらの公式のドキュメントに書かれている通り、DatadogによってCloudWatchのメトリクスを収集する際には最低でも10分の遅延が発生してしまい、これでは実用に耐えうるような設定ができませんでした。そこでよりリアルタイムに近い形でメトリクスを収集できる解決策としてk8s-cloudwatch-adapterを採用することになりました。

Cluster Autoscaler

ノードのオートスケーリングにはCluster Autoscalerを使用しています。ノードのスケールアウトはリソース不足によりPodの立ち上げが失敗したときに行われ、一定時間利用率が低いノードが存在して尚かつそのノード上のPodが他のノードに移動させられる場合にスケールインが実行されます。オートスケールに関する細かい挙動や各パラメータの役割などに関しては公式のFAQの内容が非常に充実しているため、そちらを参考にすれば大抵の問題には対応できると思います。

私たちのクラスタではほとんどパラメータを変更せずデフォルトの状態でClouster Autoscalerを稼働させています。複数のCronJobが動き始めるタイミングでノードのリソースが足りなくなることがしばしば発生するのですが、デフォルトで10秒に1回Podの状態をチェックするAPIが投げられているため即座にオートスケールが実行され、2分と経たないうちに新たなノードが立ち上がるようになっています。もう少しパラメータの最適化を行うべきではあるのですが、AWS公式の導入にただ従ってデフォルトのまま導入するだけでも十分に働いていくれています。

CI/CDの構築

先の技術スタックの項でも挙げましたが、CI/CDにはGitHub ActionsとArgo CDを使用しています。 それぞれの選定理由に関して、前者についてはCI/CDの構築を始めたときが丁度GitHub Actionsが正式に公開されたタイミングと重なっており、実験的な意味合いも込めて使ってみようということになりました。また料金面においても、それほど大規模でなければ他サービスと比較してかなり安価に利用できる点も魅力的でした。 後者に関してはデプロイツールを丁寧に調査する時間があまりなく、なるべくシンプルに使用できるものが望ましかったということもあり、他社事例が豊富にあり信頼のできるArgo CDを使用することとなりました。

Argo CD

フローの説明に入る前に、Argo CDに関して簡単に述べます。 Argo CDはArgoコミュニティとIntuit社によって開発されているオープンソースのk8s-nativeなCDツールです。宣言的継続的デリバリー(Declarative Continuous Delivery)システムなどとも説明されています。

Argo CDはGitOpsの考えに従って設計されており、k8sクラスタ内にPodとして配置された後は、設定した特定のGitリポジトリをクラスタ内のアプリケーションの状態を定義する唯一のソースと見なし、アプリケーションの展開を行います。

Argo CDを用いたデプロイまでのフローとしては、次のようなものがよく見られます。

argo flow https://blog.argoproj.io/introducing-argo-cd-declarative-continuous-delivery-for-kubernetes-da2a73a780cd より引用

上の図を説明すると、開発者がアプリケーションのコードをGitリポジトリにプッシュすると、CIによるビルドが走り、コンテナイメージがコンテナリポジトリに登録されます。そしてアプリケーションとは異なるGitリポジトリにあるマニフェストが変更されることで、Argo CDはそのマニフェストをk8sクラスタに適用して更新が反映されます。

本番デプロイまでの具体的なフロー

ここまで、GitOpsの考えにに基づく一般的なArgo CDによるデプロイのフローを説明しましたが、私たちが用いているデプロイフローもほとんどそれに従っています。 具体的にはアプリケーション用のGitリポジトリは各MSごとに複数個、マニフェスト管理用のk8sマニフェストリポジトリは全MS共通で一個存在しており、図にすると以下のようなフローとなります。

deploy flow

図を見れば概ね理解できるかと思いますが、何点か補足します。

  1. CI/CDに関わるブランチは、アプリケーションのリポジトリがfeature、development、masterの3つ、マニフェスト管理用のk8sリポジトリがmasterだけとなっています。
  2. k8sリポジトリは dev/prod/ のように環境ごとにディレクトリが分けられており、GitHub Actionsはデプロイを行う環境に合わせて、それぞれのディレクトリ配下にあるマニフェストファイルのみ変更します。
  3. 各CIの結果最後に行われるk8sリポジトリに対する変更は、Deploymentを記述したマニフェストファイル内のdockerイメージタグを書き換えるようになっています。ECRへプッシュするイメージには毎回コミットハッシュ値を付けているため、マニフェスト内のイメージタグを毎回書き換えることで最新のイメージをPodがプルしてくるようになっています。具体的には最新のk8sリポジトリをクローンした後、以下のようにsedを使用して半ば強引にイメージタグの書き換えを実行しています。
cd "dev/manifests/${MS_NAME}"
for f in "${MS_NAME}"-dev-*.yaml
do
    sed -i -e "s@image: ecr.repo/${MS_NAME}:.*\$@image: ecr.repo/${MS_NAME}-dev:${COMMIT_HASH}@" "${f}"
done

(変数名などは適当です)

  1. 開発環境へのデプロイの流れにおいて、developmentブランチにマージしてからArgo CDによってdevクラスタへデプロイが行われるまでは自動で行われるようになっています。
  2. 一方でdevelopmentブランチから本番環境までの流れでは、master(app)へマージしてからリリースを行い、さらにk8sリポジトリにおいてマニフェストの変更を手動でマージするという、二度手間どころか三度手間とも言えるような状況になっています。これに関しまして、前者は本来master(app)へのマージを行なった段階でステージング環境へのデプロイが走る予定になっているためであり、後者に関しては面倒ではありますが事故が起こる可能性を減らすためあえてこのようにしています。

このフローにおいてデプロイまでにかかる時間は、buildとマニフェストの書き換えが完了するまでがキャッシュを利用した場合で2~3分、Argo CDがマニフェストファイルの変更を検知するまでが最大1分、Podの更新が最大1分程度となっています。また今のところArgo CDの問題によってデプロイが失敗したということは少なくとも本番では発生していません。

jobの実行

多くのシステムにおいては、定常的な運用を回すために自動化された処理(job)を定期実行したいというケースがあると思います。本プロダクトにおいては定時のメール通知や発注といった機能になります。ここではリリース前にjobの実行方法として検討した、k8sのCronJobオブジェクトとairflow(kube-airflow)の2つについて紹介します。 なおリリース時にはairflowを採用しましたが、後述する理由により現在はCronJobオブジェクトを利用しており、またCronJobの一部が失敗してしまった際には、その失敗のリカバリのためのJobオブジェクトをSREが手動でクラスタに適用するオペレーションを取っています。

CronJob

CronJobはその名の通り、Cron形式で記述されたスケジュールに基づいて定期的にjobを作成します。DeploymentやServiceのようにYAMLを書いてクラスタに適用させるだけなので作成も非常に楽です。 ただし特定のjobの実行が終了した後に別のjobを実行させるようなworkflowの制御はできないため、job間に複雑な依存関係が存在する場合や、実行時間が予測できないようなjobがある場合は別の選択肢を検討した方が良いでしょう。私たちも現状ではCronJobで満足していますが、将来的にjobの実行が複雑化することも考慮して別のツールの検討を行なっています。

kube-airflow

kube-airflowはairflowをk8s上で稼働させるためのツール群をまとめたこちらのOSSです。以前からjobの実行フローをスケジュール・管理するツールとしてairflowを使用しており構築・運用のコストが低いと考えていたため、k8sクラスタ内でのjob管理ツールの第一候補と考えていました。 導入に際して、2年ほど本家の更新がなされていなかったため、自分である程度カスタマイズする必要がありました。ここでは役立つかはわかりませんが、私が行なった変更を簡単に箇条書きで説明いたします。なおHelmでインストールも可能ですが、今回は手動によるインストールを行なっています。

  • airflowのバージョンを1.10.7に変更
  • DockerfileにてPython3.7系のインストール
  • ログなどの保存に必要なDBをデフォルトで立ち上がるPostgreSQLのPodから外部のAuroraに変更
  • Gitリポジトリに置かれたDAGファイル(複数のタスクをまとめたもの)を同期するために、本家リポジトリにも同梱されているgit-syncを設定
  • Podを立ち上げる名前空間を default から airflow に変更するため、ClusterRoleとClusterRoleBindingを作成

CronJobに移行した理由

最大の理由はPodのリソースの問題でした。kube-airflowを動かそうとすると、下記の6つのPodが常時立ち上がります。

  • postgres: DAGの情報やログなどを保存するためのDB(実際はAuroraに移行したので使用しませんでした)
  • rabbitmq: クライアントとワーカーの仲介を行うブローカー
  • flower: AirflowにはCeleryという分散タスク・キューのリアルタイム・モニターおよびWeb管理を行う機能があるが、flowerはこのCeleryのクラスタを監視/管理するうためのツール
  • web: GUI
  • scheduler: タスクスケジューラー
  • worker: タスクの実行pod

これらのPodのうちweb・scheduler・workerの3つは特にリソースの消費が大きく、具体的には3つ合わせてCPUは1500m以上、メモリは2Gi以上を常時消費していました。予算に合うようにスモールにクラスタを構築していた身としては、一部のPodのためだけにノードをスケールさせるのは割りに合わなかったためリリース後に急遽CronJobの使用に移行することとなったわけです。

また別の理由として、schedulerやworkerのPodは起動時にRabbitMQのPodと疎通確認を行うのですが、RabbitMQのPodが立ち上がっているのにも関わらず疎通確認に失敗することが開発環境でしばしば発生しており、その原因が突き止められなかったため使用をやめたということもありました。

Datadogを用いた監視・ログ収集基盤の構築

コンテナベースでのシステム運用を開始する以前は、Prometheus+Grafana、あるいはCloudWatchを用いてEC2インスタンスやRDSの監視を行なっていました。いずれも非常に便利なものではありましたが、特にPrometheusはクエリの記述やアラートの設定がなかなか面倒だと感じており、リリースまであまり時間的余裕がない中であらためて導入から設定までを行うのは難しいと考えていました。そこでPrometheusの使用は潔く諦めて、クラウド環境において十分に信頼が置ける監視アプリケーションサービスであるDatadogを導入しようという運びとなりました。(前職等で使用経験のある方が社内にいたのも採用理由の一つです)

Datadogが提供する機能の中で私たちが主に利用しているのは以下の5つです。ダッシュボードの構築やSLOの設定などはまだあまり進んでいません。

  • インフラ監視
  • ログ収集
  • アラート
  • 外形監視(Synthetics)
  • ネットワーク監視

(APMは使用せずX-Rayを使用しています)

これらの機能は一通り触って使用していますが、特にネットワーク監視などは十分に使い倒せているわけではないので、後ほど知見が溜まってきたら別記事で説明できたらと思います。ですから、ここではクラスタへのDatadogエージェントの導入と、Datadogのメトリクスを使用したHPAについて記述します。

エージェントの導入

Datadogエージェントをk8sクラスタに導入するのは、チュートリアルに従えば簡単にできます。手順通りにインストールすればDaemonSetとしてエージェントが各ノードに配置されるはずです。このときデフォルトではログ収集やAPMの機能はoffになっていますので、用途に合わせてonにしておく必要があります。

各ノードに配置されたエージェントは、以下の模式図が示すようにクラスタからメトリクスを収集しています。

kubernetes diagrams after 181012v2 https://www.datadoghq.com/ja/blog/datadog-cluster-agent/ より引用

各エージェントは自分と同じノード上で動作しているkubeletからノードレベルのデータを、マスターノード上のAPIサーバーからクラスタレベルの必要な各種メトリクスを収集しますが、この方法ではクラスタのサイズが大きくなるにつれてAPIサーバーとetcdの負荷が増大してしまいます。

この問題の解決策として、下図のようにDatadog Cluster Agentを導入する方法があります。

kubernetes diagrams before 181012v3 https://www.datadoghq.com/ja/blog/datadog-cluster-agent/ より引用

Datadog Cluster Agentは各エージェントとAPIサーバーの間に立ち、双方の通信のプロキシとして機能します。これによりAPIサーバーへの負荷の集中が回避され、またクラスタレベルのデータの収集はCluster Agentが担うことになり、各エージェントはkubeletからノードレベルのデータを収集する役割に集中させることができます。

Cluster Agentを導入することによるさらなるメリットとして、External Metrics Providerインタフェースを利用できるという点があります。このインタフェースでは、Datadogアカウントからk8sクラスタにメトリクスを公開できる、つまり各エージェントが収集しているリアルタイムのデータをk8sクラスタのHPA機能に対して完全に自動化した方法で利用できるようになります。(具体的には次節で説明します)

このようにCluster Agentは基本的に初めから導入しておくべきだと思うのですが、私たちがDatadogのサインアップを行なった直後(2020年2月)に提示されたセットアップチュートリアルでは、なぜか手作業(自分でkubectlコマンドを叩く方法)でエージェントを各ノードにインストールする説明がなされていました。もちろんCluster Agentの導入はありません。 今はどのような説明になっているかわかりませんが、手作業でやるよりはHelmを利用してCluster Agentを含め一度にインストールするのが最適だと思います。 また因みにですが、既にエージェントがインストールされた状態で新たにCluster Agentを導入しようとしている場合も手作業ではなくHelmでインストールすることをお勧めします。というのも、私が試したときはノードの各エージェントとCluster Agentの通信がうまくいかないエラーが発生し深い泥沼にハマるとことがありました。(結局このエラーの原因は不明のままです)

HPAの設定

先に述べたように、Cluster Agentを有効にするとDatadog側で定義したメトリクスを用いてHPAを作成できます。以下にその一例を示します。

apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: example-autoscaler
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: example-deployment
  minReplicas: 2
  maxReplicas: 5
  metrics:
  - type: External
    external:
      metricName: example.http.requests
      metricSelector:
        matchLabels:
          key: value
      targetAverageValue: 100

この例では、exmaple という名前空間の example-deployment という名前のDeploymentから生成されるPodに関するHPAを作成しています。minReplicasmaxReplicas でPodの個数の最大値最小値を設定し、特定のラベルを持った examaple.http.requests という名前のメトリクスの値をオートスケールの基準としています。この例では targetAverageValue = 100 としていますが、スケールの判断は以下の式で行われるため、たとえば現在のPodのレプリカ数(currentReplicas)が2、example.http.requests の値(currentMetricValue)が200の場合、スケール後のPodのレプリカ(desiredReplicas)は ceil[2 * (200 / 100)] = 4個 となります。(desiredMetricValue = targetAverageValue です)

desiredReplicas = ceil[currentReplicas * ( currentMetricValue / desiredMetricValue )]

その他のTips

開発環境の自動起動・破壊

開発環境やQA環境は、開発者が開発を行いその動作確認を行う時間帯以外は立ち上げておく必要がないため、毎朝自動で起動され、深夜にまっさらな状態に自動で破壊されるようにしています。 起動・破壊スクリプトはkubectlとterraformがインストールされたEC2インスタンスからcronで実行していますが、EKSクラスタの作成等にはかなり高レベルの権限が必要となるのでセキュリティには十分気をつけなければなりません。

起動スクリプトでは主に terraform apply によるリソースの作成とCloudflare APIを用いたDNSレコードの登録を、破壊スクリプトでは kubectl delete -f によるALBの破壊と terraform destroy によるそれ以外のリソースの破壊を行なっています。

工夫を施したのは次の2点です。 1. リソースの作成・削除の合間には十分なsleepを設ける 2. 作成に時間を要するDBとRedisは作り直さず、VPCとサブネットも再作成しない

1については最初から考慮してはいたのですが、何度か次のような事故が起きてしまいました。人によって出勤時間に差がある場合、開発環境が立ち上がっていないと叩き起こされて非常に焦る羽目になるので念入りに調整しましょう。

  • ALBを破壊する際にsleepが不十分だったために一部が破壊できずに残ってしまい、その後の terraform destroy の際に、残存してしまったALBにアタッチされたセキュリティグループとの依存関係によってVPCが削除されることなくタイムアウトしてしまう。
  • 作成時、ノードが立ち上がりきる前に次の手順に移行してしまったことでArgo CDのPodが作成されず、すべてのMSのPodが作成されなくなる。

2に関して、AWSのリソースはterraformで管理することにしていますが、DBやRedisは作成にそれなりに時間がかかるため再作成はしない方針を取っています。これらは停止させておくことで課金を回避できるのでコスト面の心配もないというのも理由の一つです。(正確に言うと、Elasticacheのノードは停止させておくことができないので、Redisに関してはEC2インスタンス上に構築している場合に限ります)したがって、全体構成の章で述べたようにこれらはそれぞれ独立したVPC上で作成しているため、これらが位置するVPCやサブネット、あとはセキュリティグループもterraformによる再作成の対象としていません。一方で、VPCピアリングを都度破壊している関係上、サブネットのルートテーブルは再作成の対象となっています。

Argo CDの設定のバックアップ

開発環境の自動起動を行う上でもう一つ大事な要素がArgo CDの設定のバックアップです。基本的にArgo CDはGUI上でリポジトリを読み込む設定を行いますので、設定が保存されたバックアップがないと環境を作り直すたびにGUI上でポチポチと再設定をする羽目になります。また自動起動の際に限らず、基本的に作り直すことのない本番環境であっても、設定のバックアップを取得しておくことは重要です。

バックアップの取得方法はこちらの公式のドキュメントでDisaster Recoveryのための方法として紹介されています。 1つ注意点として、クラスタ内に作成するArgo CDのマニフェストのイメージが古いと、バックアップを取得するためのコンテナがawscliを含まないためエラーが出てしまいます。特にArgo CDの古い紹介記事などからinstall.ymlを入手するとイメージのバージョンが古いため、罠に引っかかってしまいます。(引っかかりました)

デプロイ時の瞬断への対策

特に対策をしていないと、デプロイでPodが入れ替わる際に通信の瞬断が発生することがあります。この事象に対して、readiness probe、ローリングアップデート、lifecycleの設定といった3つの対策を取り入れています。いずれもk8sに携わったことがある方にとっては有名な対策かと思いますが、私たちがどのように設定しているのかを示しておきます。

readiness probe

これはPodに関する非常によく知られた機能で、Podがリクエストを処理可能かどうかをチェックするためのものです。基本的にリクエストを受け付けるPodにはすべて記述しており、内容はとりあえずliveness probeと同じものとしています。

ローリングアップデート

こちらもk8sに限らず有名なデプロイ方法ですが、私たちは以下の方法で実現しています。(おそらく最もスタンダードな方法だと思います)maxSurge はローリングアップデート時に希望するレプリカ数より何台多くPodを作成してよいかを示しており、下の例では 5 + 100 = 105台 までデプロイ時にPodが立てられます。また maxUnavailable はローリングアップデート時に最大何台Podを減らして良いかを表していて、下の例では replicas の値から1台も減らしてはいけないという意味になります。つまり下の例では常に5台以上を維持しながらデプロイを進めることができます。

spec:
  replicas: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 100
      maxUnavailable: 0

lifecycleの設定

リリース直後は上の2つを対策として取り入れていましたが、それでも1日に何度か瞬断が発生していました。そこで新たに追加したのが下記のlifecycleの設定です。これを設定することにより瞬断の発生はほとんど0になりました。

簡単に説明すると、この設定はPodが削除されるとき初めにsleepを入れることで、「Podへのリクエストの遮断 -> Podの停止」という順番を保証しています。通常はこの2つの事象が同時に発生し、どちらが先に実行されるかが不明なため、先にPodが停止した場合に瞬断が起きてしまいます。

spec:
  template:
  ~ (省略) ~
    spec:
    ~ (省略) ~
      containers:
      - name: example
      ~ (省略) ~
        lifecycle:
          preStop:
            exec:
              command: ["sh", "-c", "sleep 5"]

Podの停止時の挙動に関して非常にわかりやすい記事を参考として載せておきます。 - 参考:https://qiita.com/superbrothers/items/3ac78daba3560ea406b2

現在の課題

運用を開始して2か月経過した現時点で抱えている課題をいくつかご紹介します。今からだと改善が厳しく、構築当時の自分の対応をかなり後悔しているものもありますので、ぜひ反面教師としていただけると幸いです。

YAMLやterraformの管理

マニフェストファイルはHelmやKustomizeを用いてYAMLを管理することで環境ごとの差分を吸収し、terraformはテンプレートやモジュールを使ってなるべく共通化を図るべきべきだと考えています。しかしリリースまでスピード重視で駆け抜けたため、現在これらはまったく達成されていません。具体的に述べると、マニフェストは環境ごとに(ほとんどコピペですが)1から作成しており、terraformの方も悪いとは言いませんが環境ごとにすべてのファイルを作成しています。こうした状況の弊害は既に現れており、リリース後にQA環境を作成した際は、大量のマニフェストファイルとクラスタ構築用のterraformファイルの作成で心が折れそうでした。環境を新たに追加するという作業はレアケースかもしれませんが、今後少なくともステージング環境を作る予定は見えているので、早くもモチベーションが下がってきています。

今後の対応ですが、マニフェストの方は既に対応は厳しいと考えています。というのも、k8sリポジトリに設置されたマニフェストファイル群は既にデプロイフローに深く組み込まれており、デプロイを数日止めて変更するのは現実的ではないと考えているためです。YAML管理ツールは導入しておくに越したことはないので、何を使うかどうかは運用が始まる前に早めに決めてしまいましょう。

terraformに関して、こちらは頻繁に使用されているわけではなく、また開発環境やQA環境に関しては最悪事故を起こしてしまっても問題ないため、徐々にリファクタリングを進めています。HCLの記述はなかなか奥が深く習得に時間がかかりそうですが、こちらは改善の可能性が十分にあるといった現状です。

本番環境でのスポットインスタンスの利用

Managed Node Groupsのところでも述べましたが、スポットインスタンスの使用がまだ実装されていないため、本番環境のノードはすべてオンデマンドで稼働しています。もちろん早くスポットインスタンスが利用できるようになれば最高ですがまだ時間がかかりそうなので、Managed Node Groupsと、スポットインスタンスを利用した通常のASGをハイブリッドしたノード群を自前で用意しようかと考えています。あまり聞いたことのない使い方なのでそれなりの検証を要するかと思いますが、AffinityやTaintなどの機能を駆使すれば実現可能であり、本番環境でも特に支障なくスポットインスタンスを利用できるようになるのではと踏んでいます。

まとめ

後半はかなり雑多な内容となってしまいましたが、全体的には構築フェーズ・運用フェーズの双方における苦労した点、工夫した点を説明できたと思います。まだまだ至らない部分や改善できる点が多々ありますが、コンテナやk8sが全盛の時代に一から構築・運用を経験できたのは非常に自分の成長に繋がっているなと感じています。

いまだに日々試行錯誤の連続ですが、今後は運用の自動化や構築速度の代わりに犠牲にしてしまった諸々の補完、さらにサービスのスケールへの事前準備や新技術の導入検討(たとえばIstioなど)をコツコツと進めていきたいと考えています。

最新の記事