さくらのVM+GitHubでCI/CDする
まだ絵に描いた餅
「つまり……インフラを管理する“リポジトリ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キー |
✅ 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 ""
public_key
に渡す
2. Terraformの例として 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 }}
✅ 最終的な秘密情報整理(人間が用意すべきもの)
名称 | 用途 | 保存先 | 備考 |
---|---|---|---|
🌸 さくらクラウド APIキー | TerraformでVM作成 | リポジトリA (IaC) |
SAKURA_ACCESS_TOKEN とSAKURA_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に流される構成なので、事前に用意・保管する必要はありません。
「このように、用意すべき秘密は最小の数に絞り込み、責任あるリポジトリに明確に分離して保管することが、セキュリティ設計における最上の礼儀ですわ。」
Terraform実行後、VMが構築されたタイミングで、リポジトリBのGitHub Actionsを自動起動するコード
.github/workflows/deploy-infra.yml
✅ GitHub Actions:リポジトリA(IaC)用 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:
「これで、インフラの変更が終わったら、自動的にサービスの再起動ボタンを押してくれるんですね!」
「ええ。まるで"舞台の設営が終わったら自動でカーテンが上がる"ようなものですわ。」
リポジトリ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しただけで、サービスがビルドされて、パッと着替えてくれる感じですね!」
「まさに、着替えもメイクも全部執事任せのドレスアップ自動機構ですわ。」
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/workflows/deploy.yml
✅ GitHub Actionsテンプレート: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から完全に自動化できます。これで、外部サービスを増やすことなく、実に美しく回りますわ。」
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を一元管理・制御しつつ、複数サービスから再利用可能
- 上書き自由、柔軟でセキュア