ブランチのpushで環境を動的に作成する開発環境を作った話

April 13, 2020

この一ヶ月 mixi で就業型のインターンに参加していました。

インターンに関しては以下の記事にまとまっています。

Dive into mixi GROUP 2020 に参加してきた話

インターンではUnlimというスポーツギフティングのサービスの開発を行うチームに配属され、 master に PR を向けただけでブランチの環境にアクセスできるようになるデプロイフローの開発 に取り組みました。

この記事では技術的に課題の解決に向けてどのように実装を進めたかを紹介したいと思います。

現状の開発フロー

Unlim では元々

  1. master にマージされる前に staging/unstable ブランチに一旦マージしてみる。
  2. staging/unstable にマージされると CircleCI が回って staging/unstable 環境のデプロイが実行される。
  3. staging/unstable 環境で OK そうなら master にマージ

と言う流れで開発が行われていました。

現状の開発フローの問題点

上記の開発の流れには以下の問題点があります。

  • unstable/staging は複数人で共同で使用するため、開発中の様々な差分が含まれる
  • unstable/staging に障害が起こると QA 出来ず開発がストップする

目指すべき姿

他人の差分からの影響を受けないようにブランチごとに環境が作成されることで以上の二点に関して解決が図れます。

具体的には

  1. master に PR を向けた時点で、ブランチの環境が EKS にデプロイされる(されている)
  2. ingress も動的に更新し、ブランチ名を含む URL でその環境にアクセスできる
  3. ただし既存の staging/unstable のデプロイフローは壊さない

という状態を目指します。 これによりブランチごとに個別の環境を作成でき正しく QA が行える状態になります。

デプロイフロー

既存のデプロイフローはこのような形です

client.png

また、manifest 管理には Helm を使用しています

  1. staging/unstable に commit が積まれると CircleCI が回る
  2. image を build して ECR に push する
  3. Helm Chart を apply で各 Pod, Deployment, Service, Ingress を更新(ECR から先ほどの image を pull する)
  4. Ingress に変更があれば AWS ALB Ingress Controller を通して ALB, Route53 が更新

という流れになります。

細かい要件定義

Unlim では server 側と client 側でリポジトリが分かれています。

技術としては server 側:  Elixir / Phoenix client 側:  TypeScript/Nuxt を使用しています。

そのため、server 側のリポジトリ、client 側のリポジトリでそれぞれ環境を動的に作成する開発環境を考える必要がありました。

最終的にそれぞれどのような要件にしたかを紹介していきます

client

Client 側では最終的には最終的に以下のように環境にアクセスできることを目指します

Untitled Diagram.png

全てのブランチで 1 つの API サーバーを共有しています

デプロイフローは以下のようになります

  1. feature-*のブランチの commit で CircleCI を回す
  2. image を build して ECR に push する
  3. remote に存在する feature-* のブランチを全て取得
  4. Helm Chart を apply で Pod, Deployment, Service を更新(ECR から先ほどの image を pull する)
  5. 4 と一緒に Ingress も更新し、ブランチ名を含む URL でアクセスできるように設定する
  6. Ingress の設定の差分により、ALB, Route53 が更新される

clicli.png

という流れになります。

また、server 側、client 側共にデプロイ完了で Slack に通知が行くようになっています IMG_6903.JPG

1 つずつ詳細を見ていきます

feature-*のブランチの commit で CircleCI を回す

master に向けた PR を作成した時に環境を確認したいということが目的でした。

PR を作成した時に初めて CircleCI を回すと初回のデプロイには 10 分ほどかかってしまうという難点がありました。 解決のため、自動デプロイを行いたいブランチには feature-* と名付けて、PR を向けた時ではなく commit が積まれた時にデプロイを回すという方針にしました。

image を build して ECR に push する

この push の際にはブランチ名をタグに付けます

Helm Chart を apply で各リソースを更新

ここでは 2 つの方針を検討しました。

  1. 全てのブランチのリソースを毎回更新する(実際には差分なしなので更新はされない)
  2. CircleCI を回しているブランチ分だけ新たに Helm install する

ここは、以下の理由により、1 を採用しました。具体的には、values として branchs を受け取り、全てのブランチのリソースを一括で扱うような Helm Chart を作成しました。1

  • 同時に別のブランチの commit が push された時不整合が起きる可能性がある
  • merge もしくは delete されたブランチの環境は適宜削除したい

1 つ目は現在の Ingress の設定を取得し、CircleCI を回しているブランチ分の設定を足して apply するという手法をとった時に同じタイミングで複数のブランチの commit が push された時にどちらか一方の設定が反映されないという可能性です。 解決方法としては DB のロックと同様に、同時に CircleCI の workflow を複数のブランチが回せないように設定するという方法もありましたが、効率を考えると厳しいです。

2 つ目に関しては CircleCI で merge は拾えても delete をフックできないという問題です。ブランチが merge されず delete された場合に余計な Pod などが残ること、Ingress に余計な設定が残ることなどの問題を防ぐためには delete されたブランチが無いか定期的にチェックする必要があります。

この 2 つの理由から毎回このタイミングで remote にある feature-* のブランチの情報を取得し、HelmChart に Values として渡し、更新させるという手法を取りました。

Helm Chart 内では引数として渡された複数の branch を for 文でループして取り出していき、渡された branch 分のリソースを作成する manifest を生成します。 helm applyでは差分のみが適応されるので

  • すでに一度 commit を積んでいるブランチの分の manifest は差分なし
  • 現在 CircleCI を回しているブランチの初 push だった場合は追加の差分が出る
  • delete もしくは merge されたブランチの文は削除の差分が出る

という風な動きになります

Ingress の設定の差分により、Route53 が更新される

Ingress には client-BRANCH.hoge.com というアドレスでアクセスできるように設定をしておきます。 この設定が AWS ALB Ingress Controller を通して Route53 などを更新し、実際にアクセスが可能になります。

server

Server 側では最終的に以下のように環境にアクセスできることを目指します

update.png

デプロイフローは以下のようになります

  1. feature-*のブランチの commit で CircleCI を回す
  2. image を build して ECR に push する
  3. remote に存在する feature-* のブランチを全て取得
  4. Helm Chart を apply で各 Pod, Deployment, Service を更新(ECR から先ほどの image を pull する)
  5. 4 と一緒に Ingress(Server 用)を更新し、ブランチ名を含む URL でアクセスできるように設定する
  6. 4 と一緒に Ingress(Client 用)を更新し、ブランチ名を含む URL でアクセスできるように設定する
  7. Ingress の設定の差分により、ALB, Route53 が更新される

Untitled Diagram (2).png

殆ど Client 側から変更は無いように見えますが、登場する Ingress が 2 つあります。

Client 側と異なる部分を説明していきます

Ingress(Server 用)を更新

Server 側の API などの URL を設定します。 api-server-BRANCH.hoge.comという URL で API サーバーのエンドポイントを設定します

Ingress(Client 用)を更新

Client 側の URL を設定します。 server-BRANCH.hoge.comという URL で BRANCH の API サーバーを使用する Client にアクセス出来ます。

Client の API の向きの決定について

update.png (先ほどの図)

Server 側、特に API に変更を加えた時などには Client と組み合わせた状態の QA が必要になります。 Client は 1 つの Pod を全ての Server 側の Client として流用するようにしました。 (上記の Ingress(Client用)を更新 では server-BRANCH.hoge.com という URL で共通 Client を結びつけるという更新をしていたわけです)

共通 Client ではどの URL からアクセスがきたかを window.location.hostname で取得し、どの API にリクエストを投げるかを動的に決定しています。

改善点

時間&僕の技術力が足りないという理由から以下の改善点が残っています。

  • window.location.hostnameを使いたいがために、共通 Client は SPA モードで build した、ちゃんと universal モードでも対応できるようにしたかった
  • ingress の apply を最後に apply するため、初回のデプロイ →URL が浸透してアクセスできる状態までしばらく時間がかかる。
  • Server 側で DB に同じものを使用しているので誰かが migrate を走らせると他の環境が壊れる
  • DB も Pod にのせて使い捨てにしたかった

終わりに

3 月の頭には Docker も Kubernetes も知らないような状態の僕がここまでの開発を経験させてもらえてとても感謝しています。

具体的には

  • Docker, Kubernetes とはなんぞや
  • CiecleCI、CI/CD とはなんぞや

と言った基本的な部分から

  • どのように manifest, リソースが管理されるか(Helm, ArgoCD, Terraform)
  • アプリケーションがどのようにインフラ的に組み合わさって動作しているか

などまで本当に幅広く濃い内容を学ぶことができました。

最高のインターンでした!


  1. Helm Chart では、Values として引数を定義し、その引数を用いて manifest を書いていくという Templete の機能があります。apply 時に --set のオプションをつけることで引数に値を渡せます。Helm に関して詳しくはこちらが参考になるかもしれません

このエントリーをはてなブックマークに追加