Github ActonsとKubernetesを使って効率的にデモ環境を作る
このエントリーは一休.com Advent Calendar 2024の6日目の記事になります。
デモ環境を簡単に作りたい
特定の新機能を関係者にお披露目するためのデモ環境は、簡単に作れて、かつ、修正も素早く反映できると、うれしいです。
- デモ環境へのデプロイは、特別なことをするのではなく、既存の開発ワークフローに混ぜ込みたい。
- デモ環境は、静的な配置場所があるのではなくて、デプロイ時に動的にリソース確保して作成され、使い終わったら、破棄される。静的な配置場所があると、「デモ環境使いたいので使い終わったら教えてください」的なやり取りが生まれますが、これは、なくしたい。
このうち、1.は、Github ActionsでGithubのイベントをトリガにして、既存の開発ワークフローにうまく混ぜ込んでデプロイできそうです。
2.については、Kubernetesを使って、ロードバランサからアプリケーションまで、オンデマンドで作成することで、実現できます。
一休では、静的な配置場所を確保しておき、そこに内製のツールで特定のブランチをデプロイすることでデモ環境を作成するということを実践してきました。が、この方式は、以下の課題があります。
- デモ配置場所の競合や、解放待ちが発生する。
- デプロイに内製ツールが必要でメンテナンスがつらい。
Github Actions + Kubernetesの方式でこの課題も解決できそうです。
デモ環境デプロイ/破棄をトリガするためのGithub ActionsでGithubのイベント
pull_requestイベントを使います。
とはいえ、すべてPRのアクティビティでトリガするのは無駄です。たとえば、必ずしもすべてのPRでデモ環境が欲しいわけではないので、PRをopenしたときにトリガするのは、無駄でしょう。なので、以下のactionsの定義の通りにしました。
name: deploy demo
on:
pull_request:
types:
- labeled
- unlabeled
- synchronize
- closed
jobs:
jobs:
deploy_demo:
if: (github.event.action == 'labeled' && github.event.label.name == 'demo') || (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'demo'))
...Kubernetesにデモ環境をデプロイする(略)...
delete_demo:
if: (github.event.action == 'unlabeled' && github.event.label.name == 'demo') || (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'demo'))
...Kubernetesからデモ環境を削除する(略)...
-
labeled
とsynchronize
でラベルがついたときとPRにコミットが追加されたときにデモ環境をデプロイする処理をトリガします。 -
unlabeled
とclosed
でラベルがはずれたとき、またはPRがクローズされたときに、環境を破棄する処理をトリガします。 - また、
if
で、もう少し、実行条件を細かく調整しています。- 付与されたラベル名が
demo
だったら、デモ環境を作る。または、PRにdemo
という名前のラベルがついていて、かつ、そのPRにコミットが加えられたら、デモ環境を作る。 - 削除されたラベル名が
demo
だったら、デモ環境を削除する。または、たは、PRにdemo
という名前のラベルがついていて、そのPRがクローズされたら、デモ環境を削除する。
- 付与されたラベル名が
あとは、必要なビルドを行ってDockerイメージをKubernetesへデプロイする、または、Kubernetesからデモ環境をする処理をActionsに書けばよさそうです。
Kubernetesにデモ環境をデプロイする
Kubernetesの前提は以下です。
- AWS EKSを使っています。
- external-dnsを使っています。
- aws load balancer controllerを使っています。
- ワイルドカード証明書が使えるデモ用のドメインあり、利用できるようになっています。
- デモ対象のアプリケーションはウェブアプリケーションです。
Kubernetesのマニュフェストファイルを用意します。Deployment
/HorizontalPodAutoscaler
/Service
/Ingress
があればよさそうです。以下のように定義してみます。※説明に不要な部分は簡略化しています。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-1:************:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx
alb.ingress.kubernetes.io/healthcheck-path: /health
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
alb.ingress.kubernetes.io/scheme: internal
alb.ingress.kubernetes.io/target-node-labels: kubernetes.io/os=linux
alb.ingress.kubernetes.io/security-groups: sg-xxxxx
alb.ingress.kubernetes.io/subnets: subnet-xxxxx
external-dns.alpha.kubernetes.io/hostname: demo-${PR_NUMBER}.hogehoge.com
kubernetes.io/ingress.class: alb
labels:
app: demo-${PR_NUMBER}
name: demo-${PR_NUMBER}
namespace: demo
spec:
rules:
- http:
paths:
- path: /*
pathType: ImplementationSpecific
backend:
service:
name: demo-${PR_NUMBER}
port:
number: 3000
---
apiVersion: v1
kind: Service
metadata:
labels:
app: demo-${PR_NUMBER}
name: demo-${PR_NUMBER}
namespace: demo
spec:
ports:
- name: http
port: 3000
protocol: TCP
selector:
app: demo-${PR_NUMBER}
type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: demo-${PR_NUMBER}
name: demo-${PR_NUMBER}
namespace: demo
spec:
progressDeadlineSeconds: 180
revisionHistoryLimit: 10
selector:
matchLabels:
app: demo-${PR_NUMBER}
template:
metadata:
annotations:
labels:
app: demo-${PR_NUMBER}
spec:
containers:
- name: demo-${PR_NUMBER}
args:
- 'node'
- './server/index.mjs'
image: ${IMAGE_NAME}
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000
name: http
protocol: TCP
serviceAccountName: demo
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: demo-${PR_NUMBER}
namespace: demo
spec:
maxReplicas: 1
metrics:
- resource:
name: cpu
target:
averageUtilization: 80
type: Utilization
type: Resource
minReplicas: 1
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: demo-${PR_NUMBER}
ポイントは以下
- すべてのリソースの
labels.app
やname
は、demo-${PR_NUMBER}
にします。${PR_NUMBER}
にはActionsで実際にデプロイするときにPRの番号を埋め込みます。こうすることで、PR単位でデモ環境ができます。 - Ingressのホスト名は
demo-${PR_NUMBER}.hogehoge.com
としています。これも、${PR_NUMBER}
を埋め込んで、PR単位でユニークなホスト名になるようにします。- *.hogehoge.comのワイルドカード証明書がある前提です。
- hogehoge.comは、説明のための架空のドメインです。
- Deploymentのイメージも同様
${IMAGE_NAME}
で、Actionsで実際にデプロイするときに埋め込みます。 - namespaceは、専用のnamespaceを割り当てておいたほうが、何かと都合がいいのでそうしています。
ActionsでKubenetesのマニュフェストに変数を埋め込むのは以下の通りです。demo.yamlという名前で上述のマニュフェストが保存されているとします。
... (前略)ビルドに必要な各種環境のセットアップや、awsのクレデンシャルのセットアップを行う(前略) ...
- name: Set IMAGE_NAME variables
run: echo "IMAGE_NAME=$(echo 'xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/test-app:demo-${{ github.event.pull_request.number }}')" >> $GITHUB_ENV
env:
TIMESTAMP: '${{ steps.time.outputs.time }}'
... (中略) 資材のビルドやDockerイメージビルド/プッシュを行う (中略)...
- name: Prepare manifest
run: |
envsubst < demo.yaml > demo_expanded.yaml
env:
PR_NUMBER: '${{ github.event.pull_request.number }}'
- uses: azure/setup-kubectl@v4
- name: Deploy
run: |
aws eks update-kubeconfig --name test-cluster
kubectl apply -f demo_expanded.yaml
ポイントは以下。
-
IMAGE_NAME
という環境変数にDockerイメージのプッシュ先(ECRを使っています)をセットして、 $GITHUB_ENVに登録しておきます。これで後続のステップでも、IMAGE_NAME
が環境変数として使えます。 - そのあと、資材のビルドとDockerイメージのビルドとプッシュを行います。デモ環境特有のビルドプロセスがあるなら、ここで一緒にやってしまいます。
- たとえば、自分自身のホスト名を構成ファイルに持つ、というようなアプリならここで書き換えをします。
-
envsubst
コマンドを使って、demo.yamlに、環境変数を埋め込みます。'${{ github.event.pull_request.number }}'をPR_NUMBER
という変数に設定しています。また、IMAGE_NAME
はすでに上述の通り設定済みです。 - あとは、kubectlをインストールして、
aws eks update-kubeconfig
で、EKSクラスタの接続を確立し、kubectl apply
でマニュフェストをapplyすれば完了です。
あとは、デプロイ完了をslackで通知するような記述を追加する、PRのコメントにdemo環境のURLを書き込む、など、必要に応じて、後続の処理を行います。
Kubernetesからデモ環境を削除する
これは、単純にkubectlで作成したリソースの削除をおこなえばいいだけです。以下のようなactionsのステップになります。
- name: Destory
run: |
aws eks update-kubeconfig --name test-cluster
kubectl delete ingress demo-${{ env.PR_NUMBER }} -n=demo
kubectl delete svc demo-${{ env.PR_NUMBER }} -n=demo
kubectl delete deployment demo-${{ env.PR_NUMBER }} -n=demo
kubectl delete hpa demo-${{ env.PR_NUMBER }} -n=demo
kubectl get all -n demo -o json | jq -r '.items[] | select(.metadata.name | startswith("demo-${{ env.PR_NUMBER }}")) | {name: .metadata.name, kind: .kind, namespace:.metadata.namespace} | "kubectl delete " + .kind + "/" + .name + " -n " + .namespace ' | sh
env:
PR_NUMBER: '${{ github.event.pull_request.number }}'
ポイントは以下
- Kubernetesのリソースの名前をすべて、
demo-${{ PRの番号 }}
で統一しているので、それを使って作成したリソースを削除しています。- 消し残しがないように、念のため最後に
kubectl get all -n demo
で、すべてのリソースを取得し、.metadata.name
でフィルタして、残ってしまっているリソースをすべて削除しています。
- 消し残しがないように、念のため最後に
なお、openなPRが残り続けたり、削除が失敗したりすると、環境が残り続け、Kubenetesクラスタのリソースがひっ迫することがあります。必要に応じて、たとえば、2週間以上残り続けている環境は、強制的に削除する、等の対策をいれるとよさそうです。
一休では、ともに良いサービスをつくっていく仲間を募集中です。クラウドインフラの運用やSREに興味がある方はぜひ募集ください。
カジュアル面談もやっています。
Discussion