github octcat icon
twitter bird icon
rss icon

GitHub ActionsでCI/CDパイプライン構築

2022-01-27

stream network

Photo Credit: Martin Adams

前回のブログで、さくらVPSにGatsbyで生成したサイトをデプロイすることはできるようになりました。 しかし、rsyncコマンドでローカルからデプロイするというなんとも言えない状態でださかったので、今回はタイトルの通りGitHub Actionsを使って自動デプロイの仕組みを構築しました。 やったことは至って普通なのですが、セキュリティに拘った結果rsyncコマンドの中身とサーバーの設定で少し時間を溶かしてしまいました。

また、今回シェルスクリプトを書いてみて、勉強してみたいと思うきっかけになってよかったです。

ちょっと前にシェル芸はやめよう。みたいなツイートがバズっていましたが、時代の流れに逆行し一流シェル芸人になりたい気持ちが高まってきています。

今回やったこと

ビルド⇨デプロイまでの作業を以下のように変えました。

設定前

  1. ビルドする
  2. プッシュする
  3. マージする
  4. rsyncでデプロイする

設定後

  1. プッシュする
  2. マージする

工程が半分になりました。気持ちがいいですね。

タイトルにはカッコつけて「CI/CDパイプライン構築」と書いていますが、今回導入した仕組みではビルド&デプロイの自動化しかしておらず、まだテストは導入できていません。次デザインをガラッと変えたくなった時にでも、Storybookを入れようかなと思っています。

詳細

ここからは、備忘録も兼ねて詳細を記載していきます。

ビルドファイルをコミットするかしないか問題

私は今までビルドファイルをコミットしない派だったのですが、ビルドファイルをコミットする派もあるようでした。

なぜここで悩んだのかというと、GitHub Actionsではビルドも自動化することができるのですが、GitHub Actionsに登録するワークフローが増える&ビルド結果が異なってバグが起きたりしたら面倒だなーと思い、ならば手元でビルドしたファイルをGitHubにもプッシュして、それをそのままデプロイするようにすればいいじゃないか。と思ったのが始まりです。

ただ、結果的にこの戦略を取ることはやめてGitHub Actions上でビルド&デプロイを行うことにしました。

理由は、「push前にビルドすることを忘れそうだから」の1点だけです。

最初は package.jsonのスクリプトに以下コマンドを登録しておいて、プッシュする際には git push origin hogeではなくyarn push origin hogeにする方法を考えたのですが、他のリポジトリではgit push~~しているのでどう考えても間違えてしまいそうということで却下しました。

"scripts": {
    ・
		・
		・
    "push": "gatsby build && git push origin"
  },

スクリプトを呼び出す際は、ハイフン2つを入れて、そのあとスペースを開けるとスクリプトに続く引数を渡すことができます。

これで、ビルドしてからプッシュすることができるようになるのですが絶対間違える自信があります。

yarn push -- branch-name

一方、1人で開発している場合にはビルドファイルをコミットすることには以下のメリットもあるなと思ったので、今後の選択肢からはまだ消えていません。デバックを早くしたい時とかにはローカルでビルドした方が早いのでいいかもしれません。

  • 開発しているのは自分1人だし今後も1人なので、最新とリリース用が離れてコンフリクトなどは起こりにくい。
  • GitHub Actionsで依存関係をインストールして、ビルドしてという作業が不要になるので設定が楽。
  • ローカルだと依存関係のインストールが都度都度は不要でビルドが早い。基本的にはCI/CD回している間放置でいいのであまり問題にならないはずだが、デバッグする際には早くて楽。

workflow.yamlを作る

ここからが今回の本題です。

GitHub Actionsを用いて自動実行される作業を定義するためには、rootディレクトリ配下に .github/workflow/hoge.yaml ファイルを作りそこに設定を定義していく必要があります。

今回はデプロイの自動化なので deployment.yaml ファイルを作成しました。

最終的なファイルは以下で、シェルスクリプトだけ別で1つ書きました。

# .github/workflow/deployment.yaml
name: Deployment to sakura-vps

on:
  push:
    branches:
      - main
env:
  secret_key : ${{secrets.SECRET_KEY}}
  server_port : ${{secrets.SERVER_PORT}}
  server_ip : ${{secrets.SERVER_IP}}
  user_name : ${{secrets.USER_NAME}}
  server_destination : ${{secrets.SERVER_DESTINATION}}

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Node.js setup
        uses: actions/setup-node@v2
        with:
          node-version: 16.x
      - name: Get yarn cache directory path
        id: yarn-cache-dir-path
        run: echo "::set-output name=dir::$(yarn cache dir)"
      - uses: actions/cache@v2
        id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
        with:
          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-
      - run: yarn install
      - run: yarn build
      - name: change permissions
        run: chmod +x ./sync.sh
      - name: deploy
        run: ./sync.sh
#./sync.sh

#!/bin/sh
set -eu

KEYPATH="$HOME/.ssh"
if [ ! -d "$KEYPATH" ]; then
  mkdir -p "$KEYPATH"
fi
echo "$secret_key" > "$KEYPATH/key"
chmod 400 "$KEYPATH/key"
sh -c "rsync -vv -azr --delete -e 'ssh -i $KEYPATH/key -o StrictHostKeyChecking=no -p $server_port' ./public/ $user_name@$server_ip:$server_destination"
rm -rf $HOME/.ssh

workflow起動条件

条件は色々と設定できます。

今回は1で設定しましたが、2やその他より細かくブランチを指定した条件も作り込むことが可能です。

記法はこちらに紹介されています。

  1. mainブランチにプッシュされたら(マージもプッシュとして扱われます)
  2. 直接mainブランチにプッシュされた場合には走らないようにして、他のブランチからmainブランチにマージされた時だけなど
# 1. 今回の設定
on:
  push:
    branches:
      - main

# 2. マージされた時だけ走らせる(mainブランチに直接プッシュはダメ)
on:
  pull_request:
    branches:
      - main
    types: [closed]

jobs:
  job:
    runs-on: ubuntu-latest
    if: github.event.pull_request.merged == true

環境変数

env:
  secret_key : ${{secrets.SECRET_KEY}}
  server_port : ${{secrets.SERVER_PORT}}
  server_ip : ${{secrets.SERVER_IP}}
  user_name : ${{secrets.USER_NAME}}
  server_destination : ${{secrets.SERVER_DESTINATION}}

yamlファイルの中では環境変数を設定して、使用することが可能です。

環境変数は.envファイルではなく、https://github.com/username/repository_name/settings/secrets/actions から設定できます。

Node.jsセットアップ

- uses: actions/checkout@v2
- name: Node.js setup
  uses: actions/setup-node@v2
  with:
    node-version: 16.x

Node.jsはバージョン16系を活用しています。

最初に書いてある、 actions/checkout@v2はリポジトリのコンテンツにアクセス可能にするためのライブラリです。

キャッシュの活用

- name: Get yarn cache directory path
        id: yarn-cache-dir-path
        run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
  id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
  with:
    path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
    key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
    restore-keys: |
      ${{ runner.os }}-yarn-

actions/cacheから詳細は確認できますが、依存とビルド結果をキャッシュしてワークフローの実行を高速化するための設定をしています。

キャッシュした時としていない時とでどのぐらい速度に差が出るのかはテストしていないので不明ですが、気になるところです。

ビルド&デプロイ

- run: yarn install
- run: yarn build
- name: change permission
  run: chmod +x ./sync.sh
- name: deploy
  run: ./sync.sh

これが今回の肝です。

大枠の流れは

  1. パッケージをインストールしてビルド、その後 ./sync.shファイルを実行。
  2. スクリプト内で環境変数を使って鍵を作成して、rsyncでデプロイ。
  3. 最後に鍵を削除して終了。

という流れになっています。

ビルドまではスルーして、その先から見ていきましょう。

./sync.shファイルの実行権限を付与

- name: change permission
  run: chmod +x ./sync.sh

ワークフロー内でファイルに定義したコードを実行するためには、実行権限を付与する必要があります。そのため、chmodコマンドで ./sync.shに対する実行権限を付与しています。

鍵を使ってrsyncでデプロイ

- name: deploy
  run: ./sync.sh

ここでは、./sync.shに書いた以下のシェルスクリプトを実行しています。

#!/bin/sh
set -eu

KEYPATH="$HOME/.ssh"
if [ ! -d "$KEYPATH" ]; then
  mkdir -p "$KEYPATH"
fi
echo "$secret_key" > "$KEYPATH/key"
chmod 400 "$KEYPATH/key"
sh -c "rsync -azr --delete -e 'ssh -i $KEYPATH/key -o StrictHostKeyChecking=no -p $server_port' ./public/ $user_name@$server_ip:$server_destination"
rm -rf $HOME/.ssh

シェルスクリプトは今まで真面目に調べたり活用したりしてこなかったので、今回は入門としていい経験になりました。

マーケットプレイスで探すと、rsyncに関わるサードパーティー製ライブラリもあったのですが、記述量があるわけではない&ちょうどいい練習になりそうということで、今回は自作しました。

まず、#!/bin/shとShebangを書きます。 #!/bin/sh は ただのコメントじゃないよ! Shebangだよ!で紹介してくださっていますが、ただのコメントではないということを初めて知りました。

次に、set -euでシェルの設定を変えます。今回はeでコマンドが1つでもエラーになったら直ちにシェルを終了して。uで設定していない環境変数があったらエラーになるようにしています。

#./sync.sh
KEYPATH="$HOME/.ssh"
if [ ! -d "$KEYPATH" ]; then
  mkdir -p "$KEYPATH"
fi

GitHubデフォルトの環境変数である$HOMEを使ってKEYPATHディレクトリが存在していなかったらディレクトリを作成するようにしています。

そして、作成したディレクトリに鍵を格納します。

#./sync.sh
echo "$secret_key" > "$KEYPATH/key"
chmod 400 "$KEYPATH/key"

$secret_keyは環境変数で前回さくらVPS × CentOS Stream 9 × Nginxでホストで設定したrsyncコマンド用の鍵の中身を設定しておきます。

鍵の中身は以下のようになっているかと思いますが、最初の行と最後の行も含めて全部設定しておく必要があります。

-----BEGIN OPENSSH PRIVATE KEY-----
・
・
・
-----END OPENSSH PRIVATE KEY-----

まずは、echo "$secret_key" > "$KEYPATH/key"で設定した鍵を"$KEYPATH/key"として書き出して、 作成した鍵ファイルに対してchmod 400 "$KEYPATH/key"で読み込み権限を付与しています。

#./sync.sh
sh -c "rsync -azr --delete -e 'ssh -i $KEYPATH/key -o StrictHostKeyChecking=no -p $server_port' ./public/ $user_name@$server_ip:$server_destination"

前回設定したコマンドに近いのですが、環境変数と--deleteオプションを追加して、デプロイで存在しないファイルがリモートにあった際に削除するようにしています。

また、StrictHostKeyChecking=noを設定しておかないとワークフローからデプロイすることができません。 このオプションをnoにしていると、鍵情報が漏れた際に別のPCなどからssh接続ができるようになってしまうのでできればしたくないのですが、前回のブログでも書いた通り、rootではrsyncコマンド以外実行できないようにしているので、今回はワークフローを構築するために許容しています。

もしroot権限で実行可能なコマンドを制御していない場合には、StrictHostKeyChecking=noにしない方が良さそうです。 詳細は sshのホスト鍵を無視する方法を読ませていただきました。

#./sync.sh
rm -rf $HOME/.ssh

最後にデプロイが終わったら、念の為鍵を削除しておきます。

さてこれで、設定は終わりな気がしますが、このままワークフローを実行するとエラーになってしまいます。

protocol version mismatch~~

というような内容を含むエラーメッセージが出るはずです。 ここで気づきます。前回は自分のローカルからrsyncした結果とprotocolを合わせるために登録していたので、今回も合わせてやる必要があります。

#./sync.sh
sh -c "rsync -vv -azr --delete -e 'ssh -i $KEYPATH/key -o StrictHostKeyChecking=no -p $server_port' ./public/ $user_name@$server_ip:$server_destination"

-vvオプションをrsyncに追加して処理中の経過ファイル名を表示するようにしておきます。その状態でワークフローを実行すると以下のようなエラーメッセージが追加されているはずです。

rsync --server -vvlogDtprze.iLsfxC --delete . **

これから、vvオプションを取り除いて、remoteの/root/.ssh/authorized_keys先頭のcommandに追加します。

#remote
sudo vi /root/.ssh/authorized_keys
・
・
・
command="rsync --server -logDtprze.iLsfxC --delete 絶対パス/public/" ssh-rsa ******
・
・
・

これで問題なく実行できるはずです。 余計なファイル名が表示されないよう、再度-vvオプションを削除して完了です。

#./sync.sh
sh -c "rsync -azr --delete -e 'ssh -i $KEYPATH/key -o StrictHostKeyChecking=no -p $server_port' ./public/ $user_name@$server_ip:$server_destination"

その他

試しにruns-on: ubuntu-latestで設定していたところを、runs-on: macos-latestにしてみたところ、処理にめちゃくちゃ時間がかかり終わりませんでした。 どのプロセスが遅いのか、どこかに問題があったのかなど、今回は調査できていないのですが気になるところです。機会があれば調べてみたいと思います。


シェル芸人に俺はなる。