Gitlab(Runner、Container Registry)＋AnsibleでローカルにCI環境を作ってみる

# 概要

Gitlab、Gitlab Runner、Gitlab Container Registry、AnsibleでCI環境をローカルに作ってみます
上記のすべての環境はDockerコンテナで扱っていきますので、予めDockerの導入をしてください
なお、デプロイはGitlab Runner以外でも利用できるAnsibleを採用しています

当記事でCIの一通りの流れを体験できれば幸いです
コードの解説は最後にまとめてやります

参考：[Ansible勉強＆実践結果（随時更新）](https://qiita.com/comefigo/items/d749001da5637e09cb1b)

# 実行環境

Docker for Windows 18.06.0-ce

# 大まかな全体の流れ

1. Gitlabプロジェクトを作成する
2. GitlabとGitlab Runnerの連携設定
3. サンプルアプリケーションのリポジトリルートに「**.gitlab-ci.yml**」ファイルを作成し、Gitlabにプッシュする
4. Gitlab Runnerで.gitlab-ci.ymlに定義されたジョブの実行条件（ブランチ名、タグ名）でタスクが実行される  
    ※.gitlab-ci.ymlのリファレンスは以下を参照ください
     - [公式ドキュメント](http://localhost/help/ci/yaml/README.md)
     - [.gitlab-ci.yml によるジョブの設定方法](https://qiita.com/ynott/items/1ff698868ef85e50f5a1)
5. Gitlab Runnerでビルドされたアプリケーションイメージ（Node.jsのアプリ）をDocker RegistryにPushする
6. Ansible経由でRegistryからアプリケーションイメージを取得し、デプロイする

※gitlabユーザはすべてrootユーザで実行します

![structure.png](https://qiita-image-store.s3.amazonaws.com/0/30522/ef76a141-868f-3924-6a62-46f7a64c70c0.png)


# 環境作成

環境作成はこちらのソース（[github.com/comefigo/docker-gitlab-ci](https://github.com/comefigo/docker-gitlab-ci)）で作成します
以下のコンテナが立ち上がります

- Gitlab
- Gitlab Runner（Ansibleやpipなどが導入されています）
- Docker Registry（Docker Image Private Registry）
- Docker Registry frontend（Docker RegistryをGUIで参照するWebアプリケーション）

```shell
> docker-compose up -d

Creating gitlab-registry ... done
Creating gitlab          ... done
Creating gitlab-runner      ... done
Creating gitlab-registry-ui ... done
```

## Gitlab

1. rootアカウントのパスワードを設定
2. デフォルト管理者ユーザ「root」と1.で作成したパスワードでログインできることを確認
3. [ssh鍵登録](http://localhost/profile/keys)でソースをプッシュできるように公開鍵を登録  
    鍵の生成手順は[こちら](https://docs.gitlab.com/ce/ssh/README.html#generating-a-new-ssh-key-pair)を参照してください

## Gitlab Runner

runnerでDockerイメージをビルドするため、`docker.io`をインストールしています
ホストのdockerソケット（`/var/run/docker.sock:/var/run/docker.sock:ro`）を読み取り専用でマウントし、runnerコンテナからホストのdockerソケットを通してホストのDockerを操作する
もし、ホストのDockerと分離したい場合は、docker in dockerを利用してください（後述のDocker Runnerの設定で設定変更が必要になります）

参考：[Docker for Windowsでコンテナ内でDocker（dind）を使う](https://qiita.com/comefigo/items/6394a43b3bd97cde7b17)

```dockerfile:dockerfile
FROM gitlab/gitlab-runner:latest

# update libs
RUN apt-get update \
    && apt-get -y upgrade \
    && apt-get install sudo

# add sudo user
RUN echo '%gitlab-runner ALL=(ALL) NOPASSWD: ALL\n' > /etc/sudoers.d/gitlab-runner

# install python libs
RUN apt-get install -y python3 build-essential python-dev docker.io

# install pip
RUN curl -kL "https://bootstrap.pypa.io/get-pip.py" | python

# install aws cli
RUN pip install awscli ansible boto boto3 docker-py
```

```yaml:docker-compose.yml
～省略～
  gitlab-runner:
    build: 
      context: ./runner
      dockerfile: Dockerfile
    image: gitlab-runner
    container_name: gitlab-runner
    hostname: gitlab-runner
    restart: always
    volumes:
      - gitlab-runner-conf:/etc/gitlab-runner
      - /var/run/docker.sock:/var/run/docker.sock:ro
    env_file:
      - envs/common.env
      - envs/registry.env
    depends_on:
      - gitlab

```

## Container Registry

Container Registryを有効化するためにGitlabの設定ファイル([/etc/gitlab/gitlab.rb](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template))の以下の項目を有効化しています
これらの設定項目は環境変数`GITLAB_OMNIBUS_CONFIG`を通じて直接設定できるので、gitlab.rbを直接編集する必要はない
なお、`registry_host`はgitlab-registry（コンテナ名）にしていますが、このサンプル環境ではすべてのコンテナが同一ネットワーク内にあるため、ホスト名=コンテナ名にしています。ただし、Dockerホスト側からは見えないので、後述ではhostsに登録しています

SSL通信など詳しい設定方法は[こちら](https://docs.gitlab.com/ee/administration/container_registry.html)を参照してください

```yaml:docker-compose.yml
～省略～
  gitlab:
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        gitlab_rails['registry_enabled'] = true
        gitlab_rails['registry_host'] = "gitlab-registry"
        gitlab_rails['registry_port'] = "5000"
        gitlab_rails['registry_api_url'] = "http://gitlab-registry:5000"
```

### 動作確認

1. Gitlabで`busybox`という名のプロジェクトを作成してください
2. 「Container Registry」が有効になっていることを確認
![busybox_project.png](https://qiita-image-store.s3.amazonaws.com/0/30522/5ced231b-8d41-62bf-a3b4-a0f7bead8917.png)
3. Dockerのホストマシンでホスト名を追加  
Registryのホスト名（gitlab-registry）をDNSに登録していないため、ホストマシンから見た場合はlocalhostになるので、ホスト名gitlab-registryを逆引きできない。なのでホストマシンのhostsファイルに以下のように追加します

    ```conf:hosts
    127.0.0.1 gitlab-registry
    ```

4. Dockerイメージの取得
テスト用に軽量のbusyboxイメージを使用します

    ```shell
    > docker pull busybox:latest
    ```
5. イメージのタグ付け
docker tag <イメージ名>:<イメージタグ> <レジストリ名>:<ポート番号>/\<gitlabユーザ名 or グループ名>/<プロジェクト名>:<イメージタグ>

    ```shell
    > docker tag busybox:latest gitlab-registry:5000/root/busybox:latest
    ```
6. RegistryにPush
Registryに認証を追加していないため、docker loginは不要   

    ```shell
    > docker push gitlab-registry:5000/root/busybox:latest
      The push refers to repository [gitlab-registry:5000/root/busybox]
      f9d9e4e6e2f0: Pushed
      latest: digest: sha256:5e8e0509e829bb8f990249135a36e81a3ecbe94294e7a185cc14616e5fad96bd size: 527
    ```
7. 確認
PushしたDockerイメージが表示されていることを確認
![busybox_regisrty.png](https://qiita-image-store.s3.amazonaws.com/0/30522/b1ed6c7d-9594-b5cd-e264-2984de78f40c.png)

# CI体験

## 1. プロジェクトを作成  
「myapp」という名のプロジェクトを作成してください

## 2. Gitlab RunnerとGitlabの連携

gitlab-runnerの登録の詳細は[こちら](https://docs.gitlab.com/runner/register/)を参照してください

1. Runnerの登録

    ```shell
    > docker exec -it gitlab-runner gitlab-runner register
    ```
2. Gitlab URLの入力
URLはgitlabのコンテナ名を使用する

    ```shell
    Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com )
    > http://gitlab/
    ```
3. Tokenの登録
「myappプロジェクト」-「Settings」-「CI/CD」-「Runners」-「Specific Runners」の「Use the following registration token during setup:」のTokenを以下に入力

    ```shell
    Please enter the gitlab-ci token for this runner:
    > ********
    ```
![gitlab-runner-token.png](https://qiita-image-store.s3.amazonaws.com/0/30522/2f298097-deff-c967-3608-fb46a8f72b99.png)

4. Runnerの説明 
Runnerの用途がわかるような説明を入力

    ```shell
    Please enter the gitlab-ci description for this runner:
    > gitlab ci demo
    ```
5. タグ付け
Runnerのタグによって、後述の.gitlab-ci.ymlでタスクの実行を振り分けることができる
例えば、ソースコードビルド用、Dockerイメージのビルド用、デプロイ用にそれぞれタグ異なるタグを付与し、実行先を分ける

    ```shell
    Please enter the gitlab-ci tags for this runner (comma separated):
    > ci
    ```
6. 実行モードの選択

    ```
    Please enter the executor: docker+machine, docker-ssh+machine, docker, docker-ssh, shell, ssh, parallels, virtualbox, kubernetes:
    > shell
    ```
7. 登録確認
**actived**のRunnerが登録されていることを確認

![gitlab-runner-active.png](https://qiita-image-store.s3.amazonaws.com/0/30522/497aac19-0e6b-8455-1afb-1b577d3005a9.png)

## 3. ビルド用の変数を設定
gitlab-ciのタスク実行時に外部変数として渡すことができます
これを用いることで秘密鍵なども安全に渡すことができる
今回はアプリケーションのバージョンを渡せるようにキー：`APP_VERSION`、値：`1.0.0`を作成します
![gitlab_project_variables.png](https://qiita-image-store.s3.amazonaws.com/0/30522/cd450cb4-3418-3f64-a166-d8db3a5dff4f.png)

## 4. サンプルソースをプロジェクトにPush
サンプルソース「[docker-gitlab-sample-app](https://github.com/comefigo/docker-gitlab-sample-app)」をローカルにCloneし、gitlabのmyappにすべてのブランチ（master/dev）をPushしてください

※gitリモートのURLは「git@**localhost**:root/myapp.git」になります！
※当サンプルソースは簡単なNode.jsのWebアプリケーションになります
※サンプルソースでは**3000**と**3001**ポートを使用しますので、既に使用している場合は停止するか、.gitlab-ci.yml内の**app_port**を変更してください

## 5. CI/CDの確認

ブランチにプッシュされ次第、`.gitlab-ci.yml`に定義されたタスクが実行される
「myapp」-「CI/CD」-「Pipelines」で各ブランチのタスクが実行されることを確認
![gitlab_ci_pipeline.png](https://qiita-image-store.s3.amazonaws.com/0/30522/c488c18f-c384-ef79-ac25-f11108c2052e.png)

## 6. サンプルアプリの動作確認

CI/CDでpipelineの実行が成功したら、サンプルアプリの動作を確認しましょう
myapp-masterが本番用、myapp-devが開発用と想定
Dockerホストで以下のようにサンプルアプリが実行されていることを確認できます

```shell
> docker ps

CONTAINER ID        IMAGE                                      COMMAND                  CREATED              STATUS                    PORTS                                             NAMES
9ea9cb4af7df        gitlab-registry:5000/root/myapp:1.0.0      "/bin/sh -c 'node ap…"   About a minute ago   Up About a minute         0.0.0.0:3001->3001/tcp                            myapp-dev
59af2fe4858d        2ccdc5dd94c7                               "/bin/sh -c 'node ap…"   5 minutes ago        Up 5 minutes              0.0.0.0:3000->3000/tcp                            myapp-master
```

ブラウザで「http://localhost:3000 」、「http://localhost:3001 」をそれぞれ確認してください
![sample_app_master.png](https://qiita-image-store.s3.amazonaws.com/0/30522/7864f65d-7af9-b41b-1eef-01dcc7d895b3.png)
![sample_app_dev_1.0.0.png](https://qiita-image-store.s3.amazonaws.com/0/30522/93a0f1aa-ef5a-7ec1-1e1e-367e7b916920.png)

## 7. アプリケーションのバージョンアップ

仮にアプリケーションのバージョン1.0.1を開発すると想定しましょう
まず、前述で作成した変数「APP_VERSION」を「1.0.1」に更新してください
※「**Reveal value**」ボタンを押せば、値を更新できます

![gitlab_update_variables.png](https://qiita-image-store.s3.amazonaws.com/0/30522/e872c7ce-c1a6-b8df-6b03-3eca82015727.png)

## 8. アプリケーションの修正

サンプルソースのブランチを**dev**に切り替え、`app.js`を適当に変更して、コミットとgitlabにプッシュしてください
CI/CDが問題なく終了したら、「http://localhost:3001 」を確認してください
myapp-masterはver.1.0.0のままであることも確認してください
![sample_app_dev_1.0.1.png](https://qiita-image-store.s3.amazonaws.com/0/30522/faa5df09-ad2c-9b84-bc1b-d5a4ec8de10c.png)

## 9. 本番適用

「dev」から「master」にマージリクエストを作成し、マージしてください
CI/CDが実行され、問題なく終了したら、「http://localhost:3000 」を確認してください

※green/blueデプロイとかオシャレなことしていないので、プッツンと一回切れますので、ご了承ください

## 10. Container Registryの確認
ビルドしたAPPイメージがContainer Registryで登録されていることを確認

![gitlab_myapp_registry.png](https://qiita-image-store.s3.amazonaws.com/0/30522/0f18c5c7-d88d-1c69-b273-fb3da0aeab75.png)

# コード解説

## .gitlab-ci.yml

- stagesに定義されている順（build → deploy）に処理が実行されます
- tagsはGitlab Runnerの登録時に設定したタグを指定（指定しないと動きません）
- $APP_VERSIONは前述で設定したmyappプロジェクトの変数になります
- \$ENV_DOCKER_REGISTRY_HOSTと$ENV_DOCKER_REGISTRY_PORTはgitlab-runnerコンテナの環境変数として`registry.env`を読み込んでいます
- ブランチごとにタスクを分離させていて、Ansibleのタスク実行時にAnsible内の変数を上書きするように`--extra-vars=`を設定しています
- デプロイ先をローカル（`-i inventories/hosts_local`）にしていますが、デプロイ先に応じてインベントリファイルを変更することもできます  
    例:  
    　devブランチのタスクに `ansible-playbook -i inventories/hosts_develop xxxxx`  
    　masterブランチのタスクに `ansible-playbook -i inventories/hosts_production xxxxx`
- dockerコマンドはsudoで実行している  
    前述でマウントしたdocker.sockにはroot権限しか操作できない(変更できない)ため、gitlab-runnerユーザが実行する際には`sudo`が必須になります

```yaml:.gitlab-ci.yml

stages:
  - build
  - deploy

# Docker イメージのビルド&レジストリに追加
build:
  stage: build
  tags:
    - ci
  script:
    - sudo docker info
    - sudo docker build -t myapp:$APP_VERSION ./
    - sudo docker tag myapp:$APP_VERSION $ENV_DOCKER_REGISTRY_HOST:$ENV_DOCKER_REGISTRY_PORT/root/myapp:$APP_VERSION
    - sudo docker push $ENV_DOCKER_REGISTRY_HOST:$ENV_DOCKER_REGISTRY_PORT/root/myapp:$APP_VERSION

# devブランチにマージされたら以下のタスクを実行する
deploy_dev:
  stage: deploy
  tags:
    - ci
  script:
    - env
    - ansible --version
    - 'cd ./ansible && ansible-playbook -i inventories/hosts_local deploy.yml --extra-vars="{\"all\": { \"app_name\": $CI_PROJECT_NAME-$CI_BUILD_REF_NAME, \"app_version\": $APP_VERSION, \"docker_register_url\": $ENV_DOCKER_REGISTRY_HOST, \"docker_register_port\": $ENV_DOCKER_REGISTRY_PORT, \"app_port\": 3001 }}"'
  only:
    - dev

# masterブランチにマージされたら以下のタスクを実行する
deploy_production:
  stage: deploy
  tags:
    - ci
  script:
    - env
    - ansible --version
    - 'cd ./ansible && ansible-playbook -i inventories/hosts_local deploy.yml --extra-vars="{\"all\": { \"app_name\": $CI_PROJECT_NAME-$CI_BUILD_REF_NAME, \"app_version\": $APP_VERSION, \"docker_register_url\": $ENV_DOCKER_REGISTRY_HOST, \"docker_register_port\": $ENV_DOCKER_REGISTRY_PORT, \"app_port\": 3000 }}"'
  only:
    - master
```
ローカル以外の環境にデプロイしたい場合は、以下のファイル(hosts_production/hosts_develop)を修正するのと.gitlab-ci.ymlのansible-playbookで指定するインベントリファイルを変更してください

```conf:ansible/inventories/hosts_production
[prd]
dev1 ansible_host=127.0.0.1

[prd:vars]
ansible_user=<SSHユーザ>
ansible_ssh_private_key_file=<SSH鍵のパス>
```

## all.yml

ansibleの変数ファイルになります
ansible-playbookコマンドで変数の上書きがなければ、ここの変数の値が使われる

```yaml:all.yml

all:
  app_port: 3000
  app_version: "0.0.1"
  app_name: myapp
  docker_register_url: "gitlab-registry"
  docker_register_port: 5000
```