こんにちは!!
vivit で SRE をやっている 宮本 です。
今回は hinata レンタル で行っている E2E テストを GitHub Actions で実行できるように CI パイプラインを構築したので、構成やポイントなどを紹介したいと思います!!
要件とか
- コミットを GitHub に push したタイミングで E2E テストを実行する
- E2E テストなので、依存する複数のマイクロサービスも立ち上げる必要がある
- マイクロサービスのソースコードは CI を実行するリポジトリとは別のリポジトリに存在するものもある
- 多少実行時間がかかったとしても、なるべくマイクロサービスをモックにすることなく、本番に近い環境でテストすることを優先する
- vivit は開発環境、本番環境共に k8s を使っているので、CI上も k8s で環境を作れると嬉しい
- E2E テストのプロセス自体は k8s 上ではなく CI 内で直に実行する
- バックエンドマイクロサービスが使用するテストデータは簡単に変更できるようにする
vivit の技術スタックについては 、
をご参照ください 🙏
実際の構成
全体の構成を説明するのに不要な部分は削除したり、適宜内容を変えています。
そのままコピペしても動きませんので、ご了承ください。
ざっと以下のような構成となります!
name: E2E test on: push: branches: - master pull_request: paths: - "aaa/**" jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - id: auth uses: google-github-actions/auth@v0.8.0 with: credentials_json: secret - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@v0.6.0 with: project_id: secret - run: gcloud --quiet auth configure-docker - uses: actions/checkout@v3 name: clone aaa with: repository: aaa ref: master path: aaa fetch-depth: 3 token: token - name: pull aaa image working-directory: aaa run: |- git checkout master tags=($(git rev-parse HEAD | cut -c 1-7) $(git rev-parse HEAD~1 | cut -c 1-7) $(git rev-parse HEAD~2 | cut -c 1-7)) for tag in "${tags[@]}"; do json=$(gcloud container images list-tags asia.gcr.io/secret/aaa --format=json --filter="tags:${tag}") if [ "$(echo "$json" | jq '. | length')" -eq 0 ]; then echo "docker image(asia.gcr.io/secret/aaa:${tag}) doesn't exists." else docker pull asia.gcr.io/secret/aaa:"${tag}" break fi done image_id=$(docker images --format "{{.Repository}}:{{.Tag}}:{{.ID}}" | grep asia.gcr.io/secret/aaa | cut -f 3 -d ":") docker tag $image_id aaa:test # 他にも同じ方法で複数マイクロサービスの docker イメージを pull してくる - name: start minikube id: minikube uses: medyagh/setup-minikube@master - name: docker build bbb uses: docker/build-push-action@v3 with: context: . push: false load: true tags: bbb:test file: bbb/Dockerfile cache-from: type=local,src=/tmp/.buildx-cache-bbb cache-to: type=local,dest=/tmp/.buildx-cache-bbb-new,mode=max - name: minikube image load run: | minikube image load aaa:test minikube image load bbb:test - name: apply manifest run: | kubectl apply -k e2e/kubernetes sleep 3 kubectl get pods -o wide - name: minikube service mysql id: minikube-service-mysql run: | mysql_address=$(minikube service mysql --url) host=$(echo $mysql_address | sed "s/http:\/\///g") arr=(`echo $host | tr ':' ' '`) echo "::set-output name=mysql_host::${arr[0]}" echo "::set-output name=mysql_port::${arr[1]}" - name: minikube service bbb id: minikube-service-bbb run: | bbb_url=$(minikube service bbb --url) echo "::set-output name=BBB_URL::$bbb_url" - name: Use Node.js 13.14.0 uses: actions/setup-node@v1 with: node-version: 13.14.0 - name: yarn install working-directory: frontend run: yarn - name: insert test data run: | mysql \ -h ${{ steps.minikube-service-mysql.outputs.mysql_host }} \ -P ${{ steps.minikube-service-mysql.outputs.mysql_port }} \ -uuser -ppass < e2e/data/bbb.sql - name: Run E2E Test working-directory: frontend env: API_URL: ${{ steps.minikube-service-bbb.outputs.BBB_URL }} run: | yarn build yarn test:ci
順番に説明していきます。
1. docker, gcp などが使えるようにセットアップ
- name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - id: auth uses: google-github-actions/auth@v0.8.0 with: credentials_json: secret - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@v0.6.0 with: project_id: secret - run: gcloud --quiet auth configure-docker
ここは特に言うことありません。
後々、GCP のイメージレジストリである gcr から docker イメージを pull してくる必要があります。
なので GCP プロジェクトに対して認証を行っています。
2. 必要なマイクロサービスのリポジトリを clone してくる
- uses: actions/checkout@v3 name: clone aaa with: repository: aaa ref: master path: aaa fetch-depth: 3 token: token
依存するマイクロサービスが複数あります。
それらのマイクロサービスが存在する vivit organization の他リポジトリを fetch-deps 3 で clone してきます。
vivit の場合はコミット SHA をタグに使って docker イメージを継続的にビルドしています。
そのため、最新の docker イメージタグを取得するためにリポジトリを clone しています。
3. 必要なマイクロサービスのイメージを gcr から pull してくる
- name: pull aaa image working-directory: aaa run: |- git checkout master tags=($(git rev-parse HEAD | cut -c 1-7) $(git rev-parse HEAD~1 | cut -c 1-7) $(git rev-parse HEAD~2 | cut -c 1-7)) for tag in "${tags[@]}"; do json=$(gcloud container images list-tags asia.gcr.io/secret/aaa --format=json --filter="tags:${tag}") if [ "$(echo "$json" | jq '. | length')" -eq 0 ]; then echo "docker image(asia.gcr.io/secret/aaa:${tag}) doesn't exists." else docker pull asia.gcr.io/secret/aaa:"${tag}" break fi done image_id=$(docker images --format "{{.Repository}}:{{.Tag}}:{{.ID}}" | grep asia.gcr.io/secret/aaa | cut -f 3 -d ":") docker tag $image_id aaa:test
step2 でテストに使用するイメージタグが取得できたので、あとはイメージレジストリから docker pull してくるだけです。
この時、イメージのビルド中であるなどの理由で、gcr 上にイメージが存在せず pull に失敗する可能性があります。
それを防ぐために、力技ですが最新のイメージが存在しない場合には次に新しいイメージを取ってくるようにしています。
pull が完了したらタグをテスト用に書き換えて終了です。
これをテスト実行に必要なマイクロサービスの数だけやっていきます!
4. k8s クラスターを立ち上げる
- name: start minikube id: minikube uses: medyagh/setup-minikube@master
いよいよ、CI 内で k8s クラスターを立ち上げます 🎉
今回の場合、シングルノード構成で問題無いので、お手軽にクラスターを構築する為に minikube を利用してみました!
公式で紹介されているaction を利用するだけで簡単に構築できました。
5. CI を実行するリポジトリに存在するマイクロサービスをビルドする
- name: docker build bbb uses: docker/build-push-action@v3 with: context: . push: false load: true tags: bbb:test file: bbb/Dockerfile cache-from: type=local,src=/tmp/.buildx-cache-bbb cache-to: type=local,dest=/tmp/.buildx-cache-bbb-new,mode=max
他リポジトリで管理されているマイクロサービスに関しては gcr に存在するイメージを pull してきました。
しかし本リポジトリに存在するマイクロサービスに関しては当然、CI 実行時のソースコードでテストを行いたいですよね。
その為、docker イメージの build を行い、後のテストで使えるようにします。
6. 用意した Docker イメージを minikube 上で使えるようにする
- name: minikube image load run: | minikube image load aaa:test minikube image load bbb:test
ここまでのステップで必要な Docker イメージは CI が動いている環境に用意できました。
しかし minikube は CI の上に VM を立て、その中で k8s クラスターを起動しています。
よってそのままでは CI 上にあるイメージを minikube 側で認識することができません。
minikube image load
コマンドを利用して minikube 側でイメージが利用できるようにします。
7. k8s マニフェストを minikube に apply する
- name: apply manifest run: | kubectl apply -k e2e/kubernetes sleep 3 kubectl get pods -o wide
いよいよマニフェストファイルを apply してマイクロサービスを全て立ち上げます 😆
これまで用意してきたマイクロサービスのコンテナに加えて、mysql が必要なので起動します。
8. minikube service で E2E テストプロセスから k8s svc にアクセスできるようにする
- name: minikube service mysql id: minikube-service-mysql run: | mysql_address=$(minikube service mysql --url) host=$(echo $mysql_address | sed "s/http:\/\///g") arr=(`echo $host | tr ':' ' '`) echo "::set-output name=mysql_host::${arr[0]}" echo "::set-output name=mysql_port::${arr[1]}" - name: minikube service bbb id: minikube-service-bbb run: | bbb_url=$(minikube service bbb --url) echo "::set-output name=BBB_URL::$bbb_url"
7 でも説明した通り、 minikube は VM 上で立ち上がるので普通に localhost:{ポート番号}
を API のエンドポイントとして E2E テストを実行しても疎通できません。。。
minikube service
コマンドを使って k8s 内の Service(L4)との通信経路を確立します!
後のステップで使えるように output の処理も行います。
9. mysql にテストデータを入れる
- name: insert test data run: | mysql \ -h ${{ steps.minikube-service-mysql.outputs.mysql_host }} \ -P ${{ steps.minikube-service-mysql.outputs.mysql_port }} \ -uuser -ppass < e2e/data/bbb.sql
8 で取得した mysql のアドレスに対してテストデータを INSERT します。
テストデータ自体はシンプルな SQL が書かれたファイルです。
このファイルを変更するだけでテストデータの調整ができるようになっており、アプリケーションエンジニアが頻繁に CI を触る必要が無いようにしています。
10. E2E テストの実行!!
- name: Run E2E Test working-directory: frontend env: API_URL: ${{ steps.minikube-service-bbb.outputs.BBB_URL }} run: | yarn build yarn test:ci
いよいよ E2E テストを実行します!
その際の API エンドポイントは先ほど minikube service
で取得したものです。
環境変数経由で読み込ませることで、 これまでに構築した環境に向けて実行できます。
実行時間
テストの実行までにこれだけやっていると気になるのが実行時間ですよね 🤔
今のところ、トータルで12分程度となっています。
単体テストに比べると明らかに長いですが、ギリギリ許容範囲でしょうか?!
テスト実行の次に時間かかっているのが、 minikube の起動で1分半ほどかかっています。
毎回 k8s をダウンロードしているので、ここをキャッシュ等で早くできると10分程度にできるかもしれません 🙂
E2E テストの場合、実行時間や基盤構築などのコストが上がるのは仕方ないと受け入れているのですが、もっと良い方法があれば知りたいなと思っているところです。
さいごに
長くなってしまいましたが、今回はマイクロサービス構成の E2E テストを Github Actions で実行する方法について解説してみました。
vivit のエンジニアは今回紹介したような新しい取り組みを積極的に行っており、一緒にエンジニアリングできるメンバーを大募集しています!
少しでも興味を持って頂いた方は、是非カジュアル面談にお越しください。
新卒採用
キャリア採用
皆様とお話できるのを楽しみにしております!!