DEV Community

myougaTheAxo
myougaTheAxo

Posted on • Originally published at zenn.dev

Claude CodeでPRプレビュー環境を自動構築する:K8s Namespace・自動URLコメント・クリーンアップ

Claude CodeでPRプレビュー環境を自動構築する:K8s Namespace・自動URLコメント・クリーンアップ

PRごとに独立したプレビュー環境を自動構築すると、レビュアーがコードを読むだけでなく実際に動作を確認できるようになります。Claude Codeに「GitHub ActionsとKubernetesを使ったPRプレビュー環境の仕組みを実装して」と依頼すると、このような構成が出てきます。本記事ではその実装を詳しく解説します。

アーキテクチャ概要

PR open/update
    │
    ▼
GitHub Actions (preview-deploy job)
    │
    ├─ Namespace: preview-pr-{number} 作成
    ├─ ResourceQuota 適用(コスト制限)
    ├─ Helm deploy --values preview-values.yaml
    ├─ Ingress URL: https://preview-pr-{number}.example.com
    └─ PR にコメント投稿(既存コメントは更新)

PR close/merge
    │
    ▼
GitHub Actions (preview-cleanup job)
    └─ Namespace ごと削除
Enter fullscreen mode Exit fullscreen mode

GitHub Actionsワークフロー

.github/workflows/preview.yml に以下を配置します。

name: PR Preview Environment

on:
  pull_request:
    types: [opened, synchronize, reopened, closed]

jobs:
  deploy-preview:
    if: github.event.action != 'closed'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write  # PRコメント投稿に必要

    steps:
      - uses: actions/checkout@v4

      - name: Set preview namespace
        run: echo "NAMESPACE=preview-pr-${{ github.event.number }}" >> $GITHUB_ENV

      - name: Configure kubectl
        uses: azure/k8s-set-context@v3
        with:
          method: kubeconfig
          kubeconfig: ${{ secrets.KUBECONFIG }}

      - name: Create namespace
        run: |
          kubectl create namespace $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -

      - name: Apply ResourceQuota
        run: |
          cat <<EOF | kubectl apply -f -
          apiVersion: v1
          kind: ResourceQuota
          metadata:
            name: preview-quota
            namespace: $NAMESPACE
          spec:
            hard:
              requests.cpu: "500m"
              requests.memory: "512Mi"
              limits.cpu: "1"
              limits.memory: "1Gi"
              count/pods: "10"
          EOF

      - name: Deploy with Helm
        run: |
          helm upgrade --install my-app ./charts/my-app \
            --namespace $NAMESPACE \
            --values charts/my-app/values-preview.yaml \
            --set image.tag=${{ github.sha }} \
            --set ingress.host=pr-${{ github.event.number }}.preview.example.com \
            --wait --timeout 5m

      - name: Comment PR with preview URL
        uses: actions/github-script@v7
        with:
          script: |
            const previewUrl = `https://pr-${{ github.event.number }}.preview.example.com`;
            const body = [
              '## プレビュー環境',
              '',
              `| 項目 | 値 |`,
              `|------|-----|`,
              `| URL | ${previewUrl} |`,
              `| Namespace | preview-pr-${{ github.event.number }} |`,
              `| Commit | ${{ github.sha }} |`,
              `| 更新日時 | ${new Date().toISOString()} |`,
              '',
              '> このコメントはコミットごとに自動更新されます。',
            ].join('\n');

            // 既存コメントを検索して更新(重複投稿防止)
            const { data: comments } = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
            });

            const existing = comments.find(
              c => c.user.login === 'github-actions[bot]' && c.body.includes('## プレビュー環境')
            );

            if (existing) {
              await github.rest.issues.updateComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: existing.id,
                body,
              });
            } else {
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.issue.number,
                body,
              });
            }

  cleanup-preview:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest

    steps:
      - name: Configure kubectl
        uses: azure/k8s-set-context@v3
        with:
          method: kubeconfig
          kubeconfig: ${{ secrets.KUBECONFIG }}

      - name: Delete preview namespace
        run: |
          NAMESPACE=preview-pr-${{ github.event.number }}
          kubectl delete namespace $NAMESPACE --ignore-not-found=true
          echo "Namespace $NAMESPACE を削除しました"
Enter fullscreen mode Exit fullscreen mode

Helm preview-values.yaml

本番との差分を最小限にしつつ、コストを抑えた設定にします。

# charts/my-app/values-preview.yaml

replicaCount: 1  # プレビューは1レプリカで十分

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

ingress:
  enabled: true
  annotations:
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: letsencrypt-staging  # プレビューはstaging証明書
  tls:
    - secretName: preview-tls
      hosts:
        - "*.preview.example.com"

# DBはプレビュー用の軽量SQLiteを使用
database:
  type: sqlite
  path: /tmp/preview.db

# 外部APIはモック
featureFlags:
  useMockExternalAPIs: true
Enter fullscreen mode Exit fullscreen mode

Ingressコントローラーのワイルドカード設定

すべてのPRで *.preview.example.com を使えるよう、ワイルドカードDNSと証明書を設定します。

# cert-managerでワイルドカード証明書(Let's Encrypt)
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: preview-wildcard
  namespace: ingress-nginx
spec:
  secretName: preview-wildcard-tls
  dnsNames:
    - "*.preview.example.com"
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
EOF
Enter fullscreen mode Exit fullscreen mode

コスト制御のポイント

放置されたプレビュー環境がコストを食い続けないよう、追加で対策します。

# 古いプレビュー環境を定期削除するCronJob
apiVersion: batch/v1
kind: CronJob
metadata:
  name: cleanup-stale-previews
  namespace: kube-system
spec:
  schedule: "0 2 * * *"  # 毎日午前2時
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: preview-cleaner
          containers:
            - name: cleaner
              image: bitnami/kubectl:latest
              command:
                - /bin/sh
                - -c
                - |
                  # 7日以上前に作成されたpreview-pr-*ネームスペースを削除
                  kubectl get ns -o json | \
                    jq -r '.items[] | select(.metadata.name | startswith("preview-pr-")) |
                    select(.metadata.creationTimestamp | fromdateiso8601 < (now - 604800)) |
                    .metadata.name' | \
                  xargs -r kubectl delete ns
          restartPolicy: OnFailure
Enter fullscreen mode Exit fullscreen mode

Claude Codeへの指示パターン

以下の要件でPRプレビュー環境を自動構築するGitHub Actionsを実装してください。

要件:
- PR open/update時: preview-pr-{PR番号} のK8s Namespaceを作成
- ResourceQuotaでCPU/メモリを制限(requests.cpu: 500m, memory: 512Mi)
- Helmでデプロイ、preview-values.yamlを使用
- PRにプレビューURLをコメント(既存コメントは更新、重複させない)
- PR close/merge時: Namespaceごと削除
Enter fullscreen mode Exit fullscreen mode

まとめ

  • PR番号でNamespaceを分離preview-pr-{number} にすることで環境が衝突せず、クリーンアップも簡単
  • ResourceQuotaでコスト上限を設ける:プレビュー環境が意図せずリソースを食い潰さないよう必須設定
  • コメント更新で重複投稿を防ぐgithub-actions[bot] の既存コメントを検索して updateComment を使う
  • PR closeと同時にNamespace削除kubectl delete namespace はリソースをまとめて削除できるため、クリーンアップが一行で完結する

Code Review Pack ¥980 — PRレビューをClaude Codeで自動化する5プロンプトセット。セキュリティ・パフォーマンス・設計の観点から自動レビューコメントを生成します。
prompt-works.jp で販売中。

Top comments (0)