3-4. CI 基礎 - GitHub Actions でテスト・ビルド・デプロイを自動化

所要時間: 50-70分 ゴール: Go アプリの test → build → Docker image push → VPS デプロイを GitHub Actions で自動化できる コミット内容: ~/learn/infra/ci/ に動作する .github/workflows/*.yml 一式


このレッスンのゴール

  • CI と CD の責務の違いを語れる
  • GitHub Actions の用語(Workflow / Job / Step / Action)を区別できる
  • push 時にテスト → ビルド → イメージ push の3段で動かせる
  • OIDC 経由で AWS / GCP に short-lived 認証できる
  • concurrency でデプロイ多重実行を防止できる

なぜ学ぶか(実務悩みベース)

  • 「自分の手元では動いた」病を撲滅する
  • マージから本番反映を分単位に短縮する
  • 脆弱性スキャン / シークレットスキャンを自動化する
  • 採用面接の頻出質問「CI 書けますか?」に即答できる

前章とのつながり

3-3 までで「動かせる本番環境」が揃った。今回はそこに 自動でコードを流し込む 仕組み。Level 1 のイメージビルド、Level 2 の Compose、Level 3 の VPS、全てが CI/CD で1本に繋がる。


大前提: CI/CD は「自動テスト + 自動デプロイ」の総称

  • CI (Continuous Integration): コード変更ごとにテスト・lint・ビルドを自動実行し、壊れていないことを担保する
  • CD (Continuous Delivery / Deployment): テストが通ったコードを自動でステージングや本番に届ける

なぜ必須か:

  • 品質: PR 時に「テストが通ること」が機械的に保証される。レビュワーは「正しさ」より「設計」を見られる
  • 速度: 「動作確認のためのマニュアル手順書」が消える。マージ → 自動デプロイで数分後に本番反映
  • 再現性: 「自分の手元では動いた」が消滅。CI 環境で動かなければマージできない仕組み
  • セキュリティ: 脆弱性スキャン、シークレットスキャン、SBOM 生成を自動化できる
  • 採用: GitHub Actions / GitLab CI / CircleCI が読めない/書けないバックエンドエンジニアは2026年では珍しい

GitHub の OSS 文化と直結している GitHub Actions を学べば、CI/CD の本質はすべて掴める。他の CI ツール(GitLab CI, CircleCI, Buildkite)の概念モデルもほぼ同じ。


セッション①: GitHub Actions の構造(20-25分)

1. 用語の階層

[Workflow]      .github/workflows/ci.yml に書く 1 ファイル
   ↓ 含む
[Job]           独立した実行単位。並列で複数走らせられる
   ↓ 含む
[Step]          コマンド or アクションの 1 単位
   ↓ 使う
[Action]        再利用可能な処理(公式 / マーケットプレイス / 自作)

最小例:

# .github/workflows/hello.yml
name: hello-world
 
on: push           # トリガー
 
jobs:
  greeting:         # ジョブ名
    runs-on: ubuntu-latest
    steps:
      - name: echo
        run: echo "Hello, $GITHUB_ACTOR"

runs-on で指定する実行環境

GitHub がホストするランナー(仮想マシン)の OS を指定する:

ラベル中身
ubuntu-latestUbuntu 24.04 (2026 時点)
ubuntu-22.04バージョン固定
macos-latestmacOS(iOS ビルド等)
windows-latestWindows Server
self-hosted自前で立てたランナー

基本は ubuntu-latest。macOS は実行時間課金が約 10 倍、Windows は 2 倍。-latest は将来 OS が上がるので、再現性が必要なら ubuntu-24.04 のように固定する流儀もある。

2. トリガー on:

on:
  push:
    branches: [main, develop]
    paths:
      - 'src/**'
      - 'go.mod'
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 3 * * *'    # 毎日 03:00 UTC
  workflow_dispatch:        # 手動実行ボタン
    inputs:
      env:
        description: 'デプロイ環境'
        required: true
        default: 'staging'
        type: choice
        options: [staging, production]

各トリガーの実務用途

  • push: 通常の CI(main マージ時、各 PR ブランチへの push)
  • pull_request: PR 単位での CI。fork からの PR でも安全に動く(secrets が読めない制限が入る)
  • schedule: 定期実行。脆弱性スキャン、データ集計、デッドリンクチェック
  • workflow_dispatch: 手動実行。デプロイボタン、ロールバック、ホットフィックス
  • release: GitHub Release 作成時。バイナリビルド・配布
  • workflow_run: 別の workflow 完了時。「テストが通ったらデプロイ」のチェーン

pull_requestpull_request_target の違い(重要なセキュリティ)

  • pull_request: fork からの PR では secrets が空、書き込み権限なし。安全
  • pull_request_target: 親リポの権限で実行。secrets も触れる、書き込みも可能fork からの悪意あるコードがランナー上で動く危険

マーケットプレイスの pull_request_target を使うアクションは要監査。基本は pull_request を使う

3. ジョブとステップ

name: ci
 
on: [push, pull_request]
 
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.23'
      - run: go vet ./...
      - run: go install honnef.co/go/tools/cmd/staticcheck@latest
      - run: staticcheck ./...
 
  test:
    runs-on: ubuntu-latest
    needs: lint         # lint が成功した後に走る
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.23'
      - run: go test -race -cover ./...
 
  build:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.23'
      - run: go build -o app ./cmd/server
      - uses: actions/upload-artifact@v4
        with:
          name: app-binary
          path: app

needs: でジョブの依存関係

デフォルトで複数 job は並列実行。needs: [lint] を書くと「lint 成功後に開始」になる。

並列 vs 直列の選択:

  • lint と test は内容が独立 → 並列が速い
  • build は test 通過後 → 直列
  • deploy は build 通過後 → 直列

「lint と test は並列、両方終わったら build」のような複雑な依存も needs: [lint, test] で書ける。

uses:run: の違い

  • run:: シェルコマンドを直接実行。run: go test ./...
  • uses:: 既存の Action を再利用。uses: actions/checkout@v4

Action はマーケットプレイス(marketplace.github.com)で検索できる。「Slack 通知」「Docker ビルド」「AWS デプロイ」など何でもある。

4. 環境変数とシークレット

env:
  GO_VERSION: '1.23'
  REGISTRY: ghcr.io
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      DATABASE_URL: ${{ secrets.DATABASE_URL }}
    steps:
      - name: SSH デプロイ
        env:
          SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
        run: |
          echo "$SSH_KEY" > /tmp/key
          chmod 600 /tmp/key
          ssh -i /tmp/key deploy@vps.example.com "/opt/app/deploy.sh"

secrets の管理

リポジトリの Settings → Secrets and variables → Actions で登録する。

  • Repository secrets: そのリポでのみ使える
  • Environment secrets: 環境(staging/production)単位、approval workflow と組み合わせられる
  • Organization secrets: 組織内の複数リポで共有

シークレットは workflow ログでも *** でマスクされる。

secret を echo して漏らす事故

# NG
- run: echo "API_KEY is ${{ secrets.API_KEY }}"

ログには API_KEY is *** と出るのでマスクされているように見える。ただしマスクは「完全一致」のみ

危険な書き方:

# NG: 部分文字列を経由して漏らす
- run: |
    KEY="${{ secrets.API_KEY }}"
    echo "${KEY:0:5}..."   # 先頭 5 文字を出力 → マスク回避
    echo "$KEY" | base64    # エンコードしてマスク回避
    echo "$KEY" >> /tmp/key  # ファイルに書く → 直後のステップで cat /tmp/key  みたいな

攻撃: PR で workflow ファイルを書き換えて secrets を取り出す手口が実在。対策は fork からの PR では pull_request を使う(pull_request_target を使わない)信頼できないコードのレビューでは workflow 変更を要警戒

5. マトリックスビルド

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false    # 1 つ落ちても他を続ける
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        go: ['1.22', '1.23']
        exclude:
          - os: windows-latest
            go: '1.22'      # この組み合わせはスキップ
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}
      - run: go test ./...

マトリックスビルドの典型用途

  • 多バージョン対応のライブラリ: Go 1.22 / 1.23、Python 3.10 / 3.11 / 3.12
  • 多 OS 対応の CLI: Linux / macOS / Windows でビルド
  • DB 種別: MySQL 8.0 / PostgreSQL 16 / SQLite で同じテストを回す

上の例だと 3 OS × 2 バージョン = 6 ジョブが並列実行。Go の OSS で典型的なパターン。

fail-fast: false

デフォルトでは 1 ジョブ失敗で他もキャンセル。トラブル時に「全部の組み合わせの状況を見たい」場合は false に。true の方が CI 時間と費用を抑えられる。

6. キャッシュ

- uses: actions/setup-go@v5
  with:
    go-version: '1.23'
    cache: true               # go modules を自動キャッシュ
 
# 手動でキャッシュ管理する場合
- uses: actions/cache@v4
  with:
    path: |
      ~/.cache/go-build
      ~/go/pkg/mod
    key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
    restore-keys: |
      ${{ runner.os }}-go-

キャッシュで CI を高速化

依存ダウンロードに毎回数分かかると、コミットごとに数分浪費する。キャッシュで CI 時間を 30-70% 削減できる。

キャッシュキー設計のコツ:

  • hashFiles('go.sum') のように依存ファイルのハッシュを含める → 依存が変わったら自動的に新キャッシュ
  • restore-keys でフォールバック(前のキャッシュを部分的に使う)

キャッシュ容量: リポジトリあたり 10GB まで。古いキャッシュは 7 日で自動削除。


セッション②: Go アプリの実用 CI(20-25分)

7. Go アプリ向けの workflow 完成版

# .github/workflows/ci.yml
name: ci
 
on:
  push:
    branches: [main]
  pull_request:
 
permissions:
  contents: read     # 最小権限
 
env:
  GO_VERSION: '1.23'
 
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ env.GO_VERSION }}
          cache: true
      - run: gofmt -l . | tee /tmp/fmt.log
      - run: test ! -s /tmp/fmt.log     # gofmt 違反があれば失敗
      - run: go vet ./...
      - uses: dominikh/staticcheck-action@v1
        with:
          version: latest
 
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: testpass
        ports: [5432:5432]
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ env.GO_VERSION }}
          cache: true
      - name: テスト実行(カバレッジ付き)
        env:
          DATABASE_URL: postgres://postgres:testpass@localhost:5432/postgres?sslmode=disable
        run: go test -race -coverprofile=coverage.out ./...
      - name: カバレッジ報告
        run: go tool cover -func=coverage.out
      - uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage.out
 
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ env.GO_VERSION }}
      - name: govulncheck
        run: |
          go install golang.org/x/vuln/cmd/govulncheck@latest
          govulncheck ./...

services: で DB 等のサイドカー

ジョブ内で PostgreSQL / MySQL / Redis などを起動できる。統合テストに不可欠

services は Docker コンテナとして起動し、ジョブ内から localhost:<ポート> で接続できる。health-cmd で起動完了を待つ仕組み。

govulncheck で脆弱性スキャン

Go 公式の脆弱性スキャナ。go.sum の依存だけでなく、実際にコード中で呼ばれている関数のみ で判定する。「依存はあるが脆弱な関数は使っていない」を区別できるのが強み。

他に nancy, osv-scanner 等もある。現代の Go プロジェクトには 1 つは入れる

8. Docker イメージのビルドと push

# .github/workflows/release.yml
name: release
 
on:
  push:
    branches: [main]
    tags: ['v*']
 
permissions:
  contents: read
  packages: write     # ghcr.io への push に必要
 
jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - uses: docker/setup-buildx-action@v3
 
      - name: GitHub Container Registry にログイン
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: タグ生成
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=ref,event=tag
            type=sha,prefix=,format=short
            type=raw,value=latest,enable={{is_default_branch}}
 
      - name: ビルドして push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64

ghcr.io vs Docker Hub

項目ghcr.ioDocker Hub
認証GITHUB_TOKEN 自動DOCKERHUB_TOKEN 手動登録
料金プライベートも無料無料枠の制限あり
レイテンシGitHub と同居で速い別ホスト
連携GitHub Repo と密結合独立

2026 年現在、個人・小規模は ghcr.io 一択。大規模・組織横断的は ECR / Artifact Registry / Docker Hub Team が選択肢。

GITHUB_TOKEN の自動発行

各 workflow 実行時に GitHub が自動で発行するトークン。secrets.GITHUB_TOKEN で参照できる。

デフォルトの権限: read(リポジトリ設定で変更可能)。最小権限の原則に従い、各 workflow / job の permissions: ブロックで必要なものだけ宣言する。

permissions:
  contents: read
  packages: write   # コンテナレジストリへの push に必要
  pull-requests: write   # PR コメント投稿に必要

マルチアーキビルド

platforms: linux/amd64,linux/arm64 で AMD64 と ARM64 の両方をビルド。Apple Silicon Mac、AWS Graviton、Raspberry Pi で動くようになる。

ARM ランナーは Mac でないと遅い → docker/setup-buildx-action が QEMU エミュレーションで対応。ビルド時間が2倍近くなる。必要な時だけ有効化。


セッション③: VPS への自動デプロイ(20-25分)

9. VPS にデプロイする workflow

# .github/workflows/deploy.yml
name: deploy
 
on:
  workflow_run:
    workflows: ['release']
    types: [completed]
    branches: [main]
  workflow_dispatch:    # 手動実行用
 
permissions:
  contents: read
 
concurrency:
  group: production-deploy
  cancel-in-progress: false    # デプロイは並行させない
 
jobs:
  deploy:
    if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
 
    steps:
      - name: SSH 鍵を準備
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$DEPLOY_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
 
      - name: VPS でデプロイスクリプト実行
        run: |
          ssh deploy@${{ secrets.DEPLOY_HOST }} bash <<'EOF'
            set -euo pipefail
            cd /opt/app
            docker pull ghcr.io/myorg/myapp:latest
            docker compose up -d --no-deps app
            docker image prune -f
          EOF
 
      - name: ヘルスチェック
        run: |
          for i in 1 2 3 4 5; do
            if curl -fs https://example.com/healthz; then
              echo "healthcheck ok"
              exit 0
            fi
            sleep 5
          done
          echo "healthcheck failed"
          exit 1

concurrency: で多重実行を制御

同じデプロイが同時に 2 本走ると壊れることがある(DB マイグレーションが二重実行されるなど)。

concurrency:
  group: production-deploy
  cancel-in-progress: false   # 走っている方を待つ

cancel-in-progress: true にすると「最新だけ実行、古いのはキャンセル」。新しいコミットが連続で来た時に CI 時間を節約できる(テスト用途)。デプロイでは false 推奨。

environment: で承認フローを挟む

environment: production を指定すると、GitHub の Settings → Environments で 「承認者を必須にする」「特定ブランチからのみデプロイ可」「待機時間 N 分」 といった保護ルールを設定できる。

本番デプロイには「2 人の承認が必要」のようなガードが鉄板。事故防止+監査証跡。

ssh-keyscan で known_hosts を作る

ssh-keyscan -H <host> でホスト鍵を取得し ~/.ssh/known_hosts に登録。これをしないと SSH 接続時に「初めての接続です(yes/no)」が出てスクリプトが止まる、または StrictHostKeyChecking=no で危険な設定にしがち。

デプロイ鍵の権限を絞る

DEPLOY_SSH_KEY の秘密鍵に対応する公開鍵を VPS の deploy ユーザーに置く。ただし authorized_keys で:

command="/opt/app/deploy.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAA... github-actions-deploy

のように command 制約 を付け、デプロイ専用に。万一漏洩しても他用途には使えない。

10. ロールバック workflow

# .github/workflows/rollback.yml
name: rollback
 
on:
  workflow_dispatch:
    inputs:
      version:
        description: '戻すバージョン(タグまたは SHA)'
        required: true
        type: string
 
permissions:
  contents: read
 
jobs:
  rollback:
    runs-on: ubuntu-latest
    environment:
      name: production
    steps:
      - name: SSH 準備
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$DEPLOY_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
 
      - name: 指定バージョンに戻す
        run: |
          ssh deploy@${{ secrets.DEPLOY_HOST }} \
            "cd /opt/app && \
             IMAGE_TAG=${{ inputs.version }} docker compose up -d --no-deps app"

ロールバックの心構え

「本番デプロイ → 不具合発覚 → 戻したい」の時に Git revert からの再デプロイは遅い。直前イメージにタグ切替で即戻しが現実的な手法。

前提:

  • イメージタグに コミット SHA とブランチ名 を両方含めている
  • 過去 N 個のイメージはレジストリに残っている
  • VPS 側に IMAGE_TAG を受け取って docker compose が起動する仕組みがある

アンチパターン総まとめ

secret を echo / cat で出力

上述の通り、partial output や encode を経由した漏洩が起きる。secrets は ファイルにも保存しない、ログにも出さない

main ブランチでだけ動く CI

on:
  push:
    branches: [main]

PR の CI が動かないので、main にマージするまでテストが回らない。pull_request トリガーも必須

テストを skip するための CI

- run: go test ./... || true    # 失敗を握りつぶす

「CI が落ちて止まるから」と握りつぶすと、CI の意味がない。落ちたら直す。

サードパーティ Action をバージョン無指定で使う

- uses: shady-action/do-things@main    # NG: 信頼できる commit にピン留めしていない

リポオーナーが悪意あるコードを混ぜると、secrets が抜かれる。

対策: @v1.2.3 のようなセマンティックタグ、または commit SHA でピン留め:

- uses: shady-action/do-things@abc1234abc1234abc1234abc1234abc1234abc12

Dependabot でアクション更新も自動化可能。

デプロイ前にヘルスチェックなし

- run: ssh deploy@vps "docker compose up -d app"
  # → ここで workflow は成功扱い
  # → アプリは起動失敗していてもユーザーには気付かれない

デプロイ後に /healthz を叩いて 5xx でないことを確認してから「成功」とする。

permissions: を書かず write 権限デフォルトで動かす

古い GitHub Actions のデフォルトは write-allworkflow ファイル冒頭で permissions: を最小権限に絞るのが現代の作法。


セキュリティ補強

Action のセキュリティ

  • 公式アクションを優先: actions/checkout, actions/setup-go など。GitHub 公式は監査されている
  • マーケットプレイスは作者を確認: スター数、メンテ頻度、所有者の信頼性
  • OIDC で短命クレデンシャル: AWS/GCP/Azure には permissions: id-token: write で短命の OIDC トークンを発行し、永続クレデンシャルを secrets に置かない方式が現代の推奨

OIDC によるパスワードレス AWS デプロイ例

permissions:
  id-token: write
  contents: read
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: ap-northeast-1
      - run: aws s3 sync ./build s3://my-bucket/

AWS 側に IAM ロールと OIDC プロバイダ設定が必要だが、アクセスキーを GitHub に置かなくて済む。漏洩時の被害が激減。

secret scanning と Dependabot

リポジトリ Settings で:

  • Secret scanning: AWS キーや GitHub トークンが誤コミットされた瞬間に検知して通知
  • Dependabot: 依存ライブラリの脆弱性を検知、自動 PR で更新
  • Dependabot for GitHub Actions: アクションのバージョン更新も自動 PR

全部 ON にする。コスト無料。


実例: テスト → イメージビルド → VPS デプロイの完全フロー

# .github/workflows/full-pipeline.yml
name: full-pipeline
 
on:
  push:
    branches: [main]
  pull_request:
 
permissions:
  contents: read
  packages: write
 
env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.23'
          cache: true
      - run: go vet ./...
      - run: go test -race -cover ./...
 
  build:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.version }}
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=,format=short
            type=raw,value=latest
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
 
  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    concurrency:
      group: production-deploy
      cancel-in-progress: false
    steps:
      - name: SSH 鍵設置
        env:
          KEY: ${{ secrets.DEPLOY_SSH_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
 
      - name: デプロイ
        run: |
          ssh deploy@${{ secrets.DEPLOY_HOST }} bash <<EOF
            set -euo pipefail
            cd /opt/app
            export IMAGE_TAG=${{ needs.build.outputs.image-tag }}
            docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:\$IMAGE_TAG
            docker compose up -d --no-deps app
            docker image prune -f
          EOF
 
      - name: ヘルスチェック(最大 30 秒)
        run: |
          for i in $(seq 1 6); do
            if curl -fs https://example.com/healthz; then
              echo "OK"; exit 0
            fi
            sleep 5
          done
          echo "FAILED"; exit 1

練習課題

  1. 小さな Go プロジェクト(hello.go レベル)を GitHub に push
  2. .github/workflows/ci.ymlgo vet + go test を走らせる
  3. PR を作って Actions タブで実行を観察、緑チェックマークを確認
  4. .github/workflows/release.yml で ghcr.io に Docker イメージを push
  5. ghcr.io のリポジトリページで自分のイメージが見えるか確認
  6. VPS の deploy ユーザー向けの SSH 鍵を作り、GitHub Secrets に登録
  7. .github/workflows/deploy.yml で VPS にデプロイ
  8. main に新しいコミットを push して、テスト → ビルド → デプロイが連鎖することを確認
  9. ヘルスチェック失敗時の挙動を試す(わざと 500 を返すコードで)
  10. ロールバック workflow も作って手動実行できることを確認

締め: git で証跡を残す

cd ~/learn/infra/ci
 
# workflow ファイル一式を別途学習用にコピー
cp -r ~/myapp/.github .
 
git add .github/ README.md
git commit -m "feat(ci): GitHub Actions の test/build/deploy フルパイプライン構築"

チェックリスト

  • workflow, job, step, action の階層関係を説明できる
  • on: で push / pull_request / schedule / workflow_dispatch を使い分けられる
  • secrets と環境変数の違いを説明できる
  • マトリックスビルドで多バージョンテストできる
  • needs: でジョブ依存を制御できる
  • ghcr.io に Docker イメージを push できる
  • VPS に SSH で自動デプロイできる
  • permissions: を最小権限で書ける
  • サードパーティ Action のリスクと SHA ピン留めを意識している
  • OIDC でパスワードレス AWS デプロイの方法を知っている

詰まった時のチートシート

やりたいこと書き方
push と PR で動かすon: [push, pull_request]
main のみon: push: branches: [main]
手動実行on: workflow_dispatch:
毎日定時on: schedule: - cron: '0 3 * * *'
ジョブ依存needs: [test, lint]
条件付き実行if: github.ref == 'refs/heads/main'
secret 参照${{ secrets.NAME }}
env 参照${{ env.NAME }}
出力受け渡しoutputs: + ${{ needs.xxx.outputs.yyy }}
Go セットアップuses: actions/setup-go@v5
キャッシュcache: true(setup-* で)
Docker ビルドuses: docker/build-push-action@v6
並行制御concurrency: group: ...

対比表: デプロイ戦略の選択肢

戦略仕組みメリットデメリット
手動 SSH デプロイgit pull + restart単純再現性ゼロ、ヒューマンエラー多発
GitHub Actions → SSHssh で本番にコマンド自動化、再現性SSH 鍵管理
GitHub Actions → イメージ push → 本番 pullレジストリ経由ロールバック容易レジストリ運用
GitOps (ArgoCD等)リポジトリ状態 = 本番状態完全宣言的学習コスト、k8s 前提が多い

対比表: 認証方式

方式内容推奨度
長期 secret (Access Key)AWS_ACCESS_KEY_ID△ (漏えいリスク)
OIDC (id-token)GitHub → AWS/GCP に short-lived 認証◎ (推奨)
SSH 鍵 + GitHub secretappleboy/ssh-action 等で SSH○ (VPS デプロイ)

「実務OK」基準

  • Go の test/lint/build/security を CI で組める
  • Docker イメージを ghcr.io に push できる
  • VPS へ自動デプロイの workflow が書ける
  • secrets を漏らさず使える
  • permissions: で最小権限を宣言できる
  • マトリックスビルドで複数バージョン同時テストできる
  • concurrency でデプロイ多重実行を防げる
  • OIDC / 環境保護ルール / Dependabot を含む現代的なセキュリティ運用を理解している

自己評価チェックリスト

知識レベル

  • CI と CD の責務の違いを語れる
  • OIDC が「長期 secret 漏洩リスク」を解決する仕組みを言える
  • concurrency がなぜ必要かを即答できる

実行レベル

  • test → build → image push の Workflow を書いた
  • secrets を ${{ secrets.X }} で参照する書き方を実践した
  • permissions: を最小権限で宣言した

メタ認知

  • 自分のリポジトリで「手動デプロイ」が残っていないか棚卸しした
  • CI が「ただの飾り」になっていないか(テスト失敗が無視されていないか)チェックした

さらに深掘りするなら


次のレッスン

3-5_ログ収集と監視.md で、デプロイされたアプリの動きを把握する監視・ロギングの世界へ。