12 Factor App on Kubernetes を12ヶ月実践して見えてきたもの

12 Factor App on Kubernetes を12ヶ月実践して見えてきたもの

Kubernetesを使って12 Factor Appの実装、運用を実践した結果得られた知見を12Factorの各項目別に紹介します。

A3fabd96e2ca4bee5b0333ff2a640f01?s=128

translucens

July 27, 2020
Tweet

Transcript

  1. 12 Factor App on Kubernetes を 12ヶ月実践して見えてきたもの 2020-07-27 @translucens 1

    ※背景写真は2018年のものです
  2. これの後日談です https://speakerdeck.com/translucens/the-twelve-factor-apptokubernete s-f79ad903-297d-44fb-901f-09c3b5ba9454 2

  3. 12Factor Appとは? 概要をさらっと見ましょう

  4. The Twelve-Factor App • HerokuのFounder & CTO, Adam Wiggins氏らによる アプリケーションの設計論

    – 「Twelve-Factorの方法論は、どのようなプログラミング言語で書かれたアプリ ケーションにでも適用できる」とある通り、具体的な実装例と言うよりは抽象 的、方向性を示すもの • 実際の構成で12個の項目を具体化した例を紹介 4
  5. Beyond the 12 Factor App Pivotal社のadvisory solutions architect Kevin Hoffman氏による、

    12factor appの新たな解釈、項目の追加 追加された項目は下記の3点 • 2. API First • 14. Telemetry • 15. Authentication and Authorization 以降、単にBeyondとよぶ 5
  6. 背景 自己紹介や部署の状況など

  7. 社内向けに複数サービスサイトの利用状況を 集約、分析、活用する 主な利用者は社内であるが、 活用の一つの形態として公開APIサーバもある 部署の立ち位置 エンドユーザが使うサービスサイト そのものはスコープ外 7

  8. 基本的な方針 • 独自実装、拡張は最小限に – マネージドサービスやサポートの利用 – 作って終わりではなく、運用する期間の方が長い – 世間に公開されている技術情報を見れば分かるようにしたい •

    コストに対して十分に恩恵の得られる技術の採用 – 金銭面のほか学習、運用コストも考慮に入れる • 「◦◦ツールを入れればDevOps!アジャイル!クラウドネイティブ!」みたいなことはない – ベンダーではないので、最先端技術自体では収益化できない – 具体的にはサービスメッシュは計画から外した • 社内の規則類がそれなりに存在 – 交渉するか実装をハックして回避するかのバランス… 8
  9. 具体的な構成要素 Kubernetesクラスタ: Kubernetes Engine (GKE) • DWHがBigQueryにあり、同一クラウドの方が認証が楽 公開APIサーバ: Cloud Run

    9 Kubernetes Engine プログラミング言語: Go • 型サポート、単一のバイナリができる扱いやすさ インフラ構成管理: Terraform • GCP公式ツール(Deployment Manager)より事例が多い Cloud Run
  10. GKEとCloud Runの使い分け 社内・公開用で要件が異なるので、GKEで作り込むのではなく 公開APIサーバをCloud Run(knative互換のサーバーレスサービス)に切り出すことにした。 10 社内向け 公開 ネットワーク要件 専用線対応や各種規則の遵守

    インターネットへ公開 アクセスパターン 穏やか イベントによりスパイク(数百倍の幅) データ保護 社外秘データあり 公開可能なデータのみ
  11. Cloud Runの導入で得たもの GKEやCloud Functionsであれば検討、実装が必要なことが省略できた • ノードのオートスケーリングのチューニング – イベント日程を取り込んで事前にスケールアウトする機構 – たまにTVに取り上げられて予想外に増えることもある

    • 社内用サービスとの分離 – GCPのプロジェクト単位で分けてあるので、ネットワーク的にも全く別 • 開発環境や、深夜のアイドルVMの費用がほとんどなくなった – アクセスがなければ課金されないので • RDBアクセスの軽減 – 1コンテナで80アクセスまで同時対応可能なため接続数が抑えられた 11
  12. それでは12+3項目を見ていきましょう

  13. 12+3Factorまとめ 13 項目 使用したリソース、ツール、ライブラリ どう美味しいの? I. コードベース FluxCD 頻繁なYAML書き換えからの解放 II.

    依存関係 Distroless Dockerイメージの縮小 脆弱性検知の偽陽性の削減 III. 設定 ConfigMap, Secret, Workload Identity Secret Manager, caarlos0/env 環境別にビルドし直さずに済む 機密情報自体の削減 IV. バックエンドサービス 各種マネージドストレージ コンピュートとストレージの分離 V. ビルド、リリース、実行 Docker, Kubernetes全般 イミュータブル、「謎」のVM化を抑止 VI. プロセス Pod Security Policyの ReadOnlyRootFilesystem イミュータブルになる セキュリティ向上 VII. ポートバインディング Service, Deployment サービス間が疎結合に なにぶん数が多いので、はまり ポイントとか工夫した点のある項 目に絞ります。
  14. 12+3Factorまとめ 14 項目 使用したリソース、ツール、ライブラリ どう美味しいの? VIII. 並行性 Cloud Run 0から必要な数までスケール可能

    IX. 廃棄容易性 Dockerのマルチステージビルド Distroless Preemptible VM 必要なときに素早く起動できる→スケール完了までのア クセスに耐えるマージンを削れる VMコストの低減 X. 開発/本番一致 Kustomize, FluxCD 共通の設定と環境差分の分離 XI. ログ uber-go/zap, tommy351/zap-stackdriver アプリがログを出した後のことを考慮せずに活用できる XII. 管理プロセス Job アプリ本体からの分離 2. API First OpenAPI Specification 3.0 見やすいAPI定義ページ、IFコードの生成 14. テレメトリ OpenCensus, Cloud Trace マネージドサービスのライブラリも含めたトレース記録 15. 認証・認可 Keycloak, Louketo Proxy APIサーバにOpenID Connectを意識させず実装
  15. I. コードベース 「バージョン管理されている1つのコードベースと複数のデプロイ」 具体的には? • Single source of truthとなるGitリポジトリのブランチ戦略を決める –

    本番用、試験用、開発用…はブランチで分離し、リポジトリは分けない – NG: いくつものリポジトリからファイルを集めないとビルドできない状態 – NG (Beyond): メインのアプリと密結合なワーカーを分ける • Twelve-Factor Appではアプリをスコープにしているが、 インフラやクラスタにも適用可能 – Kubernetesのマニフェスト: GitOps(Kubernetes meetup #21のテーマ) – Terraformの適用: Atlantis, Terraform Enterprise 15
  16. • GitHub Flowを選択 – Mainのコミットと1:1に対応するDockerイメージをビルド – DockerイメージにはGitのコミットハッシュのタグを付ける – リリース候補になったイメージには セマンティックバージョニングによるタグも付ける

    16 Image credit: Git by Jason Long https://git-scm.com/downloads/logos (CC BY 3.0) アプリのブランチ戦略 1a2b3c4 1a2b3c4 5d6e7f8 5d6e7f8 2.0.0 2.0.0
  17. • GitLab Flowの環境別ブランチを選択 – GitFlowほど複雑でなく、各環境の状態が追いやすい – 本番用と開発用で2つGCPプロジェクトとクラスタがあるのでブランチも2つ 17 インフラのブランチ戦略

  18. 開発クラスタ GitOpsによるデプロイ ※ここで言うGitOpsは CIOpsと対比される狭義の意味 18 Container Registry 本番クラスタ マニフェスト 更新確認

    kubectl apply 更新確認 タグの更新 更新確認 kubectl apply CDツールはFluxCDを使い、次のように環境別にデプロイされるようにしている • 本番クラスタ: Gitのメインブランチ • 開発クラスタ: Gitの開発ブランチ ◦ イメージレジストリの監視機能を使い、条件に合うイメージを使うようにマニフェストを更新 ◦ ステージングネームスペース: セマンティックバージョニングの最新イメージ ◦ 開発ネームスペース: 最新のイメージを追従
  19. FluxCDによるデプロイの実際 良い点 • CIの責務はDockerイメージをビルド するところまでなので単純 • Kubernetes APIをグローバルに公開 しないようにできる •

    デプロイツールからGitへのネットワー ク経路を固定できる • 開発版イメージのYAMLの書き換えを しなくて済む 19 課題 • ワークフローを必要とするデプロイ (例: DBをマイグレーションしてからア プリを更新)、エラー通知は対応しな い – Flux v2, toolboxに期待 – エラーはPrometheus, Alertmanager と併用して検出、通知することはでき る • FluxCDがアノテーションを付けるので kubectl diffの差分が残る
  20. II. 依存関係 「依存関係を明示的に宣言し分離する」 • Goのライブラリ: Go modulesを使い依存関係を管理 • それ以外のCLIツール等: Dockerfileに記述、暗黙の依存を防止

    – Dockerをパッケージングツールとして扱う – ディストリビューション由来のパッケージに暗黙のうちに依存したくない • 脆弱性スキャンツールの偽陽性を減らしたい意図もある 20
  21. Dockerベースイメージあれこれ • Distrolessのbase版 にした – ルート証明書やタイムゾーンファイルは入っている – static版に加え、glibcやOpenSSLが入っている その他試したもの •

    scratch – 「Goはシングルバイナリだから」と素朴に考えた時間が僅かにあった – GCPのHTTPS APIアクセスにルート証明書が必要なので(パッケージマネー ジャを使わずにコピーしてくる)必要があり、手間がかかる • alpine – glibcでなくmuslなので互換性の懸念が残る 21
  22. III. 設定 「設定を環境変数に格納する」 Beyond「設定と認証情報を区別すべき」「専用の構成管理サーバも検討」 • Kubernetesの専用リソースかDeploymentに記述 – ConfigMap – Secret

    – Deployment > spec > template > spec > containers > (コンテナ) > env • caarlos0/env を使って設定を保持する構造体へマッピング – Go組み込みの数値型の他、time.Durationやurl.URLへの変換が行える – デフォルト値の設定も可能 22
  23. Secretの扱い Secretの内容をリポジトリに含めないことを優先し、完全なGitOps化は中断 • 外部のAPIキー等 – GCPのSecret Managerに格納 • クラウドの認証キー →シークレットを生成しない認証:

    – GKEでは Workload Identity でGCPのService accountとKubernetesのService accountをリンクすることができる • Pod起動直後(実測値: 2, 3秒)は認証できない制約がある – initContainerでsleepするなりリトライするなりする – 最近のGoクライアントライブラリでは、この認証失敗でエラー終了しなくなった • 単にランダムであればよいもの(セッションの暗号化キー等) – Terraformでランダム文字列生成→TerraformからSecretに設定している 23
  24. IV. バックエンドサービス 「バックエンドサービスをアタッチされたリソースとして扱う」 Beyond「ローカルディスクはバックエンドサービスではない」 • 外部の依存するもの(ストレージ、API…)と疎結合にして コードを変更せずに切り替えられるようにしておく • 例えばRDBにMySQLを使う場合 –

    III. に従うとDBの接続情報は環境変数に格納 – 単に環境変数を変えて、開発時はローカルのDockerコンテナ、試験や本番では マネージドのMySQLインスタンスへ接続できるように 24
  25. ステージ 誰が 何を 設計 開発者 依存関係の表現方法を決める ビルド CIサービス (Cloud Build)

    • Goのソースをビルドする • Dockerイメージを作る リリース GitOpsツール (Flux) • リリース(環境設定とビルドされたものの組。 KubernetesではDeploymentに相当する)を更新 実行 Scheduler kubelet • Deploymentの記述に沿ってコンテナを起動する V. ビルド、リリース、実行 「ビルド、リリース、実行の3つのステージを厳密に分離する」 Beyond「設計フェーズを追加」 25
  26. VI. プロセス 「アプリケーションを1つもしくは複数のステートレスなプロセスとして実行する」 • 複数のプロセスが密接に連携する場合: 各プロセスごとにDockerイメージ を作り、Podに複数のコンテナを指定する – Design patterns

    for container-based distributed systems でいうSidecar等 – Louketo(旧Gatekeeper)によりOpenID Connect認証を実現した 26
  27. VI. プロセス • ステートレス:コンテナが動いているVMのメモリやディスクに永続的な データを保存しない。ファイルはCloud Storageに置く。 – 明示的に指定したパス以外にファイルを置いてしまわないよう、 Pod Security

    PolicyのReadOnlyRootFilesystemをデフォルトでtrueにした Beyondではステートレスの定義について補足されている • 実行時に生成したローカルファイルへ依存しない • 起動時に過度のキャッシュを行わないことを勧めている 27
  28. VII. ポートバインディング 「ポートバインディングを通してサービスを公開する」 • 外部のアプリケーションサーバに依存しなくてもポートを 公開できること。Goの場合アプリケーションサーバを 必要としないので、自動的に満たす • Kubernetesでは、Deploymentの spec

    > template > spec > containers > (コンテナ) > ports で公開するポートを宣言できる – 仮想LBのServiceリソースと組み合わせて公開する 28 III. 接続先は 環境変数に VII. ポートを公開
  29. VIII. 並行性 「プロセスモデルによってスケールアウトする」 • 一つのプロセスを前提として、その内でスレッドを増やしてスケールす ると、VMのスペック以上にスケールアップできなくなる – KubernetesではDeploymentの spec >

    replicas でPodの数を指定できる 29
  30. IX. 廃棄容易性 「高速な起動とグレースフルシャットダウンで堅牢性を最大化する」 • 今コンテナが起動していないVMでの起動も含む – Dockerイメージサイズが小さい(=ダウンロードが早く済む)のがよい – Dockerのマルチステージビルドを使って、実行時に不必要なファイル(コンパ イラやgitクライアント、エディタ…)が含まれないようにする

    – Distrolessの採用 30
  31. IX. 廃棄容易性(続き) • 仮に突然終了したとしてもデータがロストしないような設計 – 非同期処理のキューイングにCloud Pub/Sub (pull)を使い、メッセージを送っ たワーカーからの応答がしばらくなければ、他のワーカーへ メッセージが再送されるように

    31 Cloud Pub/Sub Pub/SubではメッセージのACKを期限内に送る必要がある。 ワーカーが生きていると、SDKが自動的にACKの期限延長 リクエストを送信する=ハートビートの役割を果たす
  32. Preemptible VMで廃棄容易性を検証する 開発環境のノードの大半を安いプリエンプティブルVMにする • 簡易カオスエンジニアリング – 最大24時間でノードが再生成されるので、Podの状態はリセットされる – CronJobによる定期的なE2Eテストの実行が成功し続けることを確認する •

    開発環境のコストが劇的に下がる(約80%off) 32
  33. X. 開発/本番一致 「開発、ステージング、本番環境をできるだけ一致させた状態を保つ」 • 時間・担当者・ツール(ミドルウェア)を近づける – OSSのRDB: Dockerを使えばをローカルで起動できる – Kubernetes環境:

    minikubeを使って一式作ろうとしたが、minikube 特有の不具合にはまり、使い慣れたComposeにした – 独自仕様のマネージドサービス: Pub/SubやBigQuery • エミュレータがなかったり、あっても一部機能が提供されていない ことがある 33
  34. KustomizeとFluxCDの併用 FluxCDのGenerate機能経由でKustomizeを使うと、Kustomizeが生成したマニフェスト は隠蔽される Helmから生成したマニフェストにJSON Patchを当てるようになってから、 差分を追いづらくなってしまった。 Kustomizeが生成したマニフェストを格納する中間リポジトリを作って解消したい。 34 base overlay

    flux-patch.yaml 環境別マニフェスト Kustomize kustomize.yaml
  35. XI. ログ 「ログをイベントストリームとして扱う」 • アプリの責務は標準出力に書き出すところまで • 下記のロガーを使用 – uber-go/zap •

    構造化されたログを出力することができる – tommy351/zap-stackdriver • zapのメソッドとStackdriverのログレベルを対応させるために使用 • Cloud Loggingが裏でいろいろしてくれる – BigQueryへログをコピーしてBigQueryの強力な検索性能を使える – コンテナ名等のメタデータをログに付与 • コンテナごとのエラーレートを集計し、アラートを設定 35
  36. XII. 管理プロセス 「管理タスクを1回限りのプロセスとして実行する」 Beyond「管理プロセスの性質を考慮して個別の方法を採るべき」 • 管理タスクの例: DBスキーマのマイグレーション – golang-migrate/migrateを使っている •

    Kubernetesでのサポート – Jobリソース • 一度だけ起動するPod 36
  37. 2. API First / Beyondのみ 「APIをまず定義することで顧客やAPIの使用者との議論がしやすくなる」 「API定義からドキュメントやモックを生成するツールを導入する」 • API Blueprintが紹介されている

    • OpenAPI Specificationを採用した – CIの中でSwagger-UIのイメージも作ってアプリと同時に更新されるように 37
  38. 14. テレメトリ Beyondのみの項目。3種に分けて考えることが推奨されている • APM • ドメイン固有 • ヘルスチェック、システムログ トレースの取得はOpenCensusとCloud

    Traceの組み合わせで実現 GCPのGoクライアントライブラリは内部処理についてもトレースが実装され ており、トレーシングを有効にするだけで記録することができる 38 Trace
  39. Readiness ProbeのReadyについて 公式ドキュメントのReadiness Probeの説明(※)を見ると、外部の依存サービス(例 えばDB)の可用性をチェックするのが良さそうである。 ※ 例えば、アプリケーションは起動時に大きなデータまたは構成ファイルを読み込む必要があ る場合や、起動後に外部サービスに依存している場合があります 一方で、DB側で障害が起きると、全てのPodのReadiness Probeが通らなくなるの

    で、クライアント側へエラー応答をすることもできなくなる。 39
  40. Readiness ProbeのReadyについて • APIサーバ – 一部はDBとキューを使う – 一部はDBのみ使う 最初はDBとキューの両方の可用性をチェックしていた キューが使えなくなると、機能BやCのように、キューが不要な

    機能も巻き添えになって無応答になるのでチェック対象から外 した (キューが利用できなくなるのはGKE側の不具合で解消済) 40 DB キュー 機能A 要 要 機能B 要 不要 機能C 要 不要
  41. 15. 認証・認可 Beyondのみ 「オリジナルにはセキュリティに関する記載がないため追加した」 「RBACによりアプリを保護する」 Keycloakとプロキシの併用により、アプリ側にはOpenID Connectの詳細 を意識させずにRBACを実装した 41 Louketo

    Proxy App 認可済リクエスト 未認証 社内認証
  42. その他コンテナのセキュリティ • パッケージ、ライブラリ脆弱性スキャン – TrivyをCronJobで毎日回している • GCRのイメージをスキャンする場合、Workload Identityの仕様に引っかかるので InitContainerにsleepを数秒入れる必要がある –

    RenovateもCronJobで毎日回している • 動的な異常検知: Falco – Cloud Native Days Tokyoの発表用に検証したきりになっている… 42
  43. 付録: Cloud Runでの12FactorApp

  44. #1 - 6 44 Kubernetes版との差異 I. コードベース Dockerイメージをビルドする点では同一 II. 依存関係

    差異はなし III. 設定 環境変数は設定可能だが、コンソールから値が見えるため、機密情報はSecret Managerに保存 IV. バックエンドサービス 当初はRedisへの接続ができなかったが、Serverless VPC Accessのサポート でできるようになった V. ビルド、リリース、実行 GitOpsはできない VI. プロセス ファイルシステムは揮発性なのでステートレスであることが必須
  45. #7 -12 45 Kubernetes版との差異 VII. ポートバインディング 環境変数PORT宛にリクエストが送られてくるのでそれで待ち受ける VIII. 並行性 コンテナあたりのリクエスト数は最大80までで指定可能

    IX. 廃棄容易性 レスポンスを返した後のCPUの割り当ては絞られるので、レスポンスを返しき るまでに必要な処理は済ませてしまう方が良い X. 開発/本番一致 Borgをローカルには作れないという点では差異が残る XI. ログ 構造化されたログを使ってログレベル”severity”を指定しないと、標準エラー出 力からもログレベルはDefaultになる XII. 管理プロセス Jobのような機構はないので別の環境で実施する
  46. Fin. 46

  47. 47 Q&A • Terraformで、クラスターのスケールやバージョンアップもやっているのですか? – はい。予期しない事象がないよう、開発環境と本番環境のGKEを交互にバージョンアップするような運用にしています。 • ComposeとGKE(Kubernetes)の両方をサービス開発者が覚えるのは大変そうだと思ったのですが、習得を助けるための 工夫はなにかしていますでしょうか? –

    それぞれテンプレを配布するくらいで、劇的に効果のあるやり方は見つかっていないです… • プリエンプティブル VM を使いだしてアプリの設計について実際に得た気づきはありましたか? – 冪等な処理の重要性や、全体として冪等であっても再実行には時間がかかるので、いくつかのステージに分割して状 態を保存し再実行の範囲を最小限にするようにしています。 • cronjobでe2eテストってかなり興味深いですが、例えばリリースとかでコンテナの生え変わりのタイミングがドンピシャで来て エラーになったりとかないもんなんですか? – 今は破壊的な変更に伴うエラーは減っていますが、開発者が更新しない早朝にテストを実行するようにして偽陽性を削 減しました。 • メッセージキューイングってマネージドでなくてもわりと開発環境を用意するのがめんどくさいコンポーネントだと思うのです が、工夫した点などあればご教授ください。 – 運良くPub/Subはgcloudコマンドにエミュレータが提供されていたのでそれを使いました。 キュー経由でメッセージを送らないとローカルでのテストができないのは不便なので、簡単なレイヤードアーキテクチャ を使ってキューからメッセージを取り出す部分を分離しています。