# リリース・デプロイ戦略を支える技術

ここでは Kubernetes 上で起動する server や worker の deploy 戦略について説明します。 Wantedly における Git 及び GitHub の運用方法が前提となるので事前に [Git の慣習](/fields/dev-process/git-convention.md) を読んでおくと良いでしょう。

## デプロイ基盤

### GitHub Flow の実現方法

各 repository で CI/CD で下の２つのことを行うことで GitHub Flow を実現しています。

* 全 branch: Docker image の build (`kube ci-build`)
* main (master): Docker image の deploy (`kube <env> deploy`)

ここで `ci-build` は commit hash を tag にもつ docker image を作成して registry に保管し、 deploy コマンドが Kubernetes の Deployment に対する変更を反映することで rolling で deploy されます。

いわゆる CIOps という Push 型のデプロイです。

Docker image 以外の更新、つまり Kubernetes Manifest のデプロイについては Argo CD による GitOps という Pull 型のデプロイを採用しています。

### Rolling Deploy

Rolling deploy では新旧２つのアプリケーションバージョンを一部ずつ置き換えていきます。 このため、アプリケーションにもよりますが最大で数分程度複数のバージョンが共存する時間ができることになるため、 すべての deploy は後方互換性をもたせる必要があります。 一例としてフロントエンドとサーバーサイドのコードを 1 つのコードベースで管理している microservice (wantedly/wantedly, wantedly/perk など) では API 追加とその API に依存する frontend は先に API のみを merge する必要があります。また逆に機能を落とす場合は先にフロントエンドからの依存がない状態を merge 仕切ってから API を落とす pull request を merge する必要があります。

### Kubernetes Manifest 管理

#### Argo CD による GitOps

Kubernetes Manifest は [Argo CD](https://argo-cd.readthedocs.io/en/stable/) を導入して GitOps という Pull 型のデプロイを行っています。

もともと Wantedly では Docker image の deploy を CICD で行っていましたが、Deployment 以外の Kubernetes manifest は長らく手動での apply になっていました。 そのため、以下のような課題・問題がありました。

* リリースフローの自動化・高速化・簡易化を行い、インフラチーム以外でも Kubernetes Manifest の管理を可能にすることで開発・リリースの速度を早めたい
* apply 忘れなどで repository と実際に apply されている manifest が異なり、Kubernetes Cluster のメンテナンスや Upgrade のブロックになっていた
* apply 忘れなどで repository と実際に apply されている manifest が異なり、リリース時にアプリケーションエラーを引き起こすインシデントになった

そこで [Argo CD による GitOps を導入 (internal)](https://github.com/wantedly/infrastructure/issues/9506) し、特に kubernetes manifest の継続的なデプロイの自動化をゴールとして、これらの課題を解決しています。

ただし先述したとおり、アプリケーションコンテナのデプロイ は CICD で Kubernetes Deployment に対して変更を加えて Rolling Deploy を行っているため、docker image tag は GitOps の対象外となっています。 これは

* 課題に対して docker image tag の GitOps は一旦スコープアウトできる
* 障害対応時には `kube <env> deploy <commit hash>` コマンドで緊急切り戻しを行っているため、このフローのアジリティを壊したくなかった

といった背景がありました。

今後、docker image tag も含めて CIOps から　GitOps への移行を検討するなど、さらなるデプロイ戦略の改善を進めています。

#### Kubernetes Manifest の自動生成・更新

GitOps に加えて、Wantedly の microservice のための Kubernetes Manifest を自動生成して継続的に更新する仕組みがあります。 これにより、その repository に必要な Deployment, HPA, Service, PDB といった Kubernetes Resource を、Wantedly の microservice における推奨設定込みで用意できます。

generator は autoscale (Deployment + HPA + Service + PDB) のほかに ingress, cronjob, worker など複数用意されており、`kube generate` のサブコマンドから一覧を確認できます。

```
$ kube generate
Generate manifest files

Usage:
  kube generate [flags]
  kube generate [command]

Aliases:
  generate, g

Available Commands:
  autoscale          Generate manifest files for Deployment with Autoscale
  cronjob            Generate manifest files for Cronjob
  grpc               Generate manifest files for gRPC Deployment
  ingress            generate manifest files for Ingress to web pod
  internal-ingress   generate manifest files for Ingress resource for internal only
  namespace          Generate manifest files for Namespace
  rbac-kube-sh-by-ci Generate manifest files for resources to enable `kube sh` from CI
  redis              Generate manifest files for redis statefulset
  sidekiq-exporter   Generate manifest files for sidekiq-exporter
  worker             Generate manifest files for worker deployment

...
```

**`kube-generate.yaml` による宣言的な管理**

以前は `kube generate <subcommand>` を都度手元で実行したり、生成コマンドを列挙したシェルスクリプトを repository に置くことで manifest を生成していました。 この方式では「現在どの generator にどの設定で manifest が作られているのか」がコマンド履歴やスクリプトを読まないとわからず、再生成のたびに手元での操作も必要でした。

そこで現在は、生成したい manifest の内容を repository ルートに置く `kube-generate.yaml` に宣言的に記述する方式を推奨しています。 たとえば次のような内容になります。

```yaml
generates:
- namespace: {}
- autoscale:
    port: 8080
    healthcheckPath: /
- cronjob:
    name: example-cleanup
    schedule: "0 * * * *"
    image: example.com/cleanup:latest
    command:
    - bash
    - -c
    - "echo cleanup"
```

`generates:` 配下の各項目が利用する generator とその設定に対応しています。 この `kube-generate.yaml` をもとに `kube generate --update` を実行することで Kubernetes manifest 一式が再生成されます。 新たに generator を追加したい場合は `kube generate --save <generator>` を実行すると `kube-generate.yaml` に対応する項目が自動で追記されます。

`kube generate` がサポートしていない設定を加えたいときは次のいずれかの方法で対応します。

* `kube generate` の generator 実装そのものを拡張する
* `kube-generate.yaml` 上で patches を指定し生成結果に変更を加える
* 手動編集した manifest を `kubernetes/` ディレクトリ以下に追加する。 `metadata.annotations` に `wantedly.com/generated-by: manual` を付与しておくと自動再生成の対象から除外されます

**継続的な再生成**

`kube generate` のテンプレートは kube のバージョンアップに伴って更新されていくため、過去に生成した manifest が古いままにならないよう継続的に再生成する仕組みを用意しています。 Renovate のように最新の kube バージョンを検知して `kube generate --update` を実行し、差分があれば各 repository に manifest 更新の Pull Request を自動で作成します。 詳しく知りたい方はこちらも見てみてください。[継続的な manifest 更新の取り組み (internal)](https://github.com/wantedly/infrastructure/issues/7902)

## FeatureFlag

基本的にリリースブランチを持たない我々の開発手法ではコンフリクトを避けるためにコンスタントに main branch (master branch) へ merge していくことが重要です。 しかしながら main branch (master branch) で merge されると自動で deploy がなされるため、場合によっては変更をユーザーに見せたくないこともあります。 このような場合に FeatureFlag が有用です。 FeatureFlag は `boolean`, `string` のいずれかを返す method の返り値を request ごとに任意の値に変更できる Wantedly の社内基盤です。 値の変更を許可しておきたい method を事前に FeatureFlag の library で wrap しておくと [Chrome Extension (internal)](https://github.com/wantedly/chrome-dev-extension) から override ができるようになります。 これを用いることでユーザーには機能を隠しておきつつ、qa 環境では簡単にその機能を顕在化させることができます。

### Override 宣言のやり方

まず override できる method を指定する方法を示します。 現在 Rails と React がサポートされていますがそれ以外のフレームワークや言語に対応要望がある場合は Infra Squad に相談してください。

#### Rails

最新 version の servicex が install されていることを確認してください。

```ruby
module SomeNewFeature
  # この module を include すると module 内の任意の method が override 可能になる
  include Servicex::FeatureFlag::Trapper
  # 正規表現で特定の method に絞ることも可能
  feature_flag_trap_only /\?$/

  # 上記の正規表現に match するため override 可能になる
  # フラグ名は `SomeNewFeature.released?` となる
  def released?
    false
  end
end
```

```ruby
module SomeNewFeature
  # より低いレイヤの API を用いることで method でなくても任意の場所での override が可能
  def released?
    return true if some_condition

    # some-flag-name という名前で override 可能になる
    Servicex::FeatureFlag::Interceptor.intercept("some-flag-name") do
      false
    end
  end
end
```

※Rails の FeatureFlag 実装は本番環境では override されないことに注意が必要です。 [実装参照（internal）](https://github.com/wantedly/servicex-ruby/blob/2709bd9c4e794cebf6fb694bec75f215972d72d6/lib/servicex/feature_flag.rb#L13)

ただし本番環境でも FeatureFlag を使いたいケースは存在するため、実現に向けて検討中の段階となっています。 [Issue 参照（internal）](https://github.com/wantedly/dx/issues/382)

使用したいモチベーションが冒頭の「コンスタントに main branch (master branch) へ merge していくこと」だけならば 実装としては本番環境かどうかは予め環境変数で弾いておき、その上で Feature Flag の制御をするのがおすすめです。 こうすることで QA, Sandbox ではその制御を dev tool から flag を override して確かめることが可能です。

#### React

```typescript
// 別途 context provider を挿入する必要があるので詳しいことは各 repository を参照
import { useBoolFlag } from "@wantedly_private/frontendx";

export const SomeComponent: React.FC<Props> = (props) => {
  const featureEnabled = useBoolFlag("some-awesome-new-feature");
  if (featureEnabled) {
    // ...
  }
};
```

### Override のやり方

#### Chrome Extension

上記の方法で override 可能にした method は [Chrome Extension (internal)](https://github.com/wantedly/chrome-dev-extension) から override できます。

![Chrome Extension の popup を開いた表示](/files/-MhWAMx_RkDcGvRCkrDK)

qa 環境にアクセスして Chrome Extension の popup を開くと response の render に使われた flag の一覧が表示されます。 ここで変更したい flag の override を値を更新してページをリロードすることでサーバーの挙動を override することができるためリリース前の機能などを試すことができます。

#### Kubefork

より高頻度に確認したい場合は、特定の Flag を有効にしたサーバーを別の URL で serve することができます。 具体例として下のような状況が実現できます。

* 新しいデザインの LP を出すかどうかを `new-lp` という Flag で提供
* `https://new-lp.qa.wantedly.com` のような URL を作成
* ここにアクセスすると任意のブラウザにおいて Chrome Extension なしで `new-lp` が有効になっているものとして動作する

エンジニアチーム外の人にテストをお願いしたいときには特に便利です。

詳しい利用方法については [fork のドキュメント（internal）](https://dev-docs.wantedly.com/fork) の「特定の FeatureFlag が有効にしたものをテストしたい」のセクションを参照してください。

## Pull Request Preview

上記のように FeatureFlag を使うことでリリースブランチを作らないようにすることが基本方針です。 しかしながら依存 Library の upgrade やアプリケーションの構成の変更などどうしてもリリースブランチを作る必要があることもあるでしょう。 こういったケースでは Pull Request Preview が便利です。 任意の Pull Request において `/preview <env>` とコメントすると次の図のように URL がコメントされるためここにアクセスするだけで preview が実現できます。

![Pull Request Preview の例](/files/7kTw1rzooSOXfa9YNgOH)

ブランチに更新があると追従されるため、数週間以上に渡ってリリースブランチを維持しなくてはいけないときに特に有効です。 この機能のバックエンドは [マイクロサービスでもポチポチ確認するための Kubefork](https://www.wantedly.com/companies/wantedly/post_articles/313884) であるため、 ユーザーフェイシングでないマイクロサービスでも利用可能です。 主要な repository はサポートされていますが自分の管理する repository が対応しているかについては Infra Squad に問い合わせてください。

## Migration

GitHub Flow では main branch (master branch) に存在する commit はすべて deploy 可能であるべきです。 もちろんこれを完全に保証することは不可能ですが、自動デプロイを行っているため少なくとも最新の commit が deploy されて困る状態は許容できません。 したがって、データベースの migration に依存する commit の merge は migration のあとに行われる必要があります。

そこでこれを保証する CI として [pending-migration-checker（internal）](https://github.com/wantedly/pending-migration-checker) があります。

![Pending Migration Checker の例](/files/-MhWAMxbipCI544UJN_J)

この check が落ちている場合は migration が必要です。 migration を `kube prod sh` などで実行して、`/check migration` とコメントすることで再度 check が実行されます。 詳しい設定方法などは上記の repository のリンク先を参照してください。

#### 話を聞きに行きたい

* Slack: [#infra](https://wantedly.slack.com/archives/C010V922570)

#### もっと知りたい

* [マイクロサービスでもポチポチ確認するための Kubefork | Wantedly Engineer Blog](https://www.wantedly.com/companies/wantedly/post_articles/313884)
* [wantedly/kube-go（internal）](https://github.com/wantedly/kube-go)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.wantedly.dev/fields/infrastructure/deploy-strategy-implement.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
