NearMe Tech Blog

NearMeの技術ブログです

ArgoによるCI構築

はじめに

今回は、NearMeにおけるCIの仕組みについて説明します。 CIとは、Continuous Integration(継続的インテグレーション)の略で、 コード変更の度にビルドとテストを自動で実行するプラクティスを指します(参考)。

NearMeではCIを実現する方法として、Kubernetes(k8s)上に動作するArgoを利用しました。 k8sはコンテナ化されたアプリケーションを管理するためのオープンソースのシステムです。 Argoはk8s上でCIやCD(Continuous delivery)を実現するツール群です。 一般に、CI構築では外部サービスを利用することも多いですが、 他システムの障害や料金に依存せずCIを管理したかったのと、 既にk8sを中心にシステムを構築していたので、 多少の煩雑さはありつつも自前で構築しました。

システム構成

CIシステムはイベントの処理を行うArgo Eventsとジョブを実行するArgo Workflowsからなります。全体としては下図のような構成になります。

Argo EventsやArgo Workflowsにはそれぞれ、k8sのカスタムリソースが用意されていて、k8s上でのリソースを容易に構築できるようになっています。ただし、NearMeではマイクロサービスで複数のリポジトリを扱うため、リソース定義を共通化したい部分も多いので、cdk8sを用いてそれらをさらにプログラム的に管理するようにしています。

Argo Events

Argo Eventsは、様々なイベントを汎用的に扱うことを想定して作られています。

NearMeのCIではGithubのwebhookのイベント扱います。これはカスタムリソースのEventSourceで実現できます。以下はその定義の例です。

kind: EventSource
metadata:
  name: github-event-source
spec:
  github:
    rideServicePullRequest:
      events:
        - pull_request
      repositories:
        - names:
            - ride-service
          owner: nearme-jp
      webhook:
        endpoint: /ride-service/pull_request
      ...

このリソースは、ride-serviceというリポジトリのプルリクエストのイベントを/ride-service/pull_requestというエンドポイントで検知します。

ただし、外部からのリクエストを受け付けるために、このエンドポイントのホストはk8sのIngressにおいて定義します。それにより、GithubからのリクエストはIngressを介してEventSourceに送られます。

このエンドポイントのURLを、GithubのリポジトリにおけるSettings > Webhooksにて設定します。このとき、プルリクエストやプッシュといった送信するイベントの種類も指定します。

EventSourceで受信したイベントは、Sensorに伝えられます。Sensorは特定のイベントをサブスクライブして、特定の処理を起動します。この間のやり取りは、EventBusを介して、Pub/Subのメッセージングモデルで行われます。

Sensorは次のようなカスタムリソースで定義します。

kind: Sensor
spec:
  dependencies:
    - eventName: rideServicePullRequest
      eventSourceName: github-event-source
      name: ride-service-merged-dep
      filters:
        name: data-filter
        data:
          - path: body.action
            type: string
            value:
              - closed
          - path: body.pull_request.merged
            type: bool
            value:
              - "true"
          - path: body.pull_request.base.ref
            type: string
            value:
              - main
  triggers:
    - template:
        conditions: ride-service-merged-dep
        k8s:
          resource: workflows
          parameters:
            - dest: spec.arguments.parameters.0.value
              src:
                dataKey: body.repository.name
                dependencyName: ride-service-merged-dep
          source:
            resource:
              kind: Workflow
              arguments:
                  parameters:
                    - name: repository_name
              ...

このリソースは、ride-serviceリポジトリのmainブランチにプルリクエストがマージされたイベントから、後述するWorkflowを起動します。

このとき、Githubのイベントのペイロードにおいてbody.repository.nameのパスに格納されたリポジトリ名をWorkflowに渡します。このような形で、プルリクエストのリポジトリ名やブランチ名、コメントやラベルなどの情報(詳細はこちら)をWorkflowで利用することが可能になります。

なお、ペイロードの中身は、Githubのwebhookの設定画面の"Recent Deliveries"タブで確認することができます。また、ここで"Redeliver"ボタンを押すことで、同じイベントを送信することができるのでデバッグに便利です(ただし、Workflowの一つのイベントに対するジョブは、同じイベントでは再度起動できないようになってるので、デバッグするときはジョブの実行履歴を消す必要があります)

Argo Workflows

NearMeのCIでは、Argo Eventsで処理されたイベントからArgo Workflowsにおけるジョブのフロー(Workflow)を起動します。

このWorkflowでビルドとテストを行い、問題なければ、ビルドしたイメージをコンテナレジストリにプッシュします。ここではさらに、チャットサービスのSlackに通知したり、タスク管理サービスのAsanaのタスクのステータスを変更したりもしています。

次図は当Workflowの内部処理を示したものです。

Workflow内部ではビルド用のタスクと、そのタスクの終了時にフックされて起動する通知用のタスクを用意しています。

各タスクの処理はk8sのコンテナのコマンドで書きます。コマンドで全処理を書けなくもないですが、それなりの量になったので、シェルスクリプトにファイル化してk8sのConfigMapに保存して実行するようにしています(もっと複雑になれば、その他の言語で書き直すかもしれません)。外部サービスだとこの辺りより簡潔に書ける可能性はありますが、この方式でも少しの努力で同じことはできると考えています。

なお、Workflowは単体でも実行することが可能です。デバッグに用いたり、イベント系で障害があった時は手動で実行したりすることもできます。

ビルドタスク

ビルドタスクではまず、Workflowの引数に渡されたリポジトリ名やブランチ名からをリポジトリをチェックアウトします。githubへアクセスするためにk8sのSecretsからプライベートキーを取得しています。

そこからコンテナのイメージをビルドします。ここでdockerコマンドを利用するために、Docker In Docker(dind)というコンテナをk8sのサイドカーで実行しています。

テストも基本的にはdockerコマンドを利用します。このとき。例えば、MySQLなどテスト実行で依存するサービスも一緒に立ち上げます。また、NearMeではRide ServiceからRouting Serviceを利用しており、Ride Serviceのテスト時にRouting Serviceを立ち上げたりもしています。

最後に、コンテナレジストリ(AWSのECR)にビルドしたイメージをプッシュします。

その他、docker savedocker loadコマンドを利用して、dockerのイメージをキャッシュして、毎回ダウンロードしないようにもしています。このキャッシュはk8sのVolumeに保存しています。

また、リポジトリによってはビルドやテスト時間が長いので適宜手動で省略できるようにもしています。具体的には、プルリクエストに付与されたラベルに基づいて、ビルドをスキップしたり、ブランチ派生前からの差分のあるファイルを検出して、そのファイルに直接関連のあるテストだけ実行するようにしています。

さらに、テストのエラーログなどを通知で表示するため、シェルスクリプトの実行ログをファイルに保存してその後の通知タスクで利用できるようにもしています。これはArgo WorkflowsのArtifactsという機能で実現しています。ここではArtifactsのバックエンドとしてS3ライクなMinioを利用しています。

通知タスク

通知タスクでは、前回のビルドタスクの成否を受け取り、プルリクエストのタイトルやコミットのリンク、ビルド時間などをSlackに通知します。また、ビルドエラーがあった場合は前述のArtifactsからログを取得して(文字のエスケープもして)Slackのメッセージに載せています。

さらに、プルリクエストのコメントに貼り付けられたAsanaのタスクのURLを抽出し、AsanaのAPIを利用して 、そのタスクのステータスを変更しています。また、そのリンクもSlackのメッセージに載せています。

おわりに

k8s上で動作するArgoを利用したNearMeのCI構成を紹介しました。 GithubのwebhookのイベントからArgo Eventsを介してArgo WorkflowsのWorkflowを起動する仕組みや、 Workflow内のビルドタスクや通知タスクでの工夫などを示しました。 ある程度k8sの知識が要求されるのでハードルはありますが、 逆にそこを越えればCIとしてやりたいことはできると思います。 Argo自体は非常に汎用的で、まだ利用できてない部分もあるので、これからさらに開発プロセスを進化させていけたらと思います。

最後になりますが、NearMeではエンジニアを募集しています!ご興味のある方はぜひ以下をご覧ください。

Author: Kenji Hosoda

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