🌴

Github ActonsとKubernetesを使って効率的にデモ環境を作る

2024/12/06に公開

このエントリーは一休.com Advent Calendar 2024の6日目の記事になります。

デモ環境を簡単に作りたい

特定の新機能を関係者にお披露目するためのデモ環境は、簡単に作れて、かつ、修正も素早く反映できると、うれしいです。

  1. デモ環境へのデプロイは、特別なことをするのではなく、既存の開発ワークフローに混ぜ込みたい。
  2. デモ環境は、静的な配置場所があるのではなくて、デプロイ時に動的にリソース確保して作成され、使い終わったら、破棄される。静的な配置場所があると、「デモ環境使いたいので使い終わったら教えてください」的なやり取りが生まれますが、これは、なくしたい。

このうち、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からデモ環境を削除する(略)...

  • labeledsynchronizeでラベルがついたときとPRにコミットが追加されたときにデモ環境をデプロイする処理をトリガします。
  • unlabeledclosedでラベルがはずれたとき、または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.appnameは、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に興味がある方はぜひ募集ください。

https://hrmos.co/pages/ikyu/jobs/1693126708022206468

カジュアル面談もやっています。

https://www.ikyu.co.jp/recruit/engineer/

Discussion