こんにちは!!
vivit で SRE をやっている 宮本 です。
今回は hinata レンタル で行っている E2E テストを GitHub Actions で実行できるように CI パイプラインを構築したので、構成やポイントなどを紹介したいと思います!!
要件とか
- コミットを GitHub に push したタイミングで E2E テストを実行する
- E2E テストなので、依存する複数のマイクロサービスも立ち上げる必要がある
- マイクロサービスのソースコードは CI を実行するリポジトリとは別のリポジトリに存在するものもある
- 多少実行時間がかかったとしても、なるべくマイクロサービスをモックにすることなく、本番に近い環境でテストすることを優先する
- vivit は開発環境、本番環境共に k8s を使っているので、CI上も k8s で環境を作れると嬉しい
- E2E テストのプロセス自体は k8s 上ではなく CI 内で直に実行する
- バックエンドマイクロサービスが使用するテストデータは簡単に変更できるようにする
vivit の技術スタックについては 、
vivit.hatenablog.com
をご参照ください 🙏
実際の構成
全体の構成を説明するのに不要な部分は削除したり、適宜内容を変えています。
そのままコピペしても動きませんので、ご了承ください。
ざっと以下のような構成となります!
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
- 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 が完了したらタグをテスト用に書き換えて終了です。
これをテスト実行に必要なマイクロサービスの数だけやっていきます!
- name: start minikube
id: minikube
uses: medyagh/setup-minikube@master
いよいよ、CI 内で k8s クラスターを立ち上げます 🎉
今回の場合、シングルノード構成で問題無いので、お手軽にクラスターを構築する為に minikube を利用してみました!
minikube.sigs.k8s.io
公式で紹介されている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 のエンジニアは今回紹介したような新しい取り組みを積極的に行っており、一緒にエンジニアリングできるメンバーを大募集しています!
少しでも興味を持って頂いた方は、是非カジュアル面談にお越しください。
新卒採用
www.wantedly.com
キャリア採用
www.wantedly.com
www.wantedly.com
www.wantedly.com
皆様とお話できるのを楽しみにしております!!