Open7

さくらのVM+GitHubでCI/CDする

js4000alljs4000all

まだ絵に描いた餅


「つまり……インフラを管理する“リポジトリA”と、サービスを運用する“リポジトリB”を分けるんですね?」
「ええ。そうすれば、構成管理とアプリケーションの責務が分離できて、より柔軟で安全な運用が可能になりますわ。」
「で、VMはTerraform+cloud-initで立ち上げて、接続情報はGitHubのSecrets経由でリポジトリBに自動転送、と。」


🖥️ VM管理(リポジトリA)

ステップ 処理内容 使用する秘密情報
1 Terraformファイルをpush -
2 GitHub ActionsでSSH鍵ペア生成、Terraform apply、cloud-init設定 🌟 さくらクラウドAPIキー
3 VMのIP&SSH秘密鍵をSecretsとしてリポジトリBに登録 🌟 GitHub PAT

📦 サービス管理(リポジトリB)

ステップ 処理内容 使用する秘密情報
4 サービスソース(Dockerfile等)をpush -
5 GitHub Actionsでイメージビルド&ghcr.ioにpush 🌟 GHCR用PAT(またはデフォルトトークン)
6 GitHub ActionsでVMにSSH接続 🌟 step2で作った秘密鍵
7 docker run でSupabase APIキーを渡して起動 🌟 Supabase APIキー
js4000alljs4000all

✅ GitHub Actions内でSSH鍵ペアを生成 → 公開鍵をTerraformに渡す構成


🎯 なぜこの構成がベターか?

  • 秘密鍵はGitHub Actions内で完結 → 安全にSecrets管理に移行できる
  • 公開鍵だけをVMにcloud-init経由で注入 → VM起動時点でSSHアクセス可能
  • VM内部に秘密鍵を置かない → セキュリティリスク低減

⚙️ 実装ステップ概要

1. ActionsでSSH鍵ペアを生成

- name: Generate SSH key
  run: |
    ssh-keygen -t ed25519 -f id_ed25519 -N ""

2. Terraformのpublic_keyに渡す

例として cloud-init.yml をテンプレートファイルとして使い、以下のように公開鍵を埋め込みます。

cloud-init.tpl.yml

#cloud-config
users:
  - default
  - name: deploy
    ssh-authorized-keys:
      - ${public_key}
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
    shell: /bin/bash

Terraformファイル:

data "template_file" "cloud_init" {
  template = file("${path.module}/cloud-init.tpl.yml")
  vars = {
    public_key = file("${path.module}/id_ed25519.pub")
  }
}

resource "sakuracloud_server" "vm" {
  ...
  user_data = data.template_file.cloud_init.rendered
}

3. 秘密鍵はGitHub Secretsなどに保存してstep6で使用

- name: Store SSH key in GitHub Secrets for repo B
  run: |
    gh secret set VM_SSH_KEY -b"$(cat id_ed25519)" --repo your-org/service-repo
  env:
    GH_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
js4000alljs4000all

✅ 最終的な秘密情報整理(人間が用意すべきもの)

名称 用途 保存先 備考
🌸 さくらクラウド APIキー TerraformでVM作成 リポジトリA (IaC) SAKURA_ACCESS_TOKENSAKURA_SECRETなど
🐙 GitHub PAT リポジトリBにSecretsを登録 リポジトリA (IaC) repoスコープが必要
🐋 GHCR PAT GitHub Container Registryへのpush リポジトリB (サービス) write:packagesスコープ推奨
🦾 Supabase APIキー コンテナ起動時の環境変数として注入 (併せてエンドポイントも必要かも) リポジトリB (サービス) .envに書かずSecrets経由で環境変数に

🔐 SSH用秘密鍵は、GitHub Actionsで動的生成され、公開鍵だけがcloud-initでVMに流される構成なので、事前に用意・保管する必要はありません

「このように、用意すべき秘密は最小の数に絞り込み、責任あるリポジトリに明確に分離して保管することが、セキュリティ設計における最上の礼儀ですわ。」

js4000alljs4000all

Terraform実行後、VMが構築されたタイミングで、リポジトリBのGitHub Actionsを自動起動するコード

✅ GitHub Actions:リポジトリA(IaC)用 .github/workflows/deploy-infra.yml

name: Deploy Infrastructure

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repo
        uses: actions/checkout@v4

      - name: Set up SSH key (if needed for Terraform providers)
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519

      - name: Terraform Init & Apply
        run: |
          terraform init
          terraform apply -auto-approve

      - name: Trigger service deployment in repo B
        run: |
          curl -X POST \
            -H "Authorization: Bearer ${{ secrets.GH_PAT }}" \
            -H "Accept: application/vnd.github.v3+json" \
            https://api.github.com/repos/your-org/repo-b/actions/workflows/deploy.yml/dispatches \
            -d '{"ref":"main"}'

🔐 必要なSecrets(リポジトリAに登録)

Name 用途
GH_PAT GitHub Personal Access Token(repo, workflowスコープ)
SSH_PRIVATE_KEY Terraformで必要な場合のみ。なければ削除してOK

🔁 リポジトリBで必要な設定

  • deploy.yml というワークフロー名が存在し、workflow_dispatch イベントをトリガーとして受け付ける必要があります:
on:
  workflow_dispatch:

「これで、インフラの変更が終わったら、自動的にサービスの再起動ボタンを押してくれるんですね!」
「ええ。まるで"舞台の設営が終わったら自動でカーテンが上がる"ようなものですわ。」

js4000alljs4000all

リポジトリBのGitHub Actionsワークフローに、Dockerコンテナのビルド → ghcr.io へのpush → SSH接続でデプロイまでの流れをすべて含めた完全版テンプレート

.github/workflows/deploy.yml(リポジトリB)

name: Build and Deploy Container

on:
  workflow_dispatch:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Log in to GitHub Container Registry
        run: echo "${{ secrets.GHCR_PAT }}" | docker login ghcr.io -u ${{ secrets.GHCR_USER }} --password-stdin

      - name: Build Docker image
        run: |
          docker build -t ghcr.io/${{ secrets.GHCR_USER }}/your-image:latest .

      - name: Push Docker image to GHCR
        run: |
          docker push ghcr.io/${{ secrets.GHCR_USER }}/your-image:latest

      - name: Set up SSH agent
        uses: webfactory/ssh-agent@v0.8.1
        with:
          ssh-private-key: ${{ secrets.VM_SSH_KEY }}

      - name: Deploy container on remote VM
        run: |
          ssh -o StrictHostKeyChecking=no ${{ secrets.VM_USER }}@${{ secrets.VM_IP }} << 'EOF'
            docker login ghcr.io -u ${{ secrets.GHCR_USER }} -p ${{ secrets.GHCR_PAT }}
            docker pull ghcr.io/${{ secrets.GHCR_USER }}/your-image:latest
            docker stop your-container || true
            docker rm your-container || true
            docker run -d --name your-container \
              -e SUPABASE_API_KEY=${{ secrets.SUPABASE_API_KEY }} \
              -p 80:80 \
              ghcr.io/${{ secrets.GHCR_USER }}/your-image:latest
          EOF

🔐 必要なSecrets(リポジトリB)

Name 用途
GHCR_USER GitHub Container Registryのユーザー名(例: your-org または個人名)
GHCR_PAT GHCR用のPersonal Access Token(write:packages, read:packages
VM_SSH_KEY SSH秘密鍵(IaC側から渡されたもの)
VM_USER SSHログインユーザー(例: deploy
VM_IP 作成されたVMのIPアドレス
SUPABASE_API_KEY アプリが使うAPIキー(またはさくらのDB接続情報)

「これで、コードをpushしただけで、サービスがビルドされて、パッと着替えてくれる感じですね!」
「まさに、着替えもメイクも全部執事任せのドレスアップ自動機構ですわ。」

js4000alljs4000all

Terraformのtfstateをさくらのクラウドのオブジェクトストレージに保存し、GitHub Actionsからapply可能な構成テンプレート

✅ Terraform構成テンプレート(S3互換バックエンド)

📄 main.tf

terraform {
  backend "s3" {
    endpoint   = "https://s3.<region>.sakurastorage.jp"  # ← 実際のエンドポイントに置き換え
    bucket     = "terraform-tfstate"
    key        = "infra/main.tfstate"
    region     = "us-east-1"  # ダミーでもOK
    access_key = ""
    secret_key = ""
    skip_credentials_validation = true
    skip_metadata_api_check     = true
    force_path_style            = true
  }
}

provider "sakuracloud" {
  token  = var.sakura_token
  secret = var.sakura_secret
}

resource "sakuracloud_server" "example" {
  name  = "example-vm"
  ...
}

📄 variables.tf

variable "sakura_token" {}
variable "sakura_secret" {}

📄 backend-config.auto.tfvars(GitHub Actions内で生成)

access_key = "your-sakura-access-key"
secret_key = "your-sakura-secret-key"

✅ GitHub Actionsテンプレート:.github/workflows/deploy.yml

name: Terraform Deploy with Sakura Object Storage

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.6.6

      - name: Write backend config file
        run: |
          cat <<EOF > backend-config.auto.tfvars
          access_key = "${{ secrets.SAKURA_STORAGE_ACCESS_KEY }}"
          secret_key = "${{ secrets.SAKURA_STORAGE_SECRET_KEY }}"
          EOF

      - name: Terraform Init
        run: terraform init

      - name: Terraform Plan
        run: terraform plan

      - name: Terraform Apply
        run: terraform apply -auto-approve

🔐 GitHub Secretsに登録する内容(リポジトリA)

Name 用途
SAKURA_STORAGE_ACCESS_KEY オブジェクトストレージ用のAPIキー
SAKURA_STORAGE_SECRET_KEY 同上のシークレットキー
SAKURA_TOKEN さくらクラウドAPIトークン(Terraform provider用)
SAKURA_SECRET 同上のシークレット

「この構成により、tfstateはさくらクラウドの中で安全に保管され、インフラ構成はGitHub Actionsから完全に自動化できます。これで、外部サービスを増やすことなく、実に美しく回りますわ。」

js4000alljs4000all

SSH接続情報の受け渡しをPush型じゃなくPull型にしたい(Pushだとサービスを増やしにくい)

✅ 問題の焦点

  • 🔐 秘密情報(SSH鍵、IPアドレスなど)を 安全に保存
  • 🔁 他のリポジトリ・ジョブから アクセス・取得可能にする
  • 🚫 ただし、オブジェクトストレージ等の外部公開チャネルは避けたい

GitHub Organization Secrets は gh secret set で上書き可能であり、しかもリポジトリ横断で共有できるため、今回の要件に理想的なソリューションですわ!」

✅ なぜ Organization Secrets は要件にぴったりなのか?

要件 Organization Secrets での対応状況
🔐 秘密情報の安全な保管 ✅ GitHubの暗号化ストレージに保存
🔄 複数リポジトリから参照 ✅ 同一Org内であれば対象リポジトリを制御して共有可能
🆕 VM構築後に上書き可能 gh secret set でいつでも上書き可能(警告なしで即時反映)
🔒 アクセス制御/最小権限 ✅ Secretごとにアクセス対象のリポジトリを絞れる

✅ 実際の使い方(構成)

🧾 A(インフラ管理)側:SecretsをOrgレベルで上書き

gh secret set VM_IP --body "192.0.2.42" --org your-org --visibility selected --repos repo-b,repo-c
gh secret set VM_SSH_KEY --body "$(cat id_ed25519)" --org your-org --visibility selected --repos repo-b
  • --org でOrganization Secretsを指定
  • --visibility selected + --repos で共有先リポジトリを制限できる(=セキュア!

📥 B(サービス側):そのままSecretsとして参照

env:
  VM_IP: ${{ secrets.VM_IP }}
  VM_SSH_KEY: ${{ secrets.VM_SSH_KEY }}

→ もうPush型なのにスケーラブル&疎結合という理想の構成が実現。


「これはまるで、執事長が全館の使用人へ厳選された鍵を配るような仕組みですわ。必要な者にだけ、必要な鍵を、必要な時に渡す――それがセキュリティの極意です。」

🎯 結論:この構成で「要件、満たせます」

  • GitHubの標準機能のみ
  • Secretsを一元管理・制御しつつ、複数サービスから再利用可能
  • 上書き自由、柔軟でセキュア