DMM.comの、一番深くておもしろいトコロ。

社内で提供しているマイクロサービスの参考実装について

社内で提供しているマイクロサービスの参考実装について

  • このエントリーをはてなブックマークに追加

Calendar for DMMグループ Advent Calendar 2021 | Advent Calendar 2021 - Qiita 17日目の記事です。

こんにちは、DMM.comに2021年新卒として入社した工藤 純です。
現在はプラットフォーム事業本部のマイクロサービスアーキテクトチームにてSREエンジニアとして働いています。

マイクロサービスアーキテクトチームの他の取り組みに関しては、是非本アドベントカレンダー5日目のpospomeさんの記事をご覧ください。

inside.dmm.com

マイクロサービスアーキテクトチームでは、Kubernetesを中心としたアプリケーションプラットフォームの構築・運用を行っています。

弊チームでは、マイクロサービスプラットフォームの開発効率を向上させるために参考実装を社内に公開しており、この参考実装をマイクロサービステンプレートと呼んでいます。 この記事では、マイクロサービステンプレート開発・運用の軌跡について述べていきます。

マイクロサービステンプレートとは?

マイクロサービステンプレート提供の背景

アプリケーションをマイクロサービスプラットフォーム(= Kubernetesクラスタ)に乗せるには、ただ単にアプリケーションを実装する以外の工程も必要になります。
アプリケーションをコンテナ化し、Kubernetesマニフェストファイルを用意しデプロイを行うことで、やっとマイクロサービスプラットフォームにアプリケーションを乗せることができます。また、その過程でCI/CDパイプラインの整備なども発生します。

弊チームではマイクロサービスプラットフォームにアプリケーションを乗せるまでのサポートを実施しています。
DMMプラットフォームでは現状各チームのテクノロジースタックが多種多様なため、それを統一するための施策も進めています。こちらに関しては上記pospomeさんの記事で解説されています。 マイクロサービスプラットフォーム利用者は、その都合でKubernetesだけでなくGolangに初めて触れるチームが存在します。

そのため、今後マイクロサービスプラットフォームにアプリケーションを乗せてもらう度に付きっきりでサポートをすることは、人手の足りなさもあり現実的に厳しい面がありました。 以上の背景から、各チームのキャッチアップコストを軽減し開発効率を向上させるために、各チームの参考とするマイクロサービステンプレートを開発しました。

マイクロサービステンプレートの全体像を以下の図で示しています。 マイクロサービスプラットフォームにアプリケーションをデプロイするまでのフローに関連したリソース + Datadog Dashboard/Monitorの構成になっています。 実稼働させるようなアプリケーションと同様にリソースが配置されており、開発フローの全容とその実装を把握することができるようになっています。

f:id:dmmadcale2021:20211206115939p:plain
マイクロサービステンプレートの全体像

おおまかに登場人物をまとめると以下になります。マイクロサービスプラットフォームとしてGKEクラスタとEKSクラスタが存在する都合で、GKE用とEKS用のマイクロサービステンプレートがあります。 今回はマイクロサービステンプレートに関しての記事なのでデプロイフローそのものに関して詳しくは触れません。

① Application ...
アプリケーション実装のリポジトリです。
Golangで実装されていて、ほとんどビジネスロジックを持たないウェブサーバー + バッチ処理の実装になっています。
GitHub ActionsによるイメージのBuild&Pushや、LintやTestの実行などが含まれています。
② Kubernetes Manifest ...
Monorepoで管理されています。
③ Artifact Registry ...
GitHub ActionsでBuildしたイメージのpush先です。
pushしたイメージはデプロイ時に参照されます。
④ Spinnaker ...
CD Pipelineです。
マイクロサービステンプレートの場合はKubernetes Manifestのリリース用のPRをマージするか、Artifact Registryに特定のタグのついた新しいイメージがpushされた際にDeployを実行します。
⑤ Datadog Dashboard/Monitor ...
実運用しているのでDatadogのダッシュボードやモニターも存在します。

また、マイクロサービステンプレートの特徴として、ただの単純な参考実装から一歩踏み込んで、できる限り実アプリケーションに寄せている、という点が挙げられます。 検証用〜本番用まで各環境のクラスタに乗っており、実際に社内ネットワークからはリクエストを投げることが出ます。 また、実際に弊チームで運用を行ってモニターやダッシュボードを用意し、SLOも管理しています。

今回は①Applicationと②Kubernetes Manifestについて、以下で実際の例を記載します。

Application

マイクロサービステンプレートのアプリケーションは、一般的なバックエンドサーバ+バッチ処理のGolang実装になっています。 複雑なビジネスロジックは無く、Datadogでトレースを取る、アクセスログを出力するといった実運用に耐える必要最低限の実装が含まれており、ある意味マイクロサービスプラットフォームにアプリケーションをのせるための最低要件を定義しているものになっています。

また、各チームがつまづきやすいであろうポイントに、コードの各箇所に解説コメントを記載して提供しているのが特徴です。
Golangに触れるのが初めてのチームもいるので、かなり細かくコメントを記載しており、特にコード単体ではよくわからない、実装背景などについてコメントを記載している事が多いです。
実例としては以下のように、アプリケーションをコンテナ化したことのないチームを想定して、Graceful Shutdownの際にSIGTERMをトリガーとすることを推奨するようコメントで解説しつつ実装しています。

... 
    // シグナルをトリガーとしたGraceful Shutdownを行いたいのでアプリケーションサーバーは別のgoroutineで動作させる。
    go func() {
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            logger.Error(fmt.Sprintf("failed to start or close server, err = %s", err.Error()))
            return
        }
    }()

    logger.Info("start server")

    /*
       Graceful Shutdownを行うために、シグナルを受信するための専用チャネルを作成している。
       Kubeletは ポッドのシャットダウンを開始する際にSIGTERMを送信するので
       Kubernetes内のマイクロサービスはSIGTERMをトリガーとしてGraceful Shutdownを実行する形式を推奨する。
   */
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGTERM, os.Interrupt)
    // 上記で設定したシグナルを受信するまで待機する。受信するまで以降のプログラムは実行されない。
    <-quit
    logger.Info("start graceful shutdown")

...

また、アプリケーション実装はGitHub Repositoryで社内公開しているのですが、GitHub Actionsを活用したCIやCDも提供しています。 マイクロサービステンプレート利用者側は、GitHub Actionsの実装を真似するだけで、コンテナイメージ配置用のArtifact Registryにビルドしたイメージをpushすることができます。 ただし、あくまでサンプル実装なので無理にコピーする必要はありません。チームの要件的にCircleCIを使いたいとなれば、CircleCIを使ってもらうことになります。

Kubernetes Manifest

Kubernetes Manifestも、アプリケーション実装と同様にわかりにくいいポイントにコメントを記載して提供をしています。
例えばバッチ処理によく使われるCronJobリソースのマニフェストのspecは実際以下のような形式で提供しています。
特にkube-controller-managerの都合で、スケジュールする際に記載するのはUTCの設定だということは各チームが間違えやすいポイントだと思います。

...    
spec:
  # scheduling time is based on the timezone of the kube-controller-manager.
  # The timezone of our cluster's kube-controller-manager is UTC.
  # The following schedule is specified to run at 6:00, 
  # but since it is 6:00 in UTC, it is scheduled for 15:00 in JST, adding 9 hours to UTC.
  schedule: "0 * * * *"
  # ===
  # If for some reason a job cannot be started at the time specified in the schedule, 
  # you can specify how many seconds later it can be started.
  #
  # Note: If a CronJob fails 100 times in a row, it will not be able to create 
  # a job again unless the CronJob itself is recreated.
  # If "startingDeadlineSeconds" is specified, the CronJob will keep 
  # creating jobs at the specified time unless it has failed 100 times in a row within this period. 
  # For example, if you specify "startingDeadlineSeconds: 300", the CronJob will keep creating jobs unless it has failed 100 times in the past 300 seconds.
  # ===
  startingDeadlineSeconds: 300 
  # How to treat concurrent executions of a job that is created by this CronJob.
  # The default setting is "Allow" to allow parallel execution. You should set it according to the characteristics of your job.
  go: Allow
  # Set how many failed job histories you want to keep. (default: 1)
  # If a job is left behind, the pod is also left behind, so you can identify the pod from a particularly failed job and look at the log to get the cause of the failure.
  failedJobsHistoryLimit: 3
  # By setting "true", you can stop CronJob temporarily. (Existing jobs will keep moving.)
  # This is used when you want to avoid CronJob to generate jobs for temporary maintenance.
  suspend: false
...

マイクロサービステンプレートを提供するメリット

マイクロサービステンプレートを開発して実際に感じたメリットをいくつか紹介します。

開発チームの開発効率を上げることが出来る

背景でも説明したとおりマイクロサービステンプレートを開発するモチベーションの軸として、開発チームのサービスをマイクロサービスプラットフォームに乗せてもらう際のコストを低減したい、というものがありました。
この点については実際、メリットを感じておりGolangやKubernetesの知見を持たないチームには、特に参考にしてもらっているようです。

仮に参考実装がなければ、Kubernetes Manifestを書いたことがない人にリソースの解説とプロパティの説明をしながらペアプロをしたり、頑張って単身でキャッチアップしてもらうことになってしまいます。
この点は細々としたコメントとドキュメントを用意しているのでのおかげで、コミュニケーションコストやキャッチアップコストを削減できていると思います。

また、マイクロサービステンプレートは実アプリケーションとしてマイクロサービスプラットフォームで実際に稼働・運用している参考実装です。
そのためコピペしたら動かないといったことはありませんし、Datadogのトレースを取るためのアプリケーション実装といった、サービス運用に現実的に必要な実装も最低限含まれているので、車輪の再発明を防ぐ効果があります。

マイクロサービスプラットフォーム利用者側の視点を得られる

上記でも説明していますが、マイクロサービステンプレートはマイクロサービスプラットフォームに乗せる一つのサービスとして実装し実際クラスタ上で稼働・運用しています。 アプリケーション実装やデプロイフローだけではなく、運用レベルまで実アプリケーションの参考となるように設計しています。その過程で、利用者側の視点を得ることができたと思います。 これによって、実際マイクロサービスプラットフォーム利用者向けのドキュメントを改善したり、利用者とのコミュニケーションが円滑になったと思います。 今後もマイクロサービスプラットフォームの提供側として、この視点を活用していきたいです。

あとこれは余談なのですが、自身が初めてチームに配属され、単身進めたプロジェクトがこのマイクロサービステンプレートでした。
KubernetesもGolangも良くわかっていなかった自分が、マイクロサービスプラットフォーム利用側とマイクロサービスプラットフォーム運営側の技術を満遍なく触ることが出来たのでオンボーディングのタスクに非常に適していたと思います。

検証用のアプリケーションとして利用できる

マイクロサービスアーキテクトチームとしてマイクロサービステンプレートを実質一つのサービスとして持つことができ、アプリケーション周りの検証に利用することができます。
例えばリソースのapiVersionのアップデートや、マニフェストを書き換えた際の挙動の確認等です。

あとはマイクロサービステンプレートの実装は必要最低限なものになっているので、検証中に壊れた場合に原因の切り分けがやりやすい、といった特徴もあります。 検証以外のフェーズとしてもマイクロサービステンプレートをDatadogで監視し続け、クラスタやコントローラによる破壊的な変更の影響があった場合に、それを検知できるようにしています。

開発を通して苦労した部分・難しく感じた部分

マイクロサービステンプレートの開発をして、苦労した部分や課題を感じている部分について述べます。
マイクロサービステンプレートはアプリケーション実装部分で、色々と難しさを感じたり苦労した部分が大きかったです。
その理由としては、やはりアプリケーション開発の自由度の大きさがあったと思います。

異なるクラスタへの対応

現在開発を進めているマイクロサービスプラットフォームはGKEとEKSの2種類があります。 この背景としては、もともと各チームの技術スタックがバラバラなこともあり、AWSを使っていたチームとGCPを使っていたチームがそれぞれ存在したためです。 本来はGKEもしくはEKSに統一したかったのですが、総合的な移行コスト等を加味して最終的にGKEとEKSの両方でマイクロサービスプラットフォームも提供することになっています。

そして、GKEとEKSの細かな仕様の違いから、別途EKS用のマイクロサービステンプレートを作成する事になってしまいました。 細かな仕様の違いの例としては以下のものがあげられます。(今回詳しい内容については掘り下げません。)

  • Ingressの設定
  • ImagePullSecretの設定
  • ExternalSecrets(機密情報)の設定

GKEとEKSのマイクロサービステンプレートはお互いに完全に独立して取り扱っており、GitHub Repositoryなども別リポジトリになっています。 ただ二つのサービスをメンテナンスし続けるのはコストパフォーマンスが悪いので、EKS側の実装はGKE側のマイクロサービステンプレートの実装よりも一部削ったものになっています。 削っている部分を軽く挙げると、hot-reloadに利用するAirやローカル開発用に利用するdocker-composeの設定ファイル、DatadogのDashboard/Monitorなどがあります。

これらを参考にしたいチームはEKSにアプリケーションを乗せるチームであっても、GKE側のマイクロサービステンプレートを見に行かなければなりません。 ここはどこまで二重管理するかのトレードオフだと思っていて、マイクロサービステンプレート利用者側からフィードバックを受けつつ、不便そうであれば今後変更を検討するべき箇所だと考えています。

バッチ処理への対応

マイクロサービステンプレートは、アプリケーションとして当初ビジネスロジックを持たない簡単なバックエンドサーバーの実装のみだったのですが、開発途中でバッチ処理を追加することになりました。 これは事業部内の各開発チームでバッチ処理を利用するケースが多く想定されたためです。

ただ簡易的なバッチ処理を実装するだけであれば、軽いGolangの実装とCronJobのマニフェストを追加する程度で済むと思っていたのですが、これが思った以上に時間がかかりました。 単純に実装するだけではなく、バッチ処理のためのDashboardやMonitorをDatadogに用意、ドキュメントの整備、CI/CDパイプラインの修正といった追加の作業が発生しました。

これだけで済めばよかったのですが、別の大きな問題が一つ発生しました。 バッチ処理もマイクロサービステンプレートのWebサーバーのアプリケーションと同じリポジトリで管理して、一部パッケージを共有しています。 その都合でArtifact RegistryへのWebサーバーのDocker Imageとバッチ処理のDocker Imageのデプロイが同時に発生するのですが、 そうすると、それをトリガーとしてクラスタにデプロイを行うSpinnaker側で稀に特定のバグを踏むようになってしまいました。

github.com github.com

最終的に、マニフェストを管理しているモノレポでバッチ処理(CronJobリソース)のマニフェストとWebサーバーの実装のマニフェストを分離することで対応しました。 ただマイクロサービステンプレートを作っていなければ、この問題に気づくのが遅れたり・気付けなかったりして、障害を引き起こしていた可能性があるので、結果的には良い点でもあったのかもしれません。

3rd Partyライブラリへの依存

マイクロサービステンプレートはあくまで参考実装なので、特に複雑なビジネスロジックはありません。 当初はできる限り標準ライブラリで実装を済ます予定でしたが、ミドルウェアが増えてきた段階で、コードの可読性が低下したので 3rd Partyライブラリのchiを導入しました。

参考実装として提供するものに、3rd Partyのライブラリを乗せたくない理由としては参考実装の運用が面倒になるのと、出来る限り実装に余計な思想をつけたくなかったためです。
ただ、現実的にはある程度のアプリケーションになってくると、chiのようにライブラリに頼ることになってきます。 今回chiに頼らない方向も検討しましたが、そうなると実装の内容としては正しくても現実的にあまり存在しないアプリケーション実装になってしまいます。 そうなってしまうと、果たしてそのアプリケーションは参考実装と呼べるか怪しくなってくるので、最終的にchiを導入しました。

現在各開発チームに利用していただいてますが、特にGolangやKuberenetesの知見がないチームにはマイクロサービステンプレートの強制力が強すぎるような雰囲気を感じています。 重要な意思決定をくださずマイクロサービステンプレートの利用している3rd Partyライブラリをそのまま真似する、アーキテクチャをそのまま真似しまうといったケースが想定されます。 マイクロサービステンプレートは、あくまで参考なので、最終的には各チームのニーズにあったアプリケーションのアーキテクチャを採用する必要があります。 各チームが絶対にその実装やアーキテクチャを真似する必要はありません。

マイクロサービステンプレートが他チームのオーナーシップを奪わないように、あくまで参考であることを念を押して提供する必要があると感じています。 また、その恐れがあるからこそマイクロサービステンプレート自身の実装や、そこで利用するライブラリの選定はより慎重にならなければいけないと思いました。

各チームの技術そのものへの理解度をあげる効果が低い

マイクロサービステンプレートはマイクロサービスプラットフォームにアプリケーションを載せるまでの開発効率は向上しますが、各チームの技術に対する理解度を深める効果は高くありません。ここで言う技術とは、今回のGolangやKubernetesが挙げられます。

Kubernetesの例ですと、マイクロサービステンプレートとしては、コメント付きでサンプルのマニフェストを提供しているので、マニフェストの内容を理解することはできます。 しかし、アプリケーションの障害発生時の調査の際に、問題の切り分けをするためにはKubernetesの内部仕様についてある程度理解しておく必要があるかもしれません。
またkubectlでどういったコマンドを叩いて、どういったことができるのか、といった内容についてマイクロサービステンプレートでは全く触れていません。

最終的に、各チームにアプリケーションを運用してもらうには、そういった箇所もある程度理解してもらう必要があります。
マイクロサービステンプレートは、アプリケーションで載せるまでの参考にはなりますが、運用部分についてのナレッジはコードやマニフェストに現れづらいので、この点に関しては現状マイクロサービステンプレートで拾い切れていない部分だと感じます (とは言っても、マイクロサービスプラットフォームにアプリケーションを乗せれば、各チームの運用コストや運用範囲は多少低減するとは思いますが)。

そのため社内で別途チュートリアルを用意する、学習のポインタとなるドキュメンテーションを行うといった対応をする必要があると思っています。

反省とまとめ

マイクロサービステンプレートは最終的にある程度の整備が完了するまで、作り始めてから期間として三ヶ月と結構かかりました。
他のタスクにも取り組んでいたので、厳密に言うとマイクロサービステンプレートだけで三ヶ月はかかっていませんが、それを踏まえても結構かかったと思います。

ただ、現在進行系で各チームにマイクロサービステンプレートを活用してもらっており、作ってよかったと思っています。 やはり実際に動くものをサンプルとして用意するのは、やはり知見共有の方法として結構コスパの良い方法でした。 できるかぎり実アプリケーションに扱いを寄せているという面についても効いていると感じます。

100人規模の組織に対してマイクロサービスプラットフォームを提供する都合上、その参考実装としても対応すべきニーズが広くなりやすい特性があります。 マイクロサービステンプレートとしては「そのニーズの範囲に対してどこで折り合いをつけるか」、が重要に感じました。 プラットフォームエンジニアリングの難しさが、プラットフォームそのもの以外のこういった箇所にも現れているケースだと思います。

まだ各チームのニーズをすべて拾い切れているとは言えませんし、マイクロサービステンプレート関連で用意しているドキュメントの記述や説明が甘い部分もあるかと思います。 今後他チームからのフィードバックを貰いつつ、マイクロサービステンプレートが肥大化しすぎないようには気をつけて、改善を続けていこうと思います。

次の記事、18日目は Calendar for DMMグループ Advent Calendar 2021 | Advent Calendar 2021 - Qiita にて中村さんです!
楽しみですね!