開発Branchごとの個別環境(Adhoc Dev環境)を構築し、開発フローを改善した話
はじめに
この記事は「Eureka Advent Calendar 2023」10日目の記事です。
お久しぶりです。Site & Data Reliability Engineer (以下、SDRE )の ogady です。
引き続きEurekaでSREをやりつつ、最近データ分析基盤の業務も増え日々なんもわからんながらキャッチアップを進めています。
今回の記事ではSREとして今年取り組んできた開発Branchごとの個別環境(社内ではAdhoc Dev環境と呼ばれています)についてお話ししようと思います。
目次
- 対象読者
- 前提
- これまでのPairs開発環境と課題
- 開発Branchごとの個別環境構築
- 環境ごとにlog、trace、profileを見れるようにする
- 導入後の利用環境の変化
- まとめ
対象読者
- EKSでインフラを構築している方
- ArgoCDを既に導入している方
- Pull Request環境に興味がある方
前提
- EKS version 1.24
- Argo CD version 5.16.2
- Istio version 1.19.3
これまでのPairs開発環境と課題
本題に行く前に、これまでのPairsにおける開発環境とその課題についてお話しします。
Pairsはモノリスアプリケーションで、AWS上で動作しています。環境はstage環境、production環境の2つしかありませんでした。その上で、開発者は基本的に下記のようなフローでPairsの開発環境での検証を行ってきました。
- feature branch を切る(local作業)
- feature branchをstage branchにmergeする(local作業)
- stage branchをremoteにpushする(local作業)
- git pushをキックにstage環境に自動デプロイされ、stage環境で動作検証を行う
一方で、QAもstage環境で実施されています。開発者が自身の動作検証のためにstage環境にデプロイすると、関係のない変更がQA中の環境に撒かれてしまいます。すると、想定外の挙動を確認するためにQAチームとのコミュニケーションが都度発生してしまったり、その不具合を治す間QA作業が一時的に止まってしまうなどの問題を生んでいました。QA内容と関係のない変更がデプロイされる状況は、QAによる品質担保がしにくい状態もなっていました。また、QAチームだけでなく、他の開発者が行っている動作検証にも影響を与えることもあります。
このように、『stage環境しか検証環境がないゆえに開発者は検証のためにstage環境にデプロイするしかない。結果として他の開発者やQA作業にその変更の影響を与えてしまう』という課題は、明らかに開発速度を低下させていたため、この解決として開発Branchごとの個別環境を構築することにしました。
開発Branchごとの個別環境構築
ここから本題です。
まずは開発Branchごとの個別環境(以下Adhoc Dev環境)全体像を示します。
PairsはEKS on EC2上にホスティングされています。Adhoc Dev環境あくまで動作するアプリケーションの個別環境となっており、DBなどのリソースはstage環境と共有しています。
続いて各ステップについて詳細に説明します。
step1
ここは従来通りbranchを切って開発し、GitHubにpushするだけです。
step2
step2では、アプリケーションコードを管理しているrepositoryのGitHub Actionsを開発者が実行することでcontainer imageのbuild、ECR pushを行い、最後にkubernetes manifestsを管理しているRepositoryのGitHub Actionsをキックします。
step3
キックされたkubernetes manifests RepositoryのGitHub Actionsでは、Adhoc Dev環境用のmanifestファイルを作成、Istio VirtualServiceの編集を行います。
実際には下記のような処理を行っています。
- stage環境のmanifestsをすべて作業ディレクトリにコピー
- branch名から環境ごとのID(
git_env_id
と呼んでいます)を生成 - manifestsをyqで編集(container image tagを更新、各種slack通知用metadata追加、namespace/hostの変更)
- Istio VirtualServiceをyqで編集
- ③、④の成果物を各Adhoc Dev環境のNamespace単位でディレクトリを切ってコピー
- 変更内容をcommit、main branchへmerge
一部抜粋ですが、愚直にこんなことをしています。(ファイル名等はサンプルです)
on:
repository_dispatch:
types: [delivery-stage-adhoc-dev]
jobs:
delivery-dev:
runs-on: ubuntu-latest
steps:
############ 中略 ############
# adhocDevで使うstageリソースを必要なものだけ選択してworkspaceにコピーする(virtula-serviceは除く)
- name: copy stage resource files for adhoc dev
id: copy_stage_resource_files_for_adhoc_dev
run: |
mkdir -p ./workspace/pairs
cp -r ./kustomize/overlays/stage/pairs/ ./workspace/
rm ./workspace/pairs/virtual-service-pairs.yaml
ls -la ./workspace/pairs
# adhoc devの構築に必要な編集を行う
- name: edit adhoc dev resource files
id: edit_adhoc_dev_resource_files
env:
DELIVERY_TARGET: adhoc-dev-${{ github.event.client_payload.service }}
run: |
# rollouts.yamlの修正
## annotationsにurlを追加
yq -i '.metadata.annotations["adhoc_url"] = "${{ env.DEV_URL }}"' \\
$(yq '.'$DELIVERY_TARGET'.filePath' ${{ env.DELIVERY_SETTINGS_YAML_PATH }})
############ 中略 ############
# namespace設定の変更
## namespace.yamlの編集
yq -i '.metadata.name = "${{ env.NAME_SPACE }}"' ./workspace/pairs/namespace.yaml
yq -i '.metadata.labels.name = "${{ env.NAME_SPACE }}"' ./workspace/pairs/namespace.yaml
## config.jsonの編集
yq -i '.name = "${{ env.NAME_SPACE }}"' ./workspace/pairs/config.json
yq -i '.path = "kustomize/overlays/stage/pairs-dev/${{ env.NAME_SPACE }}"' ./workspace/pairs/config.json
yq -i '.namespace = "${{ env.NAME_SPACE }}"' ./workspace/pairs/config.json
# configmap.yamlの編集
yq -i '.data.API_URL = "${{ env.DEV_URL }}"' ./workspace/pairs/configmap.yaml
yq -i '.data.GIT_ENV_ID = "${{ env.ENV_ID }}"' ./workspace/pairs/configmap.yaml
# VirtualServiceをTemplateを使って作成する
- name: edit virtual service
id: edit_virtual_service
run: |
TEMPLATE_FILE="./adhoc-dev/virtualservice-pairs.yaml.template"
yq -i '.metadata.name = "dev-${{ env.ENV_ID }}"' $TEMPLATE_FILE
yq -i '.spec.hosts[0] = "${{ env.DEV_HOST }}"' $TEMPLATE_FILE
yq -i '.spec.http[0].name = "dev-${{ env.ENV_ID }}"' $TEMPLATE_FILE
yq -i '.spec.http[0].route[0].destination.host = "service-pairs.${{ env.NAME_SPACE }}.svc.cluster.local"' $TEMPLATE_FILEE
step4
manifestsのEKS Cluster反映にはArgoCDを使用しています。
ArgoCD ApplicationSetを使用しているため、step3でディレクトリを切る際にディレクトリに配下に config.json
を配置することでArgoCDがAdhoc Dev環境ごとにNamespaceごとリソースを作成します。
ingressのトラフィックコントロールはAWS Load Balancer Controller(TargetGroupBinding) + Istio ingress gateway + Istio VirtualServiceで構成しており、個別環境ごとにHostを設定することができます。Route53にワイルドカードでレコード設定し、ALBにはワイルドカード証明書を使用することで、最終的には https://{環境ID(git_env_id)}.dev.sample.com
のような個別エンドポイントでアクセス可能になります。
step5
ArgoNotificationsの仕組みで開発者に作成された環境のドメインなどを通知します。
実際にはこのようなDeploy完了通知がslackに投稿され、master branchとのdiffや、Adhoc Dev環境のLogのリンク、Adhoc Dev環境へアクセスするためのエンドポイントURLを知ることができます。
step6
開発者は通知されたエンドポイントURLにアクセスし、動作検証を行うことができます。
iOSのTestFlightアプリや、Android AppDistribution、ブラウザ版のデバッグ機能からAPIエンドポイントを変更してアクセスすることが可能になっています。(画像はブラウザ版の開発環境デバッグ機能)
環境ごとにlog、trace、profileを見れるようにする
環境が分かれているのであれば、logやtrace、profileなども環境ごとに分離して見ることができるのが理想です。
そのため、アプリケーションコードを改修し、環境ごとのID( git_env_id
)を付与することでlogやtrace、profileを各環境ごとに分けて見ることができるようにしています。
Logger
func (l Logger) output(ctx context.Context, severity, format string, v ...interface{}) {
############ 中略 ############
// for filtering logs by environment
entry.Data["git_env_id"] = config.GetGitEnv().EnvID
entry.Logf(level, l.title, l.args...)
}
Tracer(datadog dd-trace-goを利用)
func Start(name string) func() {
opts := []tracer.StartOption{
tracer.WithService(name + "/" + config.Get().Country),
tracer.WithRuntimeMetrics(),
tracer.WithAgentAddr(config.Get().Datadog.TraceAgentAddr),
}
############ 中略 ############
// adhoc-devの時(環境変数にgit_env_idに値が入っている時)は、adhoc dev用のattributeを付与する
if config.GetGitEnv().EnvID != "" {
opts = append(opts, tracer.WithServiceVersion(config.GetGitEnv().EnvID))
opts = append(opts, tracer.WithEnv("adhoc-dev"))
} else {
opts = append(opts, tracer.WithServiceVersion(build.Version))
opts = append(opts, tracer.WithEnv(foundation.Mode()))
}
############ 中略 ############
tracer.Start(opts...)
return tracer.Stop
}
Profiler(datadog dd-trace-goを利用)
func Start(name string) (func(), error) {
opts := []profiler.Option{
profiler.WithService(name + "/" + config.Get().Country),
############ 中略 ############
}
// adhoc-devの時(環境変数にgit_env_idに値が入っている時)は、adhoc dev用のattributeを付与する
if config.GetGitEnv().EnvID != "" {
opts = append(opts, profiler.WithVersion(config.GetGitEnv().EnvID))
opts = append(opts, profiler.WithEnv("adhoc-dev"))
} else {
opts = append(opts, profiler.WithVersion(build.Version))
opts = append(opts, profiler.WithEnv(foundation.Mode()))
}
return profiler.Stop, profiler.Start(opts...)
}
このように、各環境ごとに独立したオブザーバビリティを提供することで、開発者が他の環境を気にすることなくデバッグなどを進めることができます。Slackに投下されるDeploy完了通知に含まれるLogのリンクなどは、環境ID( git_env_id
)でフィルタリングされた状態のリンクとなっており、開発者が扱いやすいように工夫しています。
導入後の利用環境の変化
最終的には、PairsのバックエンドAPI、管理画面、BatchをAdhoc Dev環境として立ち上げることができる仕組みを整えました。ほぼ全てのPairsのコアコンポーネントに対応した、ということです。
結果として、Adhoc Dev環境リリース前後のstage環境とAdhoc Dev環境のデプロイ数は下記メトリクスのようになりました。(上: stage環境、下: Adhoc Dev環境)
Adhoc Dev環境の使用増加に伴ってstage環境へのデプロイ数の減少が見て取れ、stage環境にデプロイせずとも開発者が動作検証を行うことができている状態になっているとわかります。
また、実際のAdhoc Dev環境自体も、5〜10環境が常に立ち上がっており、互いに独立した実行環境でこれだけの並行開発を行うことができるようになりました。
利用者の声
まとめ
開発Branchごとの個別環境(Adhoc Dev環境)の導入により、開発者ごとに独立した検証環境の利用ができるようになりました。
Adhoc Dev環境を使用することで、他の開発者、QAチームに影響を与えずに動作検証が可能になり、開発体験、開発速度ともに向上したのではないかと考えています。
この仕組み自体は今年導入したものですが、来年以降の展望として、stage環境を完全にQA専用の環境にするなどの一歩踏み込んだ開発フローや、Project専用の環境を常時立ち上げる仕組みなどに発展させ、当初の課題を根本的に解決しようと考えています。