1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub ActionsとTailscaleを使って自宅鯖CI/CD環境構築

1
Last updated at Posted at 2026-06-12

CI/CD環境を整えてデプロイを楽にした話

はじめに

みなさんデプロイする時、毎回 git pullしてbuildしていませんか?
コードを変えるだびに行うのはめんどくさいですよね〜。

この記事では、主に自宅鯖で開発・デプロイしている人向けにGitHubActionsを使って公開IPなしでCI/CD環境を構築してみた話を書いてます。

この記事ではGo言語を使った開発でのCI/CDを整えています。

ちなみに私のバイト先のシフトを通知するシステムのCI/CDを整えました。

目次

CI/CDとは

以下はGithubの記事から引用したものです。

  • 継続的インテグレーション (CI): 共有リポジトリ内でコード変更を自動的にビルドし、テストし、統合します。

  • 継続的デリバリー (CD): 自動的にコード変更を本番にすぐに移行できる環境に承認のために提供します。

  • 継続的デプロイメント (CD): コードの変更を顧客に自動的に直接デプロイします
    引用:https://github.com/resources/articles/ci-cd?locale=ja

つまりCI/CD は、ソフトウェア開発で発生する ビルド・テスト・デプロイ を自動化する考え方・仕組みのことですね。

以前までのデプロイ方法

CI/CDを整えるまでは

  1. MacBookProで開発
  2. mainへpush
  3. 自宅鯖へSSH接続
  4. git pull
  5. go build
  6. systemctl restart

という感じで、運用しているサービスのコードを修正や機能追加するたびにサーバ上で操作する必要がありました。(めんどっくさい)
Push前にテストも自分で実行。
これらを全部自動でやりたいということで今回やったことを以下にまとめます。

今回やりたいこと

今回のゴールは以下です。

  1. Pull Request作成時にテストやビルドを自動で実行する
  2. mainブランチにマージされたら自宅サーバへ自動デプロイする
  3. 自宅サーバのSSHポートをインターネットに公開しない
  4. GitHub ActionsからTailscale経由で自宅サーバへ接続する

最終的には以下の流れにします。

MacBook Proで開発
↓
作業ブランチへpush
↓
Pull Request作成
↓
GitHub ActionsでCI実行
↓
mainへマージ
↓
GitHub ActionsでCD実行
↓
Tailscale経由で自宅サーバへSSH
↓
git pull / go test / go build / systemctl restart

CIでやること

まずPRを作成したとき、またはmainブランチへpushされたときに以下を実行するようにしてみます。

  • gofmt
  • go vet
  • go test
  • go build

開発しているリポジトリに .github/workflows/CI.ymlを作成してyamlファイルに以下の内容を書き込んでください。

name: CI

on:
  pull_request:
  push:
    branches:
      - main

jobs:
  test:
    name: Go test
    runs-on: ubuntu-latest

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

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
          cache: true

      - name: Check formatting
        run: test -z "$(gofmt -l .)"

      - name: Vet
        run: go vet ./...

      - name: Test
        run: go test ./...

      - name: Build
        run: go build ./cmd/shift-notifier

実行環境・ファイルのパスなどは適宜自分の環境に合わせて使ってください。

これを追加すると、PR作成すると自動でテストやビルドなどが走ります!

スクリーンショット 2026-06-12 15.39.58.png

全てのチェックが通るとマージされるようになります。

CDでやること

次にCDの設定をやっていきます。

mainブランチのCIが成功したら、自宅サーバへSSHしてデプロイします。
ただし、今回は自宅サーバのSSHポートをインターネットに公開したくありません()。
普通にGitHub ActionsからSSHする場合は、以下のように公開IPドメインを指定することが多いと思います。

スクリーンショット 2026-06-12 15.43.29.png

しかし今回は、Tailscaleを使い、OAuth Clientを使ってプライベートIPアドレスで設定できる方法を使います。

TailscaleのGitHub Actionでは、GitHub Actions runnerを一時的なTailscaleノードとしてtailnetに参加させることができます。

構成はこんな感じです↓
スクリーンショット 2026-06-12 16.12.56.png

Tailscale ACLでアクセス先を制限する

先ほどの構成で出てきた、tagの設定をしていきます。
Tailscaleでサブネットルータを使っている場合、設定によってはGitHub Actions runnerから家庭内LANの他の機器にもアクセスできてしまいます。

そこで、GitHub Actions runnerには tag:ci を付け、ACLでデプロイ対象のSSHだけ許可します。

スクリーンショット 2026-06-12 19.56.38.png

アクセスコントロールのJSON editorを開き以下のように設定します。
以下は tag:ci 用ルールの抜粋です。自分の端末や他の用途の許可ルールは、環境に応じて別途残してください。

{
  "tagOwners": {
    "tag:ci": ["autogroup:admin"]
  },

  "grants": [
    {
      "src": ["tag:ci"],
      "dst": ["192.168.1.220"],
      "ip": ["22"]
    }
  ]
}

広い許可上位に残っていると、アクセス権限を狭めても意味ないので注意

設定した後だとこんな感じですかね。
私の環境だと、GitHub Actions用の tag:ci には 192.168.1.220:22 だけを許可するようにしました。
今まで通りサブネットルータ経由で通信もできます。

スクリーンショット 2026-06-12 20.01.26.png

Tailscale OAuth Clientを作成する

GitHub Actions runnerをtailnetへ参加させるために、TailscaleのOAuth Clientを作成します。

Tailscaleの管理画面から、
Settings -> Trust Credential -> + Credentialから作成できます。
スクリーンショット 2026-06-12 16.53.50.png
OAuthの方を選択して
スクリーンショット 2026-06-12 17.05.14.png
KeysのAuth KeysのWriteにチェックをつけてtag:ciをつけて、作成してください。

そうするとClient IDClient Secretが表示されるのでメモっといてください。

GitHub Secretsに値を設定する

次にGithub側で操作をしていきます。
GitHub Actionsで使う秘密情報は、Repository secretsに保存します。

場所は、対象のリポジトリのsettings -> Secrets and variables -> Repository secrets

今回は以下を設定しました。

Secret名 用途
HOME_SERVER_HOST デプロイ先のIPアドレス。今回は 192.168.1.220
HOME_SERVER_PORT SSHポート。今回は 22
HOME_SERVER_USER SSHユーザー
HOME_SERVER_SSH_KEY SSH接続用の秘密鍵
HOME_SERVER_APP_DIR Gitリポジトリの配置先
HOME_SERVER_BIN_PATH ビルドしたバイナリの配置先
TS_OAUTH_CLIENT_ID Tailscale OAuth Client ID
TS_OAUTH_SECRET Tailscale OAuth Client Secret

参考として私の環境だと以下のように設定しました。

HOME_SERVER_HOST=192.168.1.220
HOME_SERVER_PORT=22
HOME_SERVER_USER=root
HOME_SERVER_APP_DIR=/opt/shift-notifier/repo
HOME_SERVER_BIN_PATH=/opt/shift-notifier/shift-notifier

HOME_SERVER_SSH_KEY、TS_OAUTH_CLIENT_ID、TS_OAUTH_SECRET は秘密情報なので記事には載せません。

.github/workflows/deploy.yml の作成

CIのところで作ったように、デプロイするための.github/workflows/deploy.yml を作成します。

name: Deploy

on:
  workflow_run:
    workflows:
      - CI
    types:
      - completed

concurrency:
  group: deploy-production
  cancel-in-progress: false

jobs:
  deploy:
    name: Deploy to home server
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main' }}

    env:
      HOME_SERVER_HOST: ${{ secrets.HOME_SERVER_HOST }}
      HOME_SERVER_PORT: ${{ secrets.HOME_SERVER_PORT }}
      HOME_SERVER_USER: ${{ secrets.HOME_SERVER_USER }}
      HOME_SERVER_SSH_KEY: ${{ secrets.HOME_SERVER_SSH_KEY }}
      HOME_SERVER_KNOWN_HOSTS: ${{ secrets.HOME_SERVER_KNOWN_HOSTS }}
      HOME_SERVER_APP_DIR: ${{ secrets.HOME_SERVER_APP_DIR }}
      HOME_SERVER_BIN_PATH: ${{ secrets.HOME_SERVER_BIN_PATH }}

    steps:
      - name: Validate deployment secrets
        run: |
          test -n "$HOME_SERVER_HOST"
          test -n "$HOME_SERVER_USER"
          test -n "$HOME_SERVER_SSH_KEY"
          test -n "$HOME_SERVER_APP_DIR"
          test -n "$HOME_SERVER_BIN_PATH"

      - name: Connect to Tailscale
        uses: tailscale/github-action@v4
        with:
          oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
          oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
          tags: tag:ci
          ping: ${{ secrets.HOME_SERVER_HOST }}

      - name: Configure SSH
        run: |
          mkdir -p ~/.ssh
          chmod 700 ~/.ssh

          printf '%s\n' "$HOME_SERVER_SSH_KEY" > ~/.ssh/home_server_key
          chmod 600 ~/.ssh/home_server_key

          PORT="${HOME_SERVER_PORT:-22}"
          if [ -n "$HOME_SERVER_KNOWN_HOSTS" ]; then
            printf '%s\n' "$HOME_SERVER_KNOWN_HOSTS" > ~/.ssh/known_hosts
          else
            ssh-keyscan -p "$PORT" "$HOME_SERVER_HOST" > ~/.ssh/known_hosts
          fi
          chmod 600 ~/.ssh/known_hosts

      - name: Deploy over SSH
        run: |
          PORT="${HOME_SERVER_PORT:-22}"

          ssh \
            -i ~/.ssh/home_server_key \
            -p "$PORT" \
            -o IdentitiesOnly=yes \
            -o StrictHostKeyChecking=yes \
            "$HOME_SERVER_USER@$HOME_SERVER_HOST" \
            "SHIFT_NOTIFIER_APP_DIR='$HOME_SERVER_APP_DIR' SHIFT_NOTIFIER_BIN_PATH='$HOME_SERVER_BIN_PATH' bash -s" <<'EOSSH'
          set -euo pipefail

          cd "$SHIFT_NOTIFIER_APP_DIR"
          git fetch origin main
          git checkout main
          git pull --ff-only origin main
          bash scripts/deploy.sh
          EOSSH

workflow_runを使っているので、CI workflowが完了したあとにDeploy workflowが起動します。

if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main' }}

また上記の条件よりmain ブランチのCIが成功したときだけデプロイするようにしています。

deploy.shの作成

GitHub ActionsからSSHしたあと、サーバ側では scripts/deploy.sh を実行してデプロイをしていきます。

#!/usr/bin/env bash
set -euo pipefail

APP_DIR="${SHIFT_NOTIFIER_APP_DIR:-$(pwd)}"
BRANCH="${SHIFT_NOTIFIER_DEPLOY_BRANCH:-main}"
BIN_PATH="${SHIFT_NOTIFIER_BIN_PATH:-shift-notifier}"
RUN_TESTS="${SHIFT_NOTIFIER_DEPLOY_RUN_TESTS:-true}"
SERVICE_NAME="${SHIFT_NOTIFIER_SERVICE_NAME:-shift-notifier}"
SYSTEMCTL="${SHIFT_NOTIFIER_SYSTEMCTL:-systemctl}"
RESTART_CMD="${SHIFT_NOTIFIER_RESTART_CMD:-}"
HEALTHCHECK_URL="${SHIFT_NOTIFIER_HEALTHCHECK_URL:-}"

cd "$APP_DIR"

if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
  echo "working tree has uncommitted tracked changes; aborting deploy" >&2
  git status --short --untracked-files=no >&2
  exit 1
fi

git fetch origin "$BRANCH"
git checkout "$BRANCH"
git pull --ff-only origin "$BRANCH"

if [ "$RUN_TESTS" = "true" ]; then
  go test ./...
fi

BIN_DIR="$(dirname "$BIN_PATH")"
BIN_NAME="$(basename "$BIN_PATH")"
TEMP_BIN="$(mktemp "$BIN_DIR/.${BIN_NAME}.tmp.XXXXXX")"
trap 'rm -f "$TEMP_BIN"' EXIT

go build -o "$TEMP_BIN" ./cmd/shift-notifier
chmod 755 "$TEMP_BIN"
mv "$TEMP_BIN" "$BIN_PATH"
trap - EXIT

if [ -n "$RESTART_CMD" ]; then
  bash -lc "$RESTART_CMD"
elif [ "$(id -u)" = "0" ]; then
  "$SYSTEMCTL" restart "$SERVICE_NAME"
  "$SYSTEMCTL" is-active --quiet "$SERVICE_NAME"
else
  sudo "$SYSTEMCTL" restart "$SERVICE_NAME"
  sudo "$SYSTEMCTL" is-active --quiet "$SERVICE_NAME"
fi

if [ -n "$HEALTHCHECK_URL" ]; then
  curl --fail --silent --show-error --max-time 10 "$HEALTHCHECK_URL" >/dev/null
fi

echo "deploy completed"

このスクリプトでは以下を行っています。

  1. 作業ディレクトリへ移動
  2. 未コミットの変更がないか確認
  3. 最新のmainを取得
  4. go test ./...
  5. 一時ファイルにビルド
  6. バイナリを差し替え
  7. systemctl restart shift-notifier
  8. サービスが起動しているか確認

実行中のバイナリを直接上書きしないように、一度一時ファイルへビルドしてから mv で差し替えています。

実行確認

これにて環境構築は終了です。
実際にPRを出してマージしてみて、自動でデプロイされるか確認してみましょう!

スクリーンショット 2026-06-12 20.32.58.png
スクリーンショット 2026-06-12 20.33.23.png

サーバ側でも確認して変更した内容が反映されていれば成功です👍

まとめ

今回はTailscaleとGitHub Actionsを使用して開発環境のCI/CDを整えてみました。
実際にサーバ上で操作を行う手間が省け、テストも自動で走るので快適になりましたw

CI/CDの組み方は、各々の環境にもよりますし、方法も色々あるかと思います。
その環境でのベストプラクティスを見つけ構築することが大切ですね〜。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?