はじめに
最近マイクロサービスをscpだけでデプロイしてみました。
dockerを安くデプロイしたい【備忘録】【読みづらい】 - Qiita
まだまだ追加実装したいのですが、毎回scpするの面倒なのでCICDを実装します! なお、CICDは初めてやるので足からず! 前回CICDツールの選定をした結果、今回はGitHubActionsを使います。
前提イメージ
いろいろ調べる前のイメージ
CI
- lintは回したいかな
- フロント: typescript, React
- バック:Go
- あとはわからん、actionsみて随時きめよう
- もしかしたらbuildもこっちに入る?
CD
- デプロイしたい
- フロント:
npm run build
したbuildフォルダをアップ - バック:
go build
したmainファイルをアップ - DB:migrationとかするんかな?
- フロント:
- サーバーの再起動とかしたいよね
- build自体の処理がCIなのかCDなのかがわからん
調査
やり方が全くわからんので、まずは調べまする。
GitHubActionsの概要を掴む
GitHubの新機能「GitHub Actions」で試すCI/CD | さくらのナレッジ
→ 長かったけど、だいったいわかった
GitHub ActionsでCI/CD環境を学びながら構築する - Qiita
→ work-flowの全体を掴めた。案外簡単そうという印象を持てた。
GitHub Actionsを使ったマイクロサービスのCI/CDモジュール管理 | MoT Lab (Mobility Technologies Engineering Blog)
→ ようわからんかったんで、また今度読もう。CICDのコンポーネント化してるんかな。
【GitHub Actions】ワークフローはなんとなく書けるけど構造や仕組みの理解が曖昧な方々向けの記事 - Qiita
→
実装の参考探し
→ バックのCDはこれを参考にしよう。公式だし。
GitHub ActionsでSpringBootアプリをさくらVPSにSSHしてデプロイする - Qiita
GitHub Actions から安価なVPSのサーバーにデプロイするための Self-hosted Runner - 猫でもわかるWebプログラミングと副業
→ さくらVPSへのデプロイの参考
CI実装
まぁまぁ調べたのでさっそくやってみようか。
キャッチアップ
このページを元に試しに使ってみる。
マジでこのページに沿ってやっているだけなので、学びになったことをメモにまとめます。
- あ、結構簡単
- コマンドの意味
- runs-on: 実行環境設定
- steps/run: 実行したいシェルスクリプト
- steps/uses: 実行したいactionsを選択
- stepsはrunかusesだけ。だけ!
これだけでも相当GithubActionsを理解した。 やっぱり実装してみるのがはええわ。
CIの導入 - client
さぁ実際のアプリケーションに導入してみましょう。 じゃあ、とりあえずフォルダ作って動かしてみよう。
$ mkdir .github $ cd .github $ mkdir workflows $ cd workflows $ touch client-ci.yaml
yamlファイルにまずは、「PRが出されたときにecho
がでるように」する。
name: client-ci on: pull_request: types: [opened] jobs: lint-run: runs-on: ubuntu-latest steps: - run: echo "this is pen"
じゃあこれをpushしてPRを出してみましょ。
でーけた!いいね。 だけど、これ思ったけど、先にlintを作らんとあかんな。 簡単に作りましょう。
lint作った。
それをGithubActionsで実行してみる。
name: client-ci on: pull_request: types: [opened] jobs: lint-run: runs-on: ubuntu-latest steps: - name: checkout pushed commit uses: actions/checkout@v3 - run: cd nabelog/services/client/app/ - name: set up node uses: actions/setup-node@v3 with: node-version: 16.15.0 - name: run lint run: | npm install npm run lint
そしたらエラーでた。
あぁ、フォルダ階層間違えたわ、修正して再プッシュ。 だけど、再プッシュしただけではGithubActionsは再度回ってくれない。 だから、onを変える。あとnode入れてからcdしよう。
name: client-ci on: pull_request jobs: lint-run: runs-on: ubuntu-latest steps: - name: checkout pushed commit uses: actions/checkout@v3 - name: set up node uses: actions/setup-node@v3 with: node-version: 16.15.0 - name: run lint run: | cd services/client/app/ npm install npm run lint
そしたらおーー!でけた。
CIの導入 - back
いろいろ調べてみたけど、まとまった記事があんまりなさげ。 これを参考
どうやら、githubActionsにあるgoのlintはこれが一番ポピュラーっぽい。
GitHub - golangci/golangci-lint-action: Official GitHub action for golangci-lint from its authors
これを参考にファイルを作ってみた。
name: back-ci on: pull_request jobs: lint-run: runs-on: ubuntu-latest steps: - name: checkout pushed commit uses: actions/checkout@v3 - name: set up go uses: actions/setup-go@v3 with: node-version: 1.19.1 - name: run lint uses: golangci/golangci-lint-action@v3 with: version: v1.29 working-directory: services/server/src
じゃあこれでPR出してみよう。 だが、エラーでた。lintのエラーというよりか、setupにおけるエラーっぽい。
言ってることは、「他のモジュールがimportできないよ」 これ、思ったんだけど、localで同じツール使ってないのに、いきなりCIで別のツール使うっておかしいよね。 ということは、actionsを使うんじゃなくて、clientみたいにコマンド実行した方が良いんじゃないかなとおもた。
だけど、そもそもgoimportsはformatterでありlinterではない。なので、エラーとして出力することができないように思えた。 なので、やっぱりこれで実行できるまでやってみる。 そして最終このようになった。
name: back-ci on: pull_request jobs: lint-run: runs-on: ubuntu-latest steps: - name: checkout pushed commit uses: actions/checkout@v3 - name: set up go uses: actions/setup-go@v3 with: go-version: 1.19.1 - name: run lint uses: golangci/golangci-lint-action@v3 with: version: latest working-directory: services/server/src
やったこと
- set up goのversion指定でタイポしてたので修正
- golangci-lint-actionをとりあえずversion:latestに
これで動くので一旦よし。
CDの実装
次はCDの実装をやっていくぅ。
CDの導入 - db
さぁこっからむずくなってくる。 なんせ、さくらVPSをgithubActionsから触らないといけないからね。 セキュリティどうなんやろう。
調べてみた結果、dbへのCDはまだ必要なさそう。DBサーバーのおおもとのスイッチングは流石に手動でやるし、現状サーバーが起動したらmysqlも動くようになっているし、migrateやseedの実行もDBサーバーでさせる予定がない。
これはREADMEに書けば大丈夫そう。
CDの導入 - back
方針決め
このサイトみてみた。
self-hosted runnerいいよって言ってるんだけど、たぶん本番環境でbuildして実行しちゃった方がよくない?っていってる。 それは少し困る。buildはgithubActions内でやって、scpでファイルを移動して実行してほしい。
いろいろ調べた結果、方針はこんな感じかと。
- mainにマージかpushされたことを検知してCD開始
- buildしてバイナリファイルをつくる
- バイナリファイルを本番環境に移動する
- systemctlをrestartする
バイナリファイルだけ本番環境に移動するのちょっと嫌なんだよね。。容量的には圧倒的にいいけど、開発環境と完全に同期してないから不安。だけどそんなのdockerを使ってないっていうところから一緒だからもういっか!w. あとは作りながらやってこ。
1. mainにマージかpushされたことを検知してCD開始
いろいろ調べたけど、記述的にはこう。
name: back-cd on: push: branches: - main
これだと、mainに直接pushされた時も検知するので使い勝手が良い。 なお、検知しないのは以下のコード
name: back-cd on: pull_request: branches: - main types: [closed]
2. buildしてバイナリファイルをつくる
じゃあ早速作ってこ。 なお、jobsより前は省略してる。
jobs: build: runs-on: ubuntu-latest steps: - name: checkout pushed commit uses: actions/checkout@v3 - name: set up go uses: actions/setup-go@v3 with: go-version: 1.19.1 - name: start building run: go build main.go working-directory: ./services/server/src - name: deketa? run: ls services/server/src
これをやってみると、無事できてるーー。 バイナリファイルはGithub上にあげていないので、mainファイルがあるということはbuildできていますね。
3. バイナリファイルを別jobに移す
先ほどbuildしたファイルを本番環境に上げましょう!
job名はdeployとして、先ほどのjobと分けたい。そのため、さっきのjobからbuildしたファイルを取得する。
まずここまでやってみましょ。
jobs: build: runs-on: ubuntu-latest steps: ・ ・ - name: archive build file uses: actions/upload-artifact@v3 with: name: nabelog-server-build-main-file path: services/server/src/main deploy: needs: build runs-on: ubuntu-latest steps: - name: download build file uses: actions/download-artifact@v3 with: name: nabelog-server-build-main-file - name: deketa? run: ls -l
いいね!できてる。じゃあ次はこれを本番環境にscpして送りましょ。
4. バイナリファイルを本番環境に移動する
これ参考
まずはGithubActionsとbackサーバーを繋ぐための鍵をローカルで作る。
# RSA形式で、4096bitで、ユーザー名とホスト名をコメントでつけて、file名も指定する。 $ ssh-keygen -t rsa -b 4096 -C "ユーザー名@ホスト名" -f id_rsa_nabelog_server_cd
でけたので、これの公開鍵をまずはbackサーバーに載せる。
backサーバーにログインして、~/.ssh/authorized_keys
を開いてみるとすでに利用されているsshのpublicキーが登録されている。
ここの一番下に、さっき作成したpublicキーを貼り付ける。
これでできたのかな?ちょっとPCからログインしてみる。
$ ssh -i ~/.ssh/id_rsa_nabelog_server_cd ユーザー名@ホスト名
SAKURA internet [Virtual Private Server SERVICE]
おーーー!でけた。
次はGithubActionsで使うためにGithubに秘密鍵を登録する。
Githubのレポジトリを開いて、
Settings > Security > Secrets and variables > Actions > New repository secret
を押したら、以下の画面が出てくるのでキーを登録しましょう。
私は参考サイトをもとにこうしました。
じゃあ実際にCDで送ってみる。yamlファイルはこういうふうにやった。
deploy: needs: build runs-on: ubuntu-latest steps: - name: download build file uses: actions/download-artifact@v3 with: name: nabelog-server-build-main-file - name: scp for back server uses: appleboy/scp-action@master with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USERNAME }} key: ${{ secrets.SK }} port: ${{ secrets.SSH_PORT }} source: "./main" target: "/usr/local/src/" - name: deketa? uses: appleboy/ssh-action@master with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USERNAME }} key: ${{ secrets.SK }} port: ${{ secrets.SSH_PORT }} script: ls -l /usr/local/src/
だけど、なんかエラーがでた。どうやらscpのコマンドでミスっているっぽい。ということは鍵系でミスってる? じゃあ、sshだけ実行できるようにしてみよう。
あれ、成功した。じゃあscpのコマンドがなんかミスっているっぽい。 sshコマンドはlocalもgithubAction上でもできたけど、そういえばscpコマンドは確かめてないな。
localでscpしてみる。すると、
$ scp -i ~/.ssh/id_rsa_nabelog_server_cd main ユーザー名@ホスト名:/usr/local/src/ scp: /usr/local/src//main: Text file busy
ん?忙しい?あーーー実行中だからか。 じゃあsystemctlで止めてからもう一回やってみよう。
$ ssh nabelog-server # サーバーにログイン $ sudo systemctl stop start-apiserver.service $ sudo systemctl status start-apiserver.service ○ start-apiserver.service - Start nabelog API server Loaded: loaded (/etc/systemd/system/start-apiserver.service; enabled; vendor preset: disabled) Active: inactive (dead) since Sat 2023-03-04 12:56:18 JST; 3s ago Process: 533 ExecStart=/usr/local/src/main & (code=killed, signal=TERM) Main PID: 533 (code=killed, signal=TERM) CPU: 53.620s $ exit # localのterminalに戻る $ scp -i ~/.ssh/id_rsa_nabelog_server_cd main ユーザー名@ホスト名:/usr/local/src/ main 100% 15MB 909.3KB/s 00:16
おおーーーでけた。これが原因だったとか? もう一回CDやってみましょ。
あとなんか環境変数の使い方まちがえていたっぽいから変えてみた。
deploy: needs: build runs-on: ubuntu-latest steps: - name: download build file uses: actions/download-artifact@v3 with: name: nabelog-server-build-main-file - name: download secretKey env: PRIVATE_KEY: ${{ secrets.SK }} run: | mkdir -p ~/.ssh echo "$PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa chmod 700 ~/.ssh/id_rsa - name: scp for back server env: USER_NAME: ${{ secrets.SSH_USERNAME }} HOST: ${{ secrets.SSH_HOST }} run: scp -i ~/.ssh/id_rsa main ${USER_NAME}@${HOST}:/usr/local/src/ - name: deketa? uses: appleboy/ssh-action@master with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USERNAME }} key: ${{ secrets.SK }} port: ${{ secrets.SSH_PORT }} script: ls -l /usr/local/src/
これでどうだ?
えーーーまだだめ?てかhostでずっと怒られているの何だろう。。 んーーなんか秘密鍵を別から持ってきたらいろいろ設定する必要があるっぽいな。
参考
これをもとにまたyaml変えてみた。
- name: download secretKey env: PRIVATE_KEY: ${{ secrets.SK }} HOST: ${{ secrets.SSH_HOST }} run: | mkdir -p ~/.ssh echo "$PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa chmod 700 ~/.ssh/id_rsa eval $(ssh-agent -s) ssh-add ~/.ssh/id_rsa ssh-keyscan -H ${HOST} >> ~/.ssh/known_hosts
おおーーー!成功した。 ちゃんとmainファイルの日時変更されているっぽいし、おk。
あとやること
yamlファイルのリファクタ
うーーん、このappleboyさん、ちょっと不穏な感じがする。。 Issueがすごく溜まっている感じがするし、解消の仕方も雑に見える。。
なるべくactionはつかわない方針でいこう。
sshでsystemctlを停止して起動する処理追加
たぶん、こう。 これでやってみる。
deploy: needs: build runs-on: ubuntu-latest steps: - name: download build file uses: actions/download-artifact@v3 with: name: nabelog-server-build-main-file - name: setup secretKey env: PRIVATE_KEY: ${{ secrets.SK }} run: | mkdir -p ~/.ssh echo "$PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa chmod 700 ~/.ssh/id_rsa eval $(ssh-agent -s) ssh-add ~/.ssh/id_rsa ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts - name: systemctl stop main by ssh run: | ssh ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} " sudo systemctl status start-apiserver.service; sudo systemctl stop start-apiserver.service; sudo systemctl status start-apiserver.service; " - name: scp for back server run: scp main ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }}:/usr/local/src/ - name: systemctl start main by ssh run: | ssh ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} " sudo systemctl start start-apiserver.service; sudo systemctl status start-apiserver.service; "
これやると、systemctl status
の時にサービスが止まっていたら、なぜかエラーを出す。どう調べてもそんなはずはないのに。
だから方針を変えて、systemctl is-active
を使ってこう書いてみた。
その結果、完璧すぎた。
サーバーの起動を確認する処理
これなんだけど、さっきの謎のバグを利用する。
systemctl status
を最後に仕込んどけば、inactiveの時に止まってくれる。
そしてあと、やっぱりif文をshell側に責任持たすのはちょっと嫌なので、GithubActions内でやることにする。
完成したコードがこれ 重要なのは以下
- linuxコマンドでは変数代入の右辺で処理をしたいときは、$()ではなく``
- outputsを扱いたいときは
>> $GITHUB_OUTPUT
deploy: needs: build runs-on: ubuntu-latest steps: - name: download build file uses: actions/download-artifact@v3 with: name: nabelog-server-build-main-file - name: setup secretKey env: PRIVATE_KEY: ${{ secrets.SK }} run: | mkdir -p ~/.ssh echo "$PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa chmod 700 ~/.ssh/id_rsa eval $(ssh-agent -s) ssh-add ~/.ssh/id_rsa ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts - name: systemctl is-acitve main by ssh id: check_active_pre run: | IS_ACTIVE_STATUS=`ssh ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} "sudo systemctl is-active start-apiserver.service"` echo "IS_ACTIVE_STATUS=${IS_ACTIVE_STATUS}" >> $GITHUB_OUTPUT - name: systemctl stop main by ssh if: steps.check_active_pre.outputs.IS_ACTIVE_STATUS == 'active' run: ssh ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} "sudo systemctl stop start-apiserver.service" - name: main for back server by scp run: scp main ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }}:/usr/local/src/ - name: systemctl start main by ssh run: ssh ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} "sudo systemctl start start-apiserver.service" - name: systemctl is-acitve main by ssh id: check_active_post run: | IS_ACTIVE_STATUS=`ssh ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} "sudo systemctl is-active start-apiserver.service"` echo "IS_ACTIVE_STATUS=${IS_ACTIVE_STATUS}" >> $GITHUB_OUTPUT - name: check active if: steps.check_active_pre.outputs.IS_ACTIVE_STATUS != 'active' run: exit 3
これで、デプロイまで完成した。
参考
CDの導入 - client
最後はclientだ。 backのCDやったから何となくわかる。流れはこんな感じやろな。
- buildする
- buildしたフォルダをscpで送る
- nginxをsystemctlで再起動する
以上かな?やってく。
buildする
ひとまつbuildして、deployのjobに移すまで。
name: client-cd on: pull_request jobs: build: runs-on: ubuntu-latest steps: - name: checkout pushed commit uses: actions/checkout@v3 - name: set up node uses: actions/setup-node@v3 with: node-version: 16.15.0 - name: run build run: | npm install npm run build working-directory: ./services/client/app - name: archive build folder uses: actions/upload-artifact@v3 with: name: nabelog-client-build-folder path: services/client/app/build/ deploy: needs: build runs-on: ubuntu-latest steps: - name: download build folder uses: actions/download-artifact@v3 with: name: nabelog-client-build-folder - name: deketa? run: | ls cd build ls
buildしたフォルダをscpで送る&nginxの再起動を行う
まずはclientサーバーの~/.ssh/authorized_keys
にpublicキーを追加する。これはbackと同じものでよい。
それが終わったら、githubに秘密鍵の登録をする。これもbackと同じ作業。名前はSSH_CLIENT_HOSTにした
結果こうなった。
deploy: needs: build runs-on: ubuntu-latest steps: - name: download build file uses: actions/download-artifact@v3 with: name: nabelog-server-build-main-file - name: setup secretKey env: PRIVATE_KEY: ${{ secrets.SK }} run: | mkdir -p ~/.ssh echo "$PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa chmod 700 ~/.ssh/id_rsa eval $(ssh-agent -s) ssh-add ~/.ssh/id_rsa ssh-keyscan -H ${{ secrets.SSH_BACK_HOST }} >> ~/.ssh/known_hosts - name: systemctl is-acitve main by ssh id: check_active_pre run: | IS_ACTIVE_STATUS=`ssh ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_BACK_HOST }} "sudo systemctl is-active start-apiserver.service"` echo "IS_ACTIVE_STATUS=${IS_ACTIVE_STATUS}" >> $GITHUB_OUTPUT - name: systemctl stop main by ssh if: steps.check_active_pre.outputs.IS_ACTIVE_STATUS == 'active' run: ssh ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_BACK_HOST }} "sudo systemctl stop start-apiserver.service" - name: main for back server by scp run: scp main ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_BACK_HOST }}:/usr/local/src/ - name: systemctl start main by ssh run: ssh ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_BACK_HOST }} "sudo systemctl start start-apiserver.service" - name: systemctl is-acitve main by ssh id: check_active_post run: | IS_ACTIVE_STATUS=`ssh ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_BACK_HOST }} "sudo systemctl is-active start-apiserver.service"` echo "IS_ACTIVE_STATUS=${IS_ACTIVE_STATUS}" >> $GITHUB_OUTPUT - name: check active if: steps.check_active_post.outputs.IS_ACTIVE_STATUS != 'active' run: exit 3
これでちゃんと動いた! CICDの実装は以上!