見出し画像

PRごとに動作確認用の環境を自動生成する Now In REALITY Tech #123

REALITY の DevOps チームエンジニアの kuyama です。

本日の Now In REALITY Tech では「GitHub PRごとに動作確認用の環境を自動生成する」仕組みについて解説します。

REALITY では長年、「動作確認用の開発環境が1つしかない」ことが問題視されていました。

3年前、私は開発合宿でこの問題に取り組み、既存の開発環境から独立した環境を構築しました。
この時作成した新環境は開発合宿専用のもので、その後実際に使われることはありませんでした。

しかし、この問題への取り組みはその後も継続的に行われ、現在では「PRごとに動作確認環境を簡単に作成できる」状態へとなりました。

REALITYのサービス群は Google Cloud の Google Kubernetes Engine(以降、GKE) 上にデプロイされており、PRごとの動作確認環境への通信経路図は以下のようになります。

通信経路図

今回はこの、「PRごとの動作確認環境」がどのように実現されているのかについて詳しくお話しします。

前提知識

「PRごとの動作確認環境」を解説するにあたって、いくつかの前提知識を説明します。

通信経路図で示されているように、REALITY のバックエンドではマイクロサービスアーキテクチャを採用しており、複数のサービスが GKE クラスタ上で動いています。

そして、GKE クラスタ上で動いているリソース群のデプロイ管理は、 ArgoCD を用いて行われています。

そして、この Pod 群が deploy されている GKE クラスタには、Istio ベースのフルマネージドのサービスメッシュである、Anthos Service Mesh(以降、ASM) を導入しており、クラスタ内のトラフィックを管理しています。

昨年の GREE Tech Conference 2023 にて、弊チームのエンジニアである小田さんが発表したように、REALITYでは ASM を活用し B/G デプロイを実現しています。
この B/G デプロイのために、Istio の CRD(Custom Resource Definitions) である VirtualService を用いたヘッダベースでのルーティングによって、APIアクセス先の制御が行われています。

ここまでが前提知識となります。
一度まとめておきます。

  • REALITY のバックエンドサーバはマイクロサービスアーキテクチャを採用しており、複数のサービスが動作している

  • 上記のサービス群は、 Pod として GKE クラスタ上にデプロイされている

  • GKE クラスタ上のリソース群は、ArgoCD で管理されている

  • この GKE クラスタには ASM が導入されており、 VirtualService によってヘッダベースでのルーティングが行われている

PRごとに動作確認環境を作成する

上記の前提を基に、「PRごとに動作確認環境を作成する」ために必要な要件を、まずは書き出してみます。

① 動作確認環境を作成したい PR を検知する
② PR に対応する、Docker イメージをビルドする
③ PR に対応する、リソース群を GKE クラスタ上に作成する
④ 作成したリソース群へのルーティングを追加する

これらの要件に対して、REALITY ではそれぞれ以下のように対応しました。

① 動作確認環境を作成したい PR を検知する
 → PR に付与できる GitHub のラベルを用いて検知を行う
② PR に対応する、Docker イメージをビルドする
 → GitHub Actions と Cloud Build を利用して、ラベルが付与された PR を元に Docker ビルドを行う
③ PR に対応する、リソース群を GKE クラスタ上に作成する
 → ArgoCD の CRD である、 ApplicationSet を使用する
④ 作成したリソース群へのルーティングを追加する
 → VirtualService にヘッダベースでのルーティングを追加する

それぞれ、細かく見ていきましょう。

① 動作確認環境を作成したい PR を検知する

全ての PR に対して動作確認環境を作成した場合、リソースを使い過ぎてしまうので、特定のラベルが付与された PR のみに対して動作確認環境を作成するようにします。

② PR に対応する、 Docker イメージをビルドする

ここでは GitHub Actions と Cloud Build を利用して、ラベルが付与された PR に対して、Docker イメージビルドを行います。
REALITY では CI/CD パイプラインとして、 GitHub Actions と Cloud Build を利用しており、ここでもこれらのサービスを利用します。

特定のラベルが付与された PR に対して、Docker イメージビルドを行うには以下のように GitHub Actions のワークフローを定義します

name: Trigger PR Env Build On Labeled

on:
  # PR作成時、push時、ラベル付与時にワークフローが実行される
  pull_request:
    types:
      - opened
      - synchronize
      - labeled

jobs:
  build-pr-env-image:
    runs-on: ubuntu-latest
    # PRに特定のラベル'pr-env'が付与されていれば、ジョブが実行される
    if: contains(github.event.pull_request.labels.*.name, 'pr-env')
    permissions:
      id-token: write
      issues: read
      pull-requests: read
    steps:
      # Workload Identity を利用して Google Cloud の認証を行う
      # https://github.com/google-github-actions/auth を参照
      - name: auth
        uses: google-github-actions/auth@v2

      # Dockerイメージをビルドする Cloud Build を起動する
      - name: Trigger Build
        run: |
          gcloud builds triggers run pr-env \
          --branch=${{ github.event.pull_request.head.ref }} \
          --quiet

このワークフローにより起動される Cloud Build では、ブランチ名をタグとしてもった サービスの Docker イメージがビルドされます。(Dockerイメージのタグとして "/" が利用できないので、ブランチ名に対してエスケープ処理を行っています)

③ PR に対応する、リソース群を GKE クラスタ上に作成する

ここでは、ArgoCD の CRD である ApplicationSet を使用して、各マイクロサービスに対して、対応する Deployment とService リソースをデプロイします。

この ApplicationSet は、ArgoCD 経由でデプロイ管理するリソースのグループを、条件に応じて(一部変更を加えつつ)複製する機能を持ちます。(公式ドキュメント
今回は、①でラベルを付与したPRの数だけリソースを複製する設定を記述します。

これらの kubernetes マニフェスト を置いておく manifestリポジトリは、以下のような構成になります。

manifest
 ├── services
 |   ├── serviceA
 |   |   ├── deployment.yaml
 |   |   ├── service.yaml
 |   |   └── kustomize.yaml
 |   ├── serviceB
 |   ・
 ||   ・
 |   
 └── applications
     ├── applicationSetA.yaml
     ├── applicationSetB.yaml
     ・
     ・
     ・

services/serviceA/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: serviceA
spec:
  template:
    spec:
      containers:
        - name: serviceA
          image: serviceA

services/serviceA/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: serviceA
spec:
  type: NodePort
  selector:
    key: serviceA
  ports:
    - name: http
      port: 8080
      targetPort: 8080

services/serviceA/kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: pr-env
resources:
  - ./deployment.yaml
  - ./service.yaml

applications/applicationSetA.yaml

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: serviceA
  namespace: argocd
spec:
  generators:
    # GitHub上の特定のラベルがついたPRに対して、それぞれリソースを作成する
    - pullRequest:
        github:
          owner: ${serviceAのリポジトリのOwner}
          repo: ${serviceAのリポジトリ}
          tokenRef: ${githubと接続するためのtoken情報}
          labels:
            - pr-env
  template:
    metadata:
      # {{branch_slug}} は、リソース名として使用できるように文字列エスケープされたブランチ名
      name: "{{branch_slug}}-serviceA"
      namespace: argocd
    spec:
      destination:
        namespace: pr-env
        server: https://kubernetes.default.svc
      project: default
      syncPolicy:
        automated: {}
      source:
        path: services/serviceA
        repoURL: ${manifestリポジトリのURL}
        targetRevision: master
        kustomize:
          # リソース名を書き換え
          namePrefix: "{{branch_slug}}-"
          # ブランチ名をラベルとして付与することで、別のリソースから区別できるようにする
          commonLabels:
            pr-env/branch: "{{branch_slug}}"
          # Dockerイメージをタグ付きの正式名称に置き換える
          images:
            - serviceA=asia.gcr.io/${projectID}/serviceA:{{branch_slug}}

④ 作成したリソース群へのルーティングを追加する

 次に、VirtualService に対して作成したリソース群へのルーティングを GitHub Actions から追加します。
本当は、③のステップでルーティングに関するリソースも個別にデプロイすることで、ルーティングが追加できると良かったのですが、同じドメインに対して VirtualService を複数デプロイしても設定は統合されず、挙動が不安定になるので、泣く泣く一つの VirtualService リソースにルーティングを追加していく方式を取っています。

歴史的経緯により、REALITY ではヘッダベースでのルーティングに VirtualService を使っているためこのようなフローになっていますが、 ルーティングに Kubernetes Gateway API の HTTPRoute を利用すれば、ルーティングの設定も ApplicationSet でPRごとに GKE クラスタ上へ追加できます。そのため、1からヘッダベースでのルーティング機能を追加する場合は、Kubernetes Gateway API を採用する方が良いかもしれません。

まとめ

プロダクト初期から存在していた「開発環境が1つしかない」という問題に対して、PRごとにリソースをデプロイしヘッダベースのルーティングを追加することで、個別に動作確認環境を用意できるようになりました。

これにより、動作確認中に別の開発施策によって引き起こされたバグに悩まされることが無くなり、より信頼性の高いテストが行えるようになりました。

似たような課題として、「開発環境がアクセスしているDBが1つしかない」という問題があり、こちらに対しても引き続き取り組んでいきたいと思っています。

それでは、次回の Now In REALITY Tech もお楽しみに!