SREのたっち(@TatchNicolas)です 本記事は、先日の弊社主催のTechトークイベントで発表した内容について、もうすこし詳細に書いてみます。
TL;DR;
- GitLabのenvironmentとHelmのreleaseを対応させることで動的な環境の作成・削除を実現した
- CIOpsとGitOpsを使い分けて、「ちょうどいい」使用感を目指した
- 環境の順番待ちがなくなった!
背景
JX通信社のサーバサイド開発では、ECSによるコンテナベースでの開発・デプロイが主流です。*1
環境としては
- ローカル環境: docker-composeや
go run
yarn run
などで起動する、文字通り手元のマシンのローカル上で動かす環境 - 開発版環境: SQSやFirestoreなどのマネージドサービスや、ローカルで立てるのが大変な別のAPI*2との繋ぎ込み等に利用する環境
- ステージング環境: 本番デプロイ前のチェックに使う環境
- 本番環境: エンドユーザが実際に利用する環境
という構成になっています。
しかし、開発チームの規模が大きくなってくると開発版環境を使った繋ぎ込みの確認のために順番待ちが発生するようになり、Slack上でも「@here どなたか開発版環境つかってます?借りていいですか?」のようなやりとりが散見されるようになりました。結果、思うように開発スピードが上がらなくなってしまいました。
そこで、開発が進行している複数のトピックブランチごとに独立してコンテナのアプリケーションをデプロイできると、開発版環境の順番待ち問題を解決 できるのではないか?と考えました。
どうやって実現しているか
この問題を解決するには、以下の要件が満たされている必要があります。
- 環境が自動で作成され、独立したURLが動的に払い出される
- Merge requestが閉じられたら、その環境も閉じられる
そのために、最近社内でも導入の進んでいるKubernetesをベースにすれば、比較的簡単に実現可能と考え、以下の技術選定をしました
- URLの払い出し: Istio
- 自動的な環境作成・削除: GitLab CI & Helm
構成について
全体の構成を図に表すと以下のようになります。登場人物についてもう少し詳しく解説します。
Ingress
トラフィックの制御はIstioに任せるため、全てistio-ingressgatewayに流すようにしています。 そのため、図からは省略しました。
「クライアントからのリクエストのTLSを終端して、k8sクラスタの中へリクエストを連れて行く」ことを担当します。
GKEの場合は、GCPから発行される証明書がワイルドカードに対応していないため、cert-managerで発行したものをIngressで指定してLoadBalancerに持たせます。
EKSの場合は、AWS Load Balancer Controllerを利用しました。
Istio
「k8sクラスタの中に入ってきたリクエストを、Hostヘッダをみて環境ごとのk8s Serviceに振り分ける」を担当します。Ingressのルールでも同じことは可能ですが、Istioを使うことで以下のメリットを享受できます。
- ホスト名やパスなどによるリクエスト振り分けをVirtualServiceで設定することで、Ingressの中央集権ではなく個々のアプリケーション単位で行えるようになる*3
- 認証周りをアプリケーションから剥がすことができる
GitLab CI
「トピックブランチごとの環境の作成・削除」を担当します。
GitLabにもKubernetesクラスタと連携する機能はあるのですが、GitLabのお作法に従う必要が出てくるためあまり使っていません。
JX通信社では、プロダクト単位でGitLabのグループを分けており、その単位でクラスターを作成しているので、開発版環境のクラスタの情報を登録して、GitLab CIからkubectl
や helm
のコマンドを叩けるようにしました。*4
ArgoCD
「各クラスタに対するメインブランチの反映」を担当します。詳しくは後述
実際のワークフロー
(1) 開発者が、ある機能の実装のためにfeatureブランチを切ってpush
各アプリケーションに対応するmanifestsリポジトリを用意して、そこでブランチを切ります。
(2) GitLab CIがfeatureブランチに対応する環境を作成する
ここで、下記のCI Jobが発火します。
setup_mr_env: stage: mr_env image: docker-image-with-helm script: - helm upgrade -i -n fastalert-api ${CI_COMMIT_REF_SLUG} ./chart --set url=${CI_ENVIRONMENT_URL} environment: name: ${CI_COMMIT_REF_SLUG} url: https://${CI_COMMIT_REF_SLUG}.your-domain.com on_stop: teardown_mr_env only: - branches except: - main - tags
利用するHelmテンプレートは以下のようになっています。*5
chart ├── Chart.yaml ├── values.yaml └── templates ├── NOTES.txt ├── _helpers.tpl └── main.yaml
chart/values.yaml
と helm upgrade -i ... --set xxx=yyy
で値を渡していきます。
apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }} spec: replicas: 1 selector: matchLabels: app: {{ .Release.Name }} template: metadata: labels: app: {{ .Release.Name }} spec: serviceAccountName: your-app-serviceaccount containers: - name: api image: {{ .Values.image }} ports: - containerPort: 8000 --- apiVersion: v1 kind: Service metadata: name: {{ .Release.Name }} spec: type: ClusterIP selector: app: {{ .Release.Name }} ports: - name: http protocol: TCP targetPort: 8000 port: 80 --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: {{ .Release.Name }} spec: gateways: - istio-system/gateway hosts: - {{ (urlParse .Values.url).host }} http: - route: - destination: host: {{ .Release.Name }}.your-app-ns.svc.cluster.local
(3) レビュー担当者は、コードを確認しつつ2.でできた環境を触って動作確認したりする
前述のマニフェストが反映されたら、 https://${CI_COMMIT_REF_SLUG}.your-domain.com
というURLでアプリケーションにアクセスできるようになります。
また、正式なレビューに至る前のちょこっと書き換えて挙動を確認して、のようなことをするなら逐一CI越しのデプロイをせずにTelepresenceを使います。
(4) LGTMをもらってMergeすると、トピックブランチ用の環境は自動で削除される
GitLab上で、
- Merge Requestに表示される停止ボタンをクリックする
- Pipeline上で表示されるmanualなJobを実行する
- Merge Requestが閉じられる
のいずれかの操作を行うと、下記のCI Jobが発火します。これにより、2.
で作成されたhelm releaseがアンインストールされます。*6
teardown_mr_env: stage: mr_env variables: GIT_STRATEGY: none image: docker-image-with-helm script: - helm uninstall -n fastalert-api ${CI_COMMIT_REF_SLUG} when: manual environment: name: ${CI_COMMIT_REF_SLUG} action: stop except: - main - tags
(5) (6) ArgoCDがステージング・本番用ブランチの内容を同期する
ここだけ、GitOpsな方針になっています。詳しくは後述します。
もう少し細かい話
リポジトリ戦略について
JX通信社では、1つのアプリケーション(≒マイクロサービス)を1つのGitLabプロジェクト(=リポジトリ)に対応させ、その中でソースコードとデプロイの両方を管理しているケースがほとんどでした。
今回、Kubernetesを導入するにあたって、アプリケーションとデプロイの関係を疎にするために
- アプリケーションリポジトリ:
- CIではlint、テスト、コンテナイメージの作成までを担当する
- マニフェストリポジトリ:
- 開発版クラスタのトピックブランチ別環境へのデプロイのデプロイ
- ArgoCDがこのリポジトリのmainブランチをクラスタに同期する
という構成にしました。マニフェストリポジトリのほうはモノレポではなく、デプロイしたい単位に分けています。するとアプリケーションとマニフェストを一対一に対応させたり、複数のAPIのセットをまとめて一つのデプロイの単位としても扱えたり、柔軟な運用が可能になりました。
CIOps v. GitOps
CIOpsとGitOpsの特徴などについては、すでに良い記事が世の中にたくさんあるので、ここでは説明を割愛しますが、「CIとCD」。
- プロダクト開発チームとしては、どんどんコードを書いてデプロイしたい
- でも本番環境はバージョン管理もセキュリティもしっかりしたい
そこで、JX通信社では
- 開発版クラスタの各トピックブランチに対応する環境にGitLab CIからデプロイ(=CIOps)
- 開発版クラスタおよび本番クラスタのmainブランチは、ArgoCDでデプロイ(=GitOps)
のように、環境に応じてデプロイ方式を変える方式を採用しました。すると、
- 開発版トピックブランチへの作業はpushするたびに随時反映されていき、グイグイと開発を進められる
- 本番環境クラスタに対する権限をGitLab CIに渡していないので、よりセキュアに保てるようになる
という環境ごとの目的にマッチした利用が可能になりました。
Namespace・ServiceAccountについて
EKS/GKE上で動くアプリケーションは、マネージドサービスを利用するためにWorkload Identity/IRSAを利用します。そのためには、AWS/GCPのIAMがKSAのあるNamespaceとServiceAccountを知っている必要があります。
そのため、動的に追加/削除されるHelmのtemplateからNamespaceとServiceAccountは除外して、クラスタ自体を管理してるリポジトリで、新しいアプリケーションの追加時にArgoCD Applicationの定義と一緒にNamespaceを定義・作成しています。つまり、各トピックブランチの環境はIAMを共有しています。ここは権限の変更も含めてトピックブランチ環境間で独立させられると理想的なのですが、複雑になってしまうため今回の仕組みからは外しました。
apiVersion: v1 kind: Namespace metadata: name: your-app-ns labels: istio-injection: enabled --- apiVersion: v1 kind: ServiceAccount metadata: annotations: eks.amazonaws.com/role-arn: arn:aws:iam::1234567890:role/your-app-role name: your-app-serviceaccount namespace: your-app-ns --- apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: your-app namespace: argocd spec: project: default source: repoURL: https://your-gitlab.com/your-group/your-project.git path: path/to/chart/ targetRevision: HEAD helm: releaseName: main destination: server: https://kubernetes.default.svc namespace: your-app-ns
まとめ
- 環境の順番待ちがなくなって、複数の施策を並行して進めやすくなった
- CIOpsとGitOpsのいいとこ取りをして、「ちょうどいい」ワークフローを実現できた
- それぞれの環境はあくまで
helm install
/helm uninstall
しかしてないので、中でどんなKubernetesリソースを利用しているのかは関係なくなり、CI/CDの仕組みが標準化できるようになった
以上、先日行ったTechトークの詳細をブログにしてみました。どなたかの参考になれば幸いです。
JX通信社では、2021/06/23(Wed) に二回目のTechトークイベントを予定しています。次回はPythonにまつわる色々な話をする予定ですので、ぜひ参加してみてください。
*1:Serverless FrameworkやSAMを使ったLambdaへのデプロイもよく使われます Serverless Framework+mangum+FastAPIで、より快適なPython API開発環境を作る - JX通信社エンジニアブログ
*2:たとえば、VPCに閉じていて外部から触れない内部向けのAPIなどです
*3:AWS Load Balancer Controllerでは振り分けルールをバラして書くことができますが、GKEでもできるだけ近い方法でやりたかったのと、他にも重み付けやリトライ制御などServiceMeshの機能として使いたいことがあったのでL7ルーティングもIstioに任せました
*4:https://docs.gitlab.com/ee/user/group/clusters/
*5:実際には、IstioのAuthorizationPolicyの設定なども含めていますが省略しています
*6: GIT_STRATEGY: noneを指定しないと、GitLabでマージと同時に元ブランチを削除する設定にしていたときに失敗してしまいます。