GitLab CIのReview Appsでブランチごとにレビュー用の環境をデプロイする
Review Apps
Review AppsはGitLab CIの機能の1つで、
アプリケーションの変更をレビューする環境を提供してくれます。
何が嬉しいか?
複数人で開発しているケースにおいて、レビューイ(コードを見てもらう側)は以下の流れになると思います。
- トピックブランチを作成する
- 変更をコミット・プッシュする
- MR(PR)を作成してレビュー依頼を出す
レビュワー側は以下です。
- レビュー依頼のあったブランチをローカルに取得する
- 環境を立ち上げて動作を確認する
- レビュー結果をコメントをする・マージする
プロジェクトの規模によりますが、1・2で手元に環境を用意しないといけないのが結構手間だったりします。
複数人から同時に依頼をもらっている場合だと、頻繁に環境を切り替えていると、
何のレビューをしているのか、環境が意図通りのものなのかわからなくなるかもしれません。
Review Appsでレビュー用の環境を自動で用意することで、こういったわずらわしさから解放されます。
素敵やん!とすぐに飛びつきたい機能ですが、レビュー用の環境を自動で用意するインターフェースが提供されているのみで、仕組みは自前で用意する必要があります。
公式のサンプルではrsyncで実現しているものがあったりしますが、
今回はKubernetesを用いて試してみることにします。
この記事の流れ
- Kubernetes構築
- Helmのインストール
- GitLab Agentの登録
- デプロイ用のNamespaceを作成
- サンプルアプリの準備
- Istioインストール
- Helmチャートのファイルを書く
- CIでデプロイしてみる
- Review Apps
レビュー環境を用意するのがメインコンテンツなので、Review Appsまでが長いです。
なお、実行した環境は以下です。
mac | 13.3.1(Intel) |
Docker | 20.10.14 |
GitLab | https://gitlab.com/ |
1. Kubernetes構築
検証なので、サクッとローカルに構築します。
手段はいろいろありますが、今回はKind(Kubernetes in Docker)でKubernetesクラスターを作成します。
> brew install kind
今回は以下の設定ファイルを用意しました。
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30950
hostPort: 80
- role: worker
- role: worker
- role: worker
> kind create cluster --name review-apps --config config.yaml
> kubectl get nodes
NAME STATUS ROLES AGE VERSION
review-apps-control-plane Ready control-plane 11m v1.26.3
review-apps-worker Ready <none> 10m v1.26.3
review-apps-worker2 Ready <none> 10m v1.26.3
review-apps-worker3 Ready <none> 10m v1.26.3
Dockerさえ入っている環境であれば、このようにサクッとKubernetesクラスターを作成することができます。
類似のminikube等と比較すると、マルチノードクラスターを作成できる点が差別化ポイントでしょうか。
※今回はマルチノードクラスターにした意味は特にありません。
2. Helmのインストール
> brew install helm
後述するGitLab Agentや、レビュー用アプリケーションのデプロイで使用します。
3. GitLab Agentの登録
CIが動作するGitLabとデプロイ先のKubernetesクラスターを接続するためには、
クラスターにGitLab Agentをインストールする必要があります。
無くても接続することは可能ですが、ローカルにお試しで構築したクラスターであれば外部公開していないと思うので必須になるでしょう。
GitLab Runnerをセルフホストするのと同じイメージになります。
以下の記事にとてもわかりやすくまとめられていました。
- GitLabでプロジェクトを作成します。
今回はreview-apps-sample
という名前で作成しています。 -
.gitlab/agents/<agent_name>/config.yaml
というパスにファイルを作成します。
config.yamlは空でOKです。 - GitLab AgentをHelmでインストールします。
コマンドは 「Connect a cluster」 ボタンからコピペ可能です。
helm repo add gitlab https://charts.gitlab.io
helm repo update
helm upgrade --install review-apps gitlab/gitlab-agent \
--namespace gitlab-agent-review-apps \
--create-namespace \
--set image.tag=v16.0.0-rc1 \
--set config.token=XXXXXXXX \
--set config.kasAddress=wss://kas.gitlab.com
画像のようにAgentが登録されればOKです。
4. デプロイ用のNamespaceを作成
defaultでもいいのですが、せっかくなのでデプロイ環境となるNamespaceを準備します。
> kubectl create namespace review-apps-sample
5. サンプルアプリの準備
今回は、この手の検証でおなじみのNginx
でやっていきます。
ディレクトリ構成は以下です。
├── .gitlab
│ └── agents
│ └── review-apps
│ └── config.yaml
├── .gitlab-ci.yml
├── Dockerfile
├── README.md
├── helm
│ ├── Chart.yaml
│ ├── templates
│ │ ├── deployment.yaml
│ │ ├── gateway.yaml
│ │ ├── service.yaml
│ │ └── virtualService.yaml
│ └── values.yaml
└── index.html
表示されるページファイルはこちらです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Review Apps Demo</title>
</head>
<body>
<h1>Review Apps 1</h1>
</body>
</html>
6. Istioインストール
公式ドキュメントに記載の方法でインストールします。
> curl -L https://istio.io/downloadIstio | sh -
完了後、istioctl
でクラスターにインストールします。
> istioctl install -y
NamespaceにLabelを付与することで、Istio(Proxy)がサイドカーとして各Pod内に自動で起動するようになります。
> kubectl label namespace review-apps-sample istio-injection=enabled
以下のコマンドでWarningが出なければOKです。
> istioctl analyze -n review-apps-sample
✔ No validation issues found when analyzing namespace: review-apps-sample.
Type: LoadBalancerをNodePortに変更する(Option)
基本的にはインストールは上記で完了ですが、
istio-ingressgateway
がType: LoadBalancer
としてデプロイされています。
Kindにはロードバランサーがなく、ドキュメントにもmetallb
で頑張って、と書いてあります。
が、今回はサクッとやりたいのでType: NodePort
で動かすことにします。
以下コマンドで設定を書き換えます。
> kubectl -n istio-system edit svc istio-ingressgateway
spec:
allocateLoadBalancerNodePorts: true
clusterIP: 10.96.146.220
clusterIPs:
- 10.96.146.220
externalTrafficPolicy: Cluster
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- name: status-port
nodePort: 31406
port: 15021
protocol: TCP
targetPort: 15021
- name: http2
# kindのextraPortMappingsのcontainerPortに合わせる
nodePort: 30950
port: 80
protocol: TCP
targetPort: 8080
- name: https
nodePort: 32490
port: 443
protocol: TCP
targetPort: 8443
selector:
app: istio-ingressgateway
istio: ingressgateway
sessionAffinity: None
# Loadbalancer -> NodePort
type: NodePort
status:
loadBalancer: {}
7. Helmチャートのファイルを書く
├── helm
│ ├── Chart.yaml
│ ├── templates
│ │ ├── deployment.yaml
│ │ ├── gateway.yaml
│ │ ├── service.yaml
│ │ └── virtualService.yaml
│ └── values.yaml
Chart.yaml
とvalues.yaml
はあまり関係ないので省略します。
deployment.yaml
, service.yaml
もいつものやつ、って感じなので特に説明はなしです。
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
spec:
replicas: 1
selector:
matchLabels:
app: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ .Release.Name }}
spec:
containers:
- name: review-app
image: doncool340/review-apps-sample:{{ .Values.tag }}
service.yaml
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}
spec:
type: ClusterIP
selector:
app: {{ .Release.Name }}
ports:
- name: http
protocol: TCP
targetPort: 80
port: 80
gateway.yaml
Ingressを担うやつです。
helm install
で指定されるhost
に対応するGatewayになります。
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: {{ .Release.Name }}-gateway
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- {{ .Values.host }}
virtualService.yaml
先ほどのgatewayをgateways
に指定することで紐付けができます。
これで、hostが.Values.host
のリクエストがServiceに流れるようになります。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: {{ .Release.Name }}
spec:
gateways:
- {{ .Release.Name }}-gateway
hosts:
- {{ .Values.host }}
http:
- route:
- destination:
host: {{ .Release.Name }}
8. CIでデプロイしてみる
CIの流れは、DockerイメージをビルドしてDockerHubにPush、その後にHelmインストールするようにしています。
今回はサンプルなのでhost
は適当な値です。
stages:
- build
- deploy
build:
stage: build
image: docker:23.0.5
services:
- docker:23.0.5-dind
variables:
DOCKER_HOST: tcp://docker:2375
before_script:
- echo ${DOCKERHUB_TOKEN} | docker login -u ${DOCKERHUB_USER} --password-stdin
script:
- docker build -t <docker_hub_repository>:${CI_COMMIT_REF_SLUG} .
- docker push <docker_hub_repository>:${CI_COMMIT_REF_SLUG}
deploy:
stage: deploy
image: alpine/k8s:1.24.13
script:
- kubectl config use-context <group>/review-apps-sample:review-apps
- helm upgrade -i -n review-apps-sample review-apps-sample-${CI_COMMIT_REF_SLUG} ./helm --set tag=${CI_COMMIT_REF_SLUG} --set host=review-${CI_COMMIT_REF_SLUG}.sample-app.local
※<docker_hub_repository>
は環境に応じて適切な値を入れます。
deployのジョブにてkubectlを実行していますが、
エージェントが正常に登録されていれば問題なく実行できると思います。
<group>/<project>:<agent>
というコンテキストで操作する必要がある点がハマりポイントです。
CIが成功すると、ローカルPC上のクラスターにデプロイされています。
コンテナが2/2になっているのはIstioがPodに注入されているためです。
> kubectl -n review-apps-sample get pod
NAME READY STATUS RESTARTS AGE
review-apps-sample-main-65745549c8-bnjg4 2/2 Running 0 78s
上記はmainブランチでのCIのため、CI_COMMIT_REF_SLUG
にmainが入ってこのような名前になっています。
確認する
では、リクエストを送信してみましょう。
> curl -H "Host:review-main.sample-app.local" localhost -v
* Trying 127.0.0.1:80...
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET / HTTP/1.1
> Host:review-main.sample-app.local
> User-Agent: curl/7.87.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< server: istio-envoy
< date: Sat, 06 May 2023 12:34:10 GMT
< content-type: text/html
< content-length: 293
< last-modified: Fri, 05 May 2023 23:24:03 GMT
< etag: "64559013-125"
< accept-ranges: bytes
< x-envoy-upstream-service-time: 0
<
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Review Apps Demo</title>
</head>
<body>
<h1>Review Apps 1</h1>
</body>
</html>
無事につながりました!
ちなみに、hostを書き換えないリクエストだと、、、
> curl localhost -v
* Trying 127.0.0.1:80...
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.87.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 Not Found
< date: Sat, 06 May 2023 12:33:23 GMT
< server: istio-envoy
< content-length: 0
<
* Connection #0 to host localhost left intact
しっかりNotFoundになっていることが確認できますね。
9. Review Apps
CIからクラスターにデプロイできることが確認できたので、ようやくReview Appsの出番です。
.gitlab-ci.yml
にReviewAppsとしての設定を加えていきます。
stages:
- build
- deploy
build:
stage: build
image: docker:23.0.5
services:
- docker:23.0.5-dind
variables:
DOCKER_HOST: tcp://docker:2375
before_script:
- echo ${DOCKERHUB_TOKEN} | docker login -u ${DOCKERHUB_USER} --password-stdin
script:
- docker build -t <docker_hub_repository>:${CI_COMMIT_REF_SLUG} .
- docker push <docker_hub_repository>:${CI_COMMIT_REF_SLUG}
setup_review:
stage: deploy
image: alpine/k8s:1.24.13
script:
- kubectl config use-context <group>/review-apps-sample:review-apps
- helm upgrade -i -n review-apps-sample review-apps-sample-${CI_COMMIT_REF_SLUG} ./helm --set tag=${CI_COMMIT_REF_SLUG} --set host=review-${CI_COMMIT_REF_SLUG}.sample-app.local
environment:
name: review/$CI_COMMIT_REF_NAME
url: http://review-${CI_COMMIT_REF_SLUG}.sample-app.local/
on_stop: teardown_review
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
teardown_review:
stage: deploy
variables:
GIT_STRATEGY: none
image: alpine/k8s:1.24.13
script:
- helm uninstall -n review-apps-sample review-apps-sample-${CI_COMMIT_REF_SLUG}
when: manual
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
setup_review
がMR作成時に実行されるジョブになります。
environment
にデプロイ先のURL等の情報を与えることで、GitLab上でenvironmentが作成されるようになります。
では、test_2
ブランチを作成してMRを作ってみます。
わかりやすいようにhtmlを変更しておきましょう。
<body>
<h1>Review Apps 2</h1>
</body>
CIが成功すると、MR画面上にリンクボタンが作成されます。
curlで確認してみると、先ほどのtest_2
ブランチの内容が返ってきました。成功ですね。
> curl -H "Host:review-test-2.sample-app.local" localhost
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Review Apps Demo</title>
</head>
<body>
<h1>Review Apps 2</h1>
</body>
</html>
不要な環境の整理
今回デプロイした環境は、MRがクローズされる=レビューが終わると不要になります。
いつまでもクラスター上に残り続けるとリソースが圧迫されますし、手動で消して回るのは面倒ですね。
.gitlab-ci.yaml
のteardown_review
というジョブに注目してください。
setup_review:
stage: deploy
image: alpine/k8s:1.24.13
script:
- kubectl config use-context <group>/review-apps-sample:review-apps
- helm upgrade -i -n review-apps-sample review-apps-sample-${CI_COMMIT_REF_SLUG} ./helm --set tag=${CI_COMMIT_REF_SLUG} --set host=review-${CI_COMMIT_REF_SLUG}.sample-app.local
environment:
name: review/$CI_COMMIT_REF_NAME
url: http://review-${CI_COMMIT_REF_SLUG}.sample-app.local/
on_stop: teardown_review
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
teardown_review:
stage: deploy
variables:
GIT_STRATEGY: none
image: alpine/k8s:1.24.13
script:
- kubectl config use-context <group>/review-apps-sample:review-apps
- helm uninstall -n review-apps-sample review-apps-sample-${CI_COMMIT_REF_SLUG}
when: manual
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
teardown_review
は環境を破棄するためのジョブです。
setup_review
のenvironment.on_stop
と紐づけることで、MRのクローズや画面上の停止ボタンをトリガーにジョブが動きます。
では、停止ボタンを押してみましょう。
helm uninstall が実行され、クラスターからリソースが削除されました。
まとめ
KubernetesクラスターとReview Appsを用いて、レビュー用の環境をいい感じに整備する検証をやってみました。
アプリケーションのコンテナ化対応だったりKubernetesクラスターの構築だったり、
実現できるまでのハードルは少しあるのですが、使いこなせると非常に便利な機能だと思います。
Discussion