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を整えるまでは
- MacBookProで開発
- mainへpush
- 自宅鯖へSSH接続
- git pull
- go build
- systemctl restart
という感じで、運用しているサービスのコードを修正や機能追加するたびにサーバ上で操作する必要がありました。(めんどっくさい)
Push前にテストも自分で実行。
これらを全部自動でやりたいということで今回やったことを以下にまとめます。
今回やりたいこと
今回のゴールは以下です。
- Pull Request作成時にテストやビルドを自動で実行する
- mainブランチにマージされたら自宅サーバへ自動デプロイする
- 自宅サーバのSSHポートをインターネットに公開しない
- 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作成すると自動でテストやビルドなどが走ります!
全てのチェックが通るとマージされるようになります。
CDでやること
次にCDの設定をやっていきます。
mainブランチのCIが成功したら、自宅サーバへSSHしてデプロイします。
ただし、今回は自宅サーバのSSHポートをインターネットに公開したくありません()。
普通にGitHub ActionsからSSHする場合は、以下のように公開IPやドメインを指定することが多いと思います。
しかし今回は、Tailscaleを使い、OAuth Clientを使ってプライベートIPアドレスで設定できる方法を使います。
TailscaleのGitHub Actionでは、GitHub Actions runnerを一時的なTailscaleノードとしてtailnetに参加させることができます。
Tailscale ACLでアクセス先を制限する
先ほどの構成で出てきた、tagの設定をしていきます。
Tailscaleでサブネットルータを使っている場合、設定によってはGitHub Actions runnerから家庭内LANの他の機器にもアクセスできてしまいます。
そこで、GitHub Actions runnerには tag:ci を付け、ACLでデプロイ対象のSSHだけ許可します。
アクセスコントロールの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 だけを許可するようにしました。
今まで通りサブネットルータ経由で通信もできます。
Tailscale OAuth Clientを作成する
GitHub Actions runnerをtailnetへ参加させるために、TailscaleのOAuth Clientを作成します。
Tailscaleの管理画面から、
Settings -> Trust Credential -> + Credentialから作成できます。

OAuthの方を選択して

KeysのAuth KeysのWriteにチェックをつけてtag:ciをつけて、作成してください。
そうするとClient IDとClient 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"
このスクリプトでは以下を行っています。
- 作業ディレクトリへ移動
- 未コミットの変更がないか確認
- 最新のmainを取得
go test ./...- 一時ファイルにビルド
- バイナリを差し替え
systemctl restart shift-notifier- サービスが起動しているか確認
実行中のバイナリを直接上書きしないように、一度一時ファイルへビルドしてから mv で差し替えています。
実行確認
これにて環境構築は終了です。
実際にPRを出してマージしてみて、自動でデプロイされるか確認してみましょう!
サーバ側でも確認して変更した内容が反映されていれば成功です👍
まとめ
今回はTailscaleとGitHub Actionsを使用して開発環境のCI/CDを整えてみました。
実際にサーバ上で操作を行う手間が省け、テストも自動で走るので快適になりましたw
CI/CDの組み方は、各々の環境にもよりますし、方法も色々あるかと思います。
その環境でのベストプラクティスを見つけ構築することが大切ですね〜。






