ちゃなべの備忘録

ほぼ備忘録です。

CI/CDをマイクロサービスに導入してみた【GitHubActions】【備忘録】

はじめに

最近マイクロサービスを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

Git Hub Actions入門

実装の参考探し

Go でのビルドとテスト - GitHub Docs

→ バックのCDはこれを参考にしよう。公式だし。

GitHub ActionsでSpringBootアプリをさくらVPSにSSHしてデプロイする - Qiita

GitHub Actions から安価なVPSのサーバーにデプロイするための Self-hosted Runner - 猫でもわかるWebプログラミングと副業

→ さくらVPSへのデプロイの参考

CI実装

まぁまぁ調べたのでさっそくやってみようか。

キャッチアップ

このページを元に試しに使ってみる。

zenn.dev

マジでこのページに沿ってやっているだけなので、学びになったことをメモにまとめます。

  • あ、結構簡単
  • コマンドの意味
    • 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作った。

ayumu1212.hatenablog.com

それを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

いろいろ調べてみたけど、まとまった記事があんまりなさげ。 これを参考

zenn.dev

どうやら、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におけるエラーっぽい。

GithubActionsのbackCIのエラー内容

言ってることは、「他のモジュールが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

方針決め

このサイトみてみた。

www.utakata.work

self-hosted runnerいいよって言ってるんだけど、たぶん本番環境でbuildして実行しちゃった方がよくない?っていってる。 それは少し困る。buildはgithubActions内でやって、scpでファイルを移動して実行してほしい。

いろいろ調べた結果、方針はこんな感じかと。

  1. mainにマージかpushされたことを検知してCD開始
  2. buildしてバイナリファイルをつくる
  3. バイナリファイルを本番環境に移動する
  4. 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できていますね。

githubActionsのCDテストの実行結果

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

githubActionsのCDテストの実行結果
githubActionsのCDテストの実行結果

いいね!できてる。じゃあ次はこれを本番環境にscpして送りましょ。

4. バイナリファイルを本番環境に移動する

これ参考

qiita.com

まずは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を押したら、以下の画面が出てくるのでキーを登録しましょう。

Githubのsecretkeyの登録画面
Githubのsecretkeyの登録画面

私は参考サイトをもとにこうしました。

じゃあ実際に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/

これでどうだ?

githubActionsのCDテストの実行結果

えーーーまだだめ?てかhostでずっと怒られているの何だろう。。 んーーなんか秘密鍵を別から持ってきたらいろいろ設定する必要があるっぽいな。

参考

zenn.dev

mgre.co.jp

これをもとにまた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。

githubActionsのCDテストの実行結果

あとやること

  • yamlファイルのリファクタ
  • sshでsystemctlを停止して起動する処理追加
  • サーバーがちゃんと動いているか確認する処理追加
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

これで、デプロイまで完成した。

参考

docs.github.com

qiita.com

CDの導入 - client

最後はclientだ。 backのCDやったから何となくわかる。流れはこんな感じやろな。

  1. buildする
  2. buildしたフォルダをscpで送る
  3. 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の実装は以上!