GitLab CI/CD を利用した継続的デプロイの試行錯誤
#gitlab #ci #cd #備忘
これまでCIは利用してきた。
主に、テストコードの実行と、コンパイルの実行として利用していて、
手元でユニットテストを動かし忘れてもgitlabのリポジトリにpushさえすれば、
テストして問題があればNGを出してもらえるようにしていた。
もちろんコンパイルに失敗してもNGを出してもらう。
今回はここから本番サーバへのリリースまでを自動化したいと思い、色々試す。
CDのイメージ
具体的な方法とかはぼんやりとしか知らないから、
色々調べる前のぼんやりイメージになるけど、下記みたいなイメージ。
1. ユニットテスト
2. コンパイル、ビルド
3. 2でビルドしたコードを本番環境で取得
4. 動いているアプリケーションを停止し、3で取得してきたバイナリを起動
もしくは
1. ユニットテスト
2. コンパイルでのテスト
3. 本番環境でソースコード取得
4. ビルド
5. 動いているアプリケーションを停止し、4で生成したバイナリを起動
ぱっと思い浮かぶけど、選択しないCDの方法
例えば一番簡単な本番環境へのデプロイは、本番環境が定期的にgit fetchして、差分が出たときにリビルドする。って方法。
cronを使えば比較的容易に出来ると思う。
デメリットとしてはテストのジョブがこけてもお構いなしに取得してビルドしちゃうところかな?
実際は本番環境のブランチなり、リリースタグをつけるなりをする前に、十分なテストをしてるはずやから、
そんなことは起きないはずやけど、まあゼロではないということで。
この方法を選ばないモチベーションというのはないけど、
できればGitLab CI/CDを活用したい。
リリースにこけたこととか検知したいし。GitLabを通してればGitLabで一元管理できるし。
どこでテストするか
ただ、本番環境でテストのJobは動かしたくない。
DBをMock化したりしていたとしても、テストにリソースを使うのは間違いないことで、
本番環境にそんな負荷はかけたくない。
なので、自動テストは違うサーバで行ないたい。例えば、GitLabのShared Runnerとか、テスト用のRunnerサーバとか。
つまり、ステージやジョブによって実行するサーバを変える必要がある。
たぶんできる!と思ってるけど、本当にできるのか調べる必要がある。
とりあえず手近なUbuntu環境にRunnerを入れてみる
実際に動くかどうかまでは分からないけど、とりあえず一番簡単に動かせるUbuntu環境にRunnnerを入れる。
今回はDockerのUbuntuイメージでやる。
$ docker run -it --rm ubuntu
$ cat /etc/os-release | grep VERSION
VERSION="20.04.2 LTS (Focal Fossa)"
VERSION_ID="20.04"
VERSION_CODENAME=focal
バージョンは20
$ apt update
$ apt install -y curl
下記を参考にrunnnerをいれる
参考: Install GitLab Runner using the official GitLab repositories | GitLab
$ curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | bash
$ GITLAB_RUNNER_DISABLE_SKEL=true apt install -y gitlab-runner
GITLAB_RUNNER_DISABLE_SKELは $HOME じゃない場所にディレクトリを作る設定らしい?
とりあえず問題を起こさないためには必要とのことなのでつけとこ
コマンド実行時に指定したけど、環境変数に入れといてもいいかも
$ gitlab-runner -v
Version: 13.12.0
Git revision: 7a6612da
Git branch: 13-12-stable
GO version: go1.13.8
Built: 2021-05-20T15:16:05+0000
OS/Arch: linux/amd64
gitlab runnerのインストール完了
runner用のtokenを取得する
適当なテスト用のリポジトリを作って、Settings > CI/CD > Runners にいく
Specific Runnersの欄にある Set up a specific runner manually に表示されているURLとtokenを使う
必要ならReset registration tokenで再生成してもOK
下記を参考に登録する
参考: Registering runners | GitLab
$ gitlab-runner register
Enter the GitLab instance URL (for example, https://gitlab.com/):
前の手順で取得したURL
Enter the registration token:
uK9pSW6r5U5qErRxfZxz
前の手順で取得したtoken
Enter a description for the runner:
test ci/cd
後から見て分かるように適当な説明つけとく
Enter tags for the runner (comma-separated):
deploy
どんなタグにするかは分からないけど、実際にデプロイするジョブのみを実行したいからとりあえずdeployにしとく
Registering runner... succeeded runner=MweLPxjN
Enter an executor: shell, virtualbox, parallels, docker, docker-ssh, ssh, docker+machine, docker-ssh+machine, kubernetes, custom:
shell
dockerコンテナ内ではあっても、中身はubuntuなのでshellで動かすことを考える
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!
準備できたよ!
runner用のtokenを取った画面に先ほど付けた説明とかタグが表示されているrunnnerが追加されたことを確認する
https://gyazo.com/a9a37a9cc671b10a5c7f2e03925d318e
runnerのインストールはこれでおわり。簡単ですね。
今回は --rm 指定したコンテナで動かしてるので、consoleなり、terminalなりを閉じるとコンテナが削除され、
セットアップしたrunnerが消えるので、gitlabのWebからもRemoveする必要があります。
また簡単に立ち上げられるので、気軽に作って決してを繰り返しながら試すのが良さそう。
必要ならgitlab-runnerのインストールまでDockerfileにしておくのもいいかもですね。
たぶんDockerHubで探せばimageは見つかると思うので、そっちを使ってもいいかもです。
おまけ程度にdockerfileからimage生成
どんなコマンドを実行すればいいか分かったので、それを実行したあとのコンテナを用意したほうが使い捨てやすいです。
毎回同じコマンドが実行される時間を待つのは嫌なので、実行した後のコンテナがほしいなーってことですね。
とりあえず先ほどやったことをDockerfileに書きます。
code:Dockerfile
FROM ubuntu:latest
# 環境変数
ENV GITLAB_RUNNER_DISABLE_SKEL=true
# 必須コマンドの準備
RUN apt update -y && \
apt install -y curl
# gitlab-runnerのinstall
RUN curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | bash && \
apt install -y gitlab-runner && \
gitlab-runner verify
あとはこのDockerfileをbuildするだけです。
$ docker build -t tsuchinaga/runner:0.1 .
カレントのDockerfileをbuildして、tsuchinaga/runner:0.1を作りますというコマンド。
これでDockerfileに書かれている処理が終わればbuildは終わります。
後は作ったimageを元にrunするだけ。
$ docker run -it --rm tsuchinaga/runner:0.1
簡単ですねー。
用意したRunnerで指定のジョブだけ動かしてみる
runnerを使い捨てられるよう簡単に環境を構築する手順を確認できたので色々試していきます。
まずは、testとbuildを実行し、両方に成功したらdeoloyするというフローを考えます。
それをgitlab-ciで書くと、
code:.gitlab-ci.yml
image: golang:1.16
variables:
TZ: Asia/Tokyo
stages:
- test
- deploy
build-project:
stage: test
tags:
- gitlab-org
script:
- echo hello,build-world!
test-project:
stage: test
tags:
- gitlab-org
script:
- echo hello,test-world!
deploy:
stage: deploy
tags:
- deploy
script:
- echo hello,deoloy-world!
Go 1.16を指定しているのはこの後デプロイで試したいからです。
が、現在は不要なのでubuntuでもalpineでもなんでもいいです。
stagesで指定しているジョブを順番に実行していきます。
stagesには複数のjobがあり、testステージにはbuild-project、 test-projectが含まれています。
testステージのジョブにはtagsに gitlab-org を指定しています。
これはshared runnerについてるタグです。
delopyステージにはdeployだけがあり、deployジョブのタグはdeployです。
先の手順でrunnerに設定したタグと一致させています。
これで、testステージはshared runnner、deoloyステージは自前のrunnnerということができそうです。
上記のciファイルをpushすると、CIが走り出します。
実行後ですが、こんな感じ。
https://gyazo.com/2b18bbf969910d21f8a35b56acc8ff1f
試しにbuild-projectを開いてみました。
選択している文字列の部分がrunnerのハッシュ値で、これはshared runnerのものと一致しています。
https://gyazo.com/e5d22f8b27a91e9acab0feb5576f73c7
deoloyのを確認してみると、ちゃんとspecific runnerのハッシュ値になっていました。
目的の処理は目的のrunnerで。は簡単に出来ますね。
動かないなーと思ったら
gitlab-runnerがうごかないなーと思ったら、とりあえず gitlab-runner restart しておけばいいと思う。
インストールしてすぐは立ち上がっていないので、むしろ restart しておかないと認識されないかも
プログラムをビルドして実行してみる
コンパイラ言語だとGoが得意なので、Goでやります。
とりあえずdeoloy時にbuild & runをします。
Goのプログラムは簡単な標準出力だけのプログラム。
ciではbuildはビルドだけ、testはテストだけ、deoloyはビルド+実行です。
code:.gitlab-ci.yml
image: golang:1.16
variables:
REPO_NAME: gitlab.com/$CI_PROJECT_PATH
TZ: Asia/Tokyo
stages:
- test
- deploy
build-project:
stage: test
tags:
- gitlab-org
script:
- mkdir -p $GOPATH/src/$REPO_NAME
- mv $CI_PROJECT_DIR/* $GOPATH/src/$REPO_NAME
- cd $GOPATH/src/$REPO_NAME
- go build ./...
- ls -al
test-project:
stage: test
tags:
- gitlab-org
script:
- mkdir -p $GOPATH/src/$REPO_NAME
- mv $CI_PROJECT_DIR/* $GOPATH/src/$REPO_NAME
- cd $GOPATH/src/$REPO_NAME
- go test ./...
deploy:
stage: deploy
tags:
- deploy
script:
- mkdir -p $GOPATH/src/$REPO_NAME
- mv $CI_PROJECT_DIR/* $GOPATH/src/$REPO_NAME
- cd $GOPATH/src/$REPO_NAME
- go run main.go
すると、 deploy でエラーがでました。
https://gyazo.com/2c9c7514a66f1733130460a148f0d01d
パーミッションエラーです。
gitlab-runnerの実行ユーザーは?
gitlab-runnerの実行ユーザーはgitlab-runnerです。
$ ps ax
root@3ec9c7cfbc87:/go# ps ax
PID TTY STAT TIME COMMAND
1 pts/0 Ss 0:00 bash
60 ? Sl 0:00 /usr/bin/gitlab-runner run --working-directory /home/gitlab-runner --config /etc/gitlab-runner/config.toml --service gitlab-runner --syslog --user gitlab-runner
180 pts/0 R+ 0:00 ps ax
長くてわかりにくいですが、PID60のプロセスの末尾に --user gitlab-runner とあると思います。
これが実行ユーザーを定義しているものですね。
$ gitlab-runner stop
で一回止めといて、
$ gitlab-runner run --working-directory /home/gitlab-runner --config /etc/gitlab-runner/config.toml --service gitlab-runner --syslog
でrunnerを起動します。
違いは、末尾の --user オプションを消したことです。
これによって、rootユーザーで実行できました。
ただ、rootユーザーで実行できるのは諸刃の剣で、権限的に危ないことをしそうなら実行ユーザーを別に作ったほうがいいです。
PATHが違う
もう一点引っかかったのが、PATHの値が違うことです。
dockerにアクセスしてPATHを確認するとこうで
$ echo $PATH
/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
runnnerごしだとこう。
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
goの実態は /usr/local/go/bin にあるので、このままでは go run main.go が動きません。
ってことで、 /usr/local/go/bin/go run main.go のように絶対パスで指定して動かします。
ここまでで出来た .gitlab-ci.yml はこんな感じ
code:.gitlab-ci.yml
image: golang:1.16
variables:
REPO_NAME: gitlab.com/$CI_PROJECT_PATH
TZ: Asia/Tokyo
stages:
- test
- deploy
build-project:
stage: test
tags:
- gitlab-org
script:
- mkdir -p $GOPATH/src/$REPO_NAME
- mv $CI_PROJECT_DIR/* $GOPATH/src/$REPO_NAME
- cd $GOPATH/src/$REPO_NAME
- go build ./...
- ls -al
test-project:
stage: test
tags:
- gitlab-org
script:
- mkdir -p $GOPATH/src/$REPO_NAME
- mv $CI_PROJECT_DIR/* $GOPATH/src/$REPO_NAME
- cd $GOPATH/src/$REPO_NAME
- go test ./...
deploy:
stage: deploy
tags:
- deploy
script:
- mkdir -p $GOPATH/src/$REPO_NAME
- mv $CI_PROJECT_DIR/* $GOPATH/src/$REPO_NAME
- cd $GOPATH/src/$REPO_NAME
- /usr/local/go/bin/go run main.go
他のJobでBuildし、Deployでは実行するだけ
本番サーバで毎回Buildするのはあまり良くない選択だと思います。
どういったプログラムかにもよりますが、実行時とBuild時で使うリソースには乖離があることも少なくなく、
Buildを含めるために余分にリソースを割り当てる羽目になることもあります。
もし本番サーバでプログラムが動いているときに、デプロイされ、Buildされてしまえば、リソース枯渇もあり得ます。
そうならないように、Buildは他の環境で行い、Buildで出力された成果物のみを本番環境に渡そうという作戦です。
artifacts
gitlab-ciにはartifactsというものがあり、Git管理されていないファイルでもCI中はJobをまたいでコピーされます。
ただ制約もあって、gitlab-runnerが動作しているカレントディレクトリ配下のファイルしかartifactsに登録できません。
そこだけ意識していれば、buildしたバイナリファイルなりだけを本番環境に渡すことは難しくありません。
code:.gitlab-ci.yml
image: golang:1.16
variables:
TZ: Asia/Tokyo
stages:
- test_stage
- deploy_stage
build:
stage: test_stage
tags:
- gitlab-org
script:
- go build ./...
artifacts:
paths:
- ./ci-cd
expire_in: 1 days
test:
stage: test_stage
tags:
- gitlab-org
script:
- go test ./...
deploy:
stage: deploy_stage
tags:
- deploy
needs:
- build
script:
- ./ci-cd
buildというJobでBuildしています。
ci-cdというプロジェクトなので、出力された成果物もci-cdになっていますね。
ci-cdをartifactsに登録し、続くJobに渡したいだけなので期限を1日にしています。
期限はもっと短くてもいいかもですね。
次にdeployJobでneedsしています。
needsキーワードは、指定したJobからのみartifactsをダウンロードするようにするものです。
これをしなくてもダウンロードはするのですが、不要なものを取り込まないために指定しています。
そしてダウンロードしたcd-cdを実行するという流れです。
常駐するプログラムをデプロイし再起動したい
デプロイするプログラムがバッチ処理で、crontabとかで定期的に実行されるものなら
Buildしたバイナリを所定のディレクトリに設置するだけで十分だと思う。
他にもPHPのようなインタプリタ言語で、実行時解決されるものであっても、
所定のディレクトリに設置するだけで十分だと思う。
ただWebサーバを含んでいるようなプログラムを自動でデプロイしたり、
それ以外でも基本的に24時間どうさせ続けるプログラムを自動でリリースしたい場合、
デプロイだけでなくサービスの再起動が必要になる。
ここで問題になるのが
バックグラウンド起動する方法
バックグラウンドで動いているプロセスを停止し、再度実行する方法
停止時に安全に停止させる、もしくは時間指定して再実行する方法
だとおもう。
sshでサーバにアクセスして常駐プログラムをバックグラウンドで起動するには、
$ nohup ./hoge &
みたいな方法があると思う。
nohupでターミナルがログアウトしても動き続け、
&でバックグラウンド実行を指定している。
他にはsystemdを利用してdaemonとして登録する方法もある。
これまではnohup &を使ってたけど、自動デプロイだとPIDを管理してもらえるsystemdが便利そう。
ってことで、systemdを使ってサービスを登録してみる。
systemdを利用してserviceが登録できていれば、 systemctl restart service-name.sercice でできる。
それを反映したらこんな感じ。
code:gitlab-ci.yml
image: golang:1.16
variables:
TZ: Asia/Tokyo
stages:
- test_stage
- deploy_stage
build:
stage: test_stage
tags:
- gitlab-org
script:
- go build ./...
artifacts:
paths:
- ./ci-cd
expire_in: 1 days
test:
stage: test_stage
tags:
- gitlab-org
script:
- go test ./...
deploy:
stage: deploy_stage
tags:
- deploy
needs:
- build
script:
- cp ./ci-cd /root/ci-cd
- systemctl restart ci-cd.service
ここまでの違いは、deployのscriptが所定のディレクトリへのコピーとサービスの再実行になっていること。
同時にWebサーバを立てるようにしたけど、期待通り本番サーバでWebサーバが立ち上がった。
これで、deployと同時にサービスの再実行を行ない、即時反映することができた。
systemdがあるような環境での実行ユーザーの変更
dockerでやる場合はsystemdは動かさないことが多いと思うけど、
本番環境はawsとかgcpとかIaaSを使うことも少なくないと思います。
そんなときはsystemdが動いているので、gitlab-runnerの設定も少し変わります。
gitlab-runnerでregisterで対応できないものとして、実行ユーザーの指定があります。
dockerの場合は gitlab-runner run のオプションでコントロールできますが、
systemdで動いている場合はserviceのユニットファイル自体を編集する必要があります。
まずユニットファイルは /etc/systemd/system/gitlab-runner.service です。
上記のファイルをviなりで開いて、 ExecStart の設定からgitlab-runnerのユーザー指定を消します。
もしroot以外から実行したいなら、ユーザーオプションで設定します。
ユニットファイルの書き換えが終われば、念のために systemctl daemon-reload でリロードし、
systemctl restart gitlab-runner.service します。
これでアクティブにできていれば、あとはpsなどでユーザの指定が設定どおりになっているかを確認するだけです。
GitLabから手動でデプロイ・リリース
gitlab-ciでジョブを手動実行する方法はおおきく2つあります。
実行済みのジョブを再実行する
実行タイミングを手動に設定し、手動で実行する
前者は一度実行されたものをもう一回実行するのに対し、
後者は前のジョブが終わっても開始させず、gitlabのweb guiから操作するという方法です。
常にすぐに動いていいなら前者、常にコントロールしたいなら後者。
手動で実行するということに焦点を当てているなら後者が良いと思います。
設定方法は when:manual です。
実際に動かして試したわけじゃないのでサンプルコードはないですが、
gitlab ci when manual とかでぐぐってもらえれば目的の情報はすぐにみつかるはずです。
そのほかの設定
そのほかにも、特定のブランチの場合にのみ動くようにするとか、
特定のリリースタグがついたときに動くようにするとかあると思います。
それらも設定次第では可能なので、組織のgitフローに沿った方法で実現するのが良いと思います。
#2021/05/23週
更新履歴
#2021/06/07 まとめおわり
#2021/06/04 systemdを使ってdeoloyと同時にサービスの再起動をする方法を追加
#2021/06/01 deploy runnerでユーザを指定し、任意のプログラムを実行するところまで追加
#2021/05/29 目的のジョブを特定のrunnerに実行させる手順を追加
#2021/05/28 runnerのセットアップを追加
#2021/05/27 書き始め