GitLab CI/CDによるDocker環境へのパイプライン考察

この記事は[富士通システムズウェブテクノロジー Advent Calendar](https://qiita.com/advent-calendar/2019/fsweb)の20日目の記事です。
本記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません。

# はじめに
[GitLab CI/CD](https://docs.gitlab.com/ee/ci/) 便利ですよね。Gitリポジトリへのコミット等をトリガーに自動処理を実行できます。
自由度が高いので何でもできるのですが、逆に自由すぎて迷うことも多々あります。
本記事では、Docker環境を活用したCI/CDの型について模索してみたいと思います。

この延長で、社員が気軽にアイデアを試せる下記のような環境を作ることを目指しています。

- Webサービスを動かすマシンを自分で用意しなくていい
- サーバOSにログインしてインストール・セットアップ作業をしなくていい
- GitリポジトリにコードをコミットするだけでWebサービスが動く

※ちなみに筆者はKubernetesについては実践経験がありません。スモールスタート主義なので、Dockerだけだと無理だ・・・と実感してから検討したい派。実はまだDocker Composeも使っていません。が、Docker Composeはさすがに使いたくなってきたところ・・・。

# 環境
本記事の試行錯誤の環境は下記のとおりです。

- GitLab環境
    - GitLab.com
- ビルドサーバ (CPU: 2core, MEM: 4GB)
    - CentOS 7.7
    - Docker CE 19.03.5
    - GitLab Runner 12.5.0
- デプロイ先サーバ (CPU: 2core, MEM: 4GB)
    - CentOS 7.7
    - Docker CE 19.03.5

※実際には社内ネットワークからのインターネットアクセスには認証プロキシを突破するための設定が必要ですが、本記事では本質ではないため割愛しています。

# CI/CDパイプラインの設計
Dockerのポータビリティを活かしたい、言い換えると、OSに直接何かをインストールすることを極力避けたいです。

- Webサービスの構成要素はすべてDockerコンテナにする
- アプリケーションのビルド環境にもDockerコンテナを使う

この路線でCI/CDパイプラインを設計すると下記のようになります。

- ビルド用コンテナをビルドする
- ↑のビルド用コンテナを使ってアプリケーションをビルドし、Webサービスコンテナにする
- Webサービスコンテナを別サーバであるデプロイ先サーバ上に起動する

# 0.ビルド用コンテナをビルドする準備
まずはビルドサーバを構築します。
CentOSにDockerをインストールする手順は[Docker本家](https://docs.docker.com/install/linux/docker-ce/centos/)に詳しく書かれているので割愛します。
また、GitLab Runnerをインストールする手順についても[GitLab本家](https://docs.gitlab.com/runner/install/linux-repository.html)をご参照ください。

ここではDockerコンテナを使って自動ビルドできるようにしたいため、Docker ExecutorモードでGitLab Runnerをregisterします。（やはり[本家の手順書](https://docs.gitlab.com/runner/register)が参考になります。）

ただ、ちょっと待てよ。**ビルド用コンテナをビルドするためのコンテナは何にすればいいのか？**
そもそも、**Dockerコンテナの中でDockerイメージをビルドできるのか？**など疑問が次々と出てきます。

それで色々と勉強したのですが、主に下記記事はたいへん参考にさせていただきました。

- [コンテナからコンテナを操作する](https://blog.nijohando.jp/post/docker-in-docker-docker-outside-of-docker/)
- [Dockerコンテナ内からDockerを使うことについて](https://qiita.com/esaka/items/37e5d60d66c6061c19d0)

まとめると、Dockerコンテナの中でDockerイメージをビルドする方法は下記の通り:

- Docker in Docker、Docker outside of Docker、Daemon-less Image Builder の3方式がある
- Docker in Docker(DinD)は権限が強すぎて危なそう
- Daemon-less Image Builderはまだあんまり事例がない
- CIで使う時はDocker outside of Docker(DooD)でいいんじゃないか？という情報が多い

ということで素直にDocker outside of Docker(DooD)方式を採用することにします。

Docker outside of Docker(DooD)をするには具体的に何をすればいいのでしょうか？
どうやらDocker Hubに`docker`という名前のコンテナがあるらしく、このコンテナはDockerの中でDockerを使うためのコンテナのようです(DinD、DooD兼用)。素晴らしい！（素晴らしいが名前が極めて紛らわしい！）
この`docker`コンテナを使った上で、「コンテナ側からホストのdocker.sock (/var/run/docker.sock)をマウント」すればいいらしいです。（この仕組みについては、まだ人様に説明できるほど理解できていません。）

GitLab Runnerのregisterは下記の手順で実施しました

```shell
# gitlab-runner register -n \
--url <GitLab URL> \
--registration-token <token> \
--executor docker \
--description <サーバ名等> \
--tag-list "docker-build" \
--docker-image "docker:latest" \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock
```

- 4行目 `--executor docker` はDocker Executorとしてregisterするという意味
- 6行目 `--tag-list "docker-build"` は`docker-build`というタグ名で登録という意味
- 7行目 `--docker-image "docker:latest"` がDockerの中でDockerを使うためのコンテナを指定
- 8行目 `--docker-volumes /var/run/docker.sock:/var/run/docker.sock` がコンテナ側からホストのdocker.sockマウント

# 1.ビルド用コンテナをビルドする
Docker outside of Docker(DooD)の環境ができたので、早速ビルド用のコンテナを作ってみることにします。
言語は何でもいいのですが、私が一番馴染みがあるJavaをGradleでビルドするコンテナを作ります。

GitLabに新規プロジェクトを作成します。ここでは名前を「gradle-container」とします。
このプロジェクトには下記2ファイルを配置してコミットします。

```Dockerfile:Dockerfile
FROM gradle:6.0.1-jdk13
```

```yml:.gitlab-ci.yml
stages:
    - package

package:
    stage: package
    tags:
        - docker-build
    script:
        - docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
        - docker build -t ${CI_REGISTRY_IMAGE} .
        - docker push ${CI_REGISTRY_IMAGE}
```

- `tags`に指定している`docker-build`は、register時に指定したタグ名

これらのファイルをコミットすると自動ビルドが実行され、見事ビルド用コンテナができあがります。
※この例は、Docker Hubに置いてあるgradleコンテナを持ってきて、そのままGitLab Registryに格納しているだけですが、ビルド環境にカスタマイズを入れたい場合にこのDockerfileを修正するだけなのでCI/CDの型としては重宝します。

# 2.ビルド用コンテナを使ってアプリケーションをビルドし、Webサービスコンテナにする
上記で作成したビルド用コンテナを使って、次にアプリケーションをビルドします。

GitLabに新規プロジェクトを作成します。ここでは名前を「sampleapp」とします。
自動ビルドを制御する下記2ファイルを作成してコミットします。
（アプリケーションのソースコードは割愛します。素のServlet/JSPを想定。）

```.gitlab-ci.yml
stages:
  - build

build:
  stage: build
  image: <ビルド用コンテナのURL>:latest
  tags:
      - docker-build
  script:
      - gradle war
  artifacts:
      paths:
          - build/libs/*.war
```

```gradle:build.gradle
apply plugin: 'eclipse-wtp'
apply plugin: 'war'

repositories {
    jcenter()
}

dependencies {
    providedCompile 'javax.servlet:javax.servlet-api:3.1.0' 
}

sourceCompatibility = '1.8'
targetCompatibility = '1.8'
```

- `build`ジョブの`image`にビルド用コンテナを指定しています

これで、自分で作ったビルド用コンテナを使ってアプリケーションのビルドに成功しました。
続いて、このアプリケーションをWebサービスコンテナにしましょう。

同じGitLabプロジェクトに下記を追加します。

```yml:gitlab-ci.yml
stages:
    - build
    - package

build:
    <省略(上記参照)>

package:
    stage: package
    tags:
        - docker-build
    script:
        - docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
        - docker build -t ${CI_REGISTRY_IMAGE} .
        - docker push ${CI_REGISTRY_IMAGE}
    dependencies:
        - build
```

```Dockerfile:Dockerfile
FROM tomcat

COPY build/libs/*.war /usr/local/tomcat/webapps/
```

- アプリケーションをWebサービスとして動かすためにTomcatを使います。Dockerfileでは`tomcat`コンテナをベースに、ひとつ前のジョブで作成したアプリケーションのWARファイルを同梱しています。

自分で作ったビルド用コンテナを使ってアプリケーションをビルドし、それをWebサービスコンテナにすることに成功しました。

# 3.Webサービスコンテナを別サーバであるデプロイ先サーバ上に起動する
最後に上記で作成したWebサービスコンテナをデプロイします。

さて、GitLab CI/CDを使ってデプロイ先サーバ上でWebサービスコンテナを起動するにはどうすればいいでしょうか？
デプロイ先サーバにGitLab Runnerを入れてしまうことを考えましたが、アプリケーションの実行環境にはあまり入れたくありません。
軽く調べた結果、GitLab Runnerに[SSH Executor](https://docs.gitlab.com/runner/executors/ssh.html)というモードがあります。これを使えば、ビルドサーバ上から別サーバを操作できそうです。接続のための情報もビルドサーバに保存されるので、GitLab上にパスワードを設定するよりは安全なのではないかと。

GitLab Runner SSH Executorのregister手順は割愛しますが、対話型で接続先のサーバのIPアドレス、ユーザ、パスワードまたは鍵ファイルを指定するだけです。タグ名は`docker-ssh`にしたと仮定します。

さて、実際にデプロイを行うスクリプトを書いていきましょう。
※デプロイと言っても、ここでは簡単にdocker runで起動する程度に留めます。

GitLabプロジェクトを新規作成します。ここではプロジェクト名は「sampleapp-deploy」とします。

```yml:.gitlab-ci.yml
stages:
    - deploy

deploy_review_app:
    tags:
        - docker-ssh
    stage: deploy
    variables:
        GIT_STRATEGY: none
    script:
        - sudo docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
        - sudo docker pull <sampleappコンテナのURL>:latest
        - sudo docker rm -f sampleapp
        - sudo docker run --name sampleapp -d -p 8080:8080 <sampleappコンテナのURL>:latest
    environment:
        name: review
        url: http://xxx.xxx.xxx.xxx:8080/
        on_stop: stop_review_app
    when: manual

stop_review_app:
    tags:
        - docker-ssh
    stage: deploy
    variables:
        GIT_STRATEGY: none
    script:
        - sudo docker stop sampleapp
    when: manual
    environment:
        name: review
        action: stop

    environment:
        name: review
        action: stop
```

- デプロイと停止はマニュアルジョブとして定義しました。
- [GitLab CI/CDのEnvironment機能](https://docs.gitlab.com/ee/ci/environments.html)を活用して、デプロイと停止が対になるように設定しています。
- デプロイした際に、GitLabのUIからアプリの画面を直接起動できるURLを設定しています。
- ※初回実行に失敗するバグがあります（これがDocker Composeを使いたい理由の第一位）

# パイプライン考察
GitLab CI/CDによるDocker環境へのパイプラインの型をまとめると下図の通りです。

![20191219dockerpipeline.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526269/72032352-38f2-936f-dddf-411b99c760ac.png)


- ソースコードをビルドしてコンテナにするまでをビルドプロジェクト(CI)、デプロイ先サーバにデプロイするのはデプロイプロジェクト(CD)と、プロジェクト分割している。実際は複数モジュールを組み合わせてデプロイすることや、複数環境にデプロイすることなどを考慮しなければならない。その時の柔軟性を考慮すると、この分割の仕方が良いと思っている。
- 上記を実現するためには、ビルドプロジェクトの成果物には環境依存の情報を含めてはいけない。Dockerコンテナ起動時に環境変数として渡すか、マウント先のファイルで制御できるようにする。
- SSH Executorがベストソリューションなのか自信がない。Docker Executor上でAnsibleによる制御の方が良い気もしているが、その場合、サーバへの接続情報はGitLabのSECRET VARIABLES等を使うのだろうか？
- 今後の検討事項
  - Webサービスを複数コンテナ構成にする(アプリケーションとデータベース等)
  - 検証環境と本番環境といった複数デプロイ環境に対応する。いや、ブランチごとに別環境としてデプロイする場合にはどうすればいいか？
  - デプロイパイプラインを特定の人しか実行できないようにするにはどうすればいいか？
  - 監視、ログなどを共通的に実現できないか？
  - 環境を汚さないのはいいけど遅い！

こんな感じでやりたいことを消化していくと、いつかKubernetesが欲しいと思うようになる日が来るのだろうか？

# おわりに
この半年間、少しずつ時間をかけてGitLab CI/CDとDockerを実際に使ってみて、環境を汚さないって素晴らしい！すべてをコードで記述するって素晴らしい！と感動しています。
これから、実際のプロジェクトでこういった技術を使う機会が増えてくればいいなぁ。