どんなに小さなサイドプロジェクトでもCI/CDパイプラインは大いに役立つ

>こちらの記事は、[Luc Juggery](https://medium.com/@lucjuggery?source=post_page-----281f80f39fdf----------------------) 氏により2018年9月に公開された『 [Even the Smallest Side Project Deserves its CI/CD Pipeline](https://medium.com/better-programming/even-the-smallest-side-project-deserves-its-ci-cd-pipeline-281f80f39fdf) 』の和訳です。
本記事は原著者から許可を得た上で記事を公開しています。

![Image](https://miro.medium.com/max/1400/1*qehZ1KLTEO6y8tcCwlwWzQ.png)

# TL;DR
今を行く最新のツールを使えば、シンプルなCI/CDパイプラインを構築することは難しくありません。個人的なサイドプロジェクトであっても、CI/CDパイプラインを構築することで多くのことを学ぶことができるでしょう。Docker、GitLab、Portainerはこのようなセットアップで使うのに最適なコンポーネントです。

# サンプルプロジェクト
南フランスのソフィア・アンティポリス地域で技術イベントをよく主催する私は、今後開催されるイベントを全部知る方法はないのかとよく聞かれていました(ミートアップ、ジャグ、自治会主催のものなど)。これまでそういったイベントを全てリストアップしている所は一つも無かったので、常に最新のイベントの一覧情報を更新する https://sophia.events というシンプルなウェブページを思いつきました。このプロジェクトは[GitLab](https://gitlab.com/lucj/sophia.events)上で確認できます。

免責事項: これは単純なプロジェクトですが、プロジェクトの複雑さはここでは重要ではありません。これから説明するCI/CDパイプラインのコンポーネントは、さらに複雑なプロジェクトでも同じように使うことができますが、今回の様なマイクロサービスには特に適しています。

# コードの概要
物事をなるべくシンプルに保つため、基本的には新しいイベントが追加されるたびにそのイベントのjsonファイルが追加される様になっています。このファイルの一部は以下のスニペットの様になっています(フランス語になっている部分はごめんなさい)。

```events.json
{
  "events": [
    {
      "title": "All Day DevOps 2018",
      "desc": "We're back with 100, 30-minute practitioner-led sessions and live Q&A on Slack. Our 5 tracks include CI/CD, Cloud-Native Infrastructure, DevSecOps, Cultural Transformations, and Site Reliability Engineering. 24 hours. 112 speakers. Free online.",
      "date": "17 octobre 2018, online event",
      "ts": "20181017T000000",
      "link": "https://www.alldaydevops.com/",
      "sponsors": [{"name": "all-day-devops"}]
    },
    {
      "title": "Création d'une Blockchain d'entreprise (lab) & introduction aux smart contracts",
      "desc": "Venez avec votre laptop ! Nous vous proposons de nous rejoindre pour réaliser la création d'un premier prototype d'une Blockchain d'entreprise (Lab) et avoir une introduction aux smart contracts.",
    "ts": "20181004T181500",
    "date": "4 octobre à 18h15 au CEEI",
    "link": "https://www.meetup.com/fr-FR/IBM-Cloud-Cote-d-Azur-Meetup/events/254472667/",
    "sponsors": [{"name": "ibm"}]
    },
    …
  ]
}
```

このファイルにmustacheの[テンプレート](https://gitlab.com/lucj/sophia.events/blob/master/index.mustache)が適用され、最終的なウェブアセットが生成されます。

# Dockerマルチステージビルド
ウェブアセットを生成したら、それらはターゲットマシンにデプロイされたnginxイメージにコピーされます。

マルチステージビルドのおかげで、ビルドは以下の二段階に分かれて行われます:

- アセットの生成
- アセットを含む最終的なイメージを作成

ビルドに使うDockerfileは以下の通りです:

```Dockerfile
# Generate the assets
FROM node:8.12.0-alpine AS build
COPY . /build
WORKDIR /build
RUN npm i
RUN node clean.js
RUN ./node_modules/mustache/bin/mustache events.json index.mustache > index.html# Build the final image used to serve them
FROM nginx:1.14.0
COPY --from=build /build/*.html /usr/share/nginx/html/
COPY events.json /usr/share/nginx/html/
COPY css /usr/share/nginx/html/css
COPY js /usr/share/nginx/html/js
COPY img /usr/share/nginx/html/img
```

# ローカルテスト
サイトの生成をテストするために、レポジトリをクローンしてtest.shスクリプトを実行します。するとイメージが作成され、そこからコンテナが実行されます。

```bash
$ git clone git@gitlab.com:lucj/sophia.events.git$ cd sophia.events$ ./test.sh
Sending build context to Docker daemon  2.588MB
Step 1/12 : FROM node:8.12.0-alpine AS build
---> df48b68da02a
Step 2/12 : COPY . /build
---> f4005274aadf
Step 3/12 : WORKDIR /build
---> Running in 5222c3b6cf12
Removing intermediate container 5222c3b6cf12
---> 81947306e4af
Step 4/12 : RUN npm i
---> Running in de4e6182036b
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN www@1.0.0 No repository field.added 2 packages from 3 contributors and audited 2 packages in 1.675s
found 0 vulnerabilitiesRemoving intermediate container de4e6182036b
---> d0eb4627e01f
Step 5/12 : RUN node clean.js
---> Running in f4d3c4745901
Removing intermediate container f4d3c4745901
---> 602987ce7162
Step 6/12 : RUN ./node_modules/mustache/bin/mustache events.json index.mustache > index.html
---> Running in 05b5ebd73b89
Removing intermediate container 05b5ebd73b89
---> d982ff9cc61c
Step 7/12 : FROM nginx:1.14.0
---> 86898218889a
Step 8/12 : COPY --from=build /build/*.html /usr/share/nginx/html/
---> Using cache
---> e0c25127223f
Step 9/12 : COPY events.json /usr/share/nginx/html/
---> Using cache
---> 64e8a1c5e79d
Step 10/12 : COPY css /usr/share/nginx/html/css
---> Using cache
---> e524c31b64c2
Step 11/12 : COPY js /usr/share/nginx/html/js
---> Using cache
---> 1ef9dece9bb4
Step 12/12 : COPY img /usr/share/nginx/html/img
---> e50bf7836d2f
Successfully built e50bf7836d2f
Successfully tagged registry.gitlab.com/lucj/sophia.events:latest
=> web site available on http://localhost:32768
```
出力の最後に表示されたURLから、ウェブページにアクセスできるようになります:

![Image](https://miro.medium.com/max/1400/1*Y21lGvz9lCwXw6GAq_-1Bw.png)

# ターゲット環境
## クラウド事業者が提供する仮想マシン
お気づきの通り、このウェブサイトはクリティカルなものではなく(1日に数十件の訪問しかない)そのため、1台の仮想マシンでしか動作させていません。このサイトはヨーロッパの有名なクラウド事業者である[Exoscale](http://exoscale.ch/)上でDocker Machineを使って作成したものです。

もしExoscaleを使ってみたいのなら、私にメッセージしてもらえれば20€の割引クーポンを差し上げますよ。

# SwarmモードのDockerデーモン

上記の仮想マシン上で動作しているDockerデーモンはSwarmモードで動作するように設定されているので、スタック、サービス、設定、シークレットプリミティブとDocker Swarmの優れていて使いやすいオーケストレーション機能を使用することができます。

# Dockerスタックとして実行中のアプリケーション
以下のファイルでは、ウェブアセットを含むnginxウェブサーバを動作させるサービスを定義しています。

```docker-compose.yml
version: "3.7"
services:
  www:
    image: registry.gitlab.com/lucj/sophia.events
    networks:
      - proxy
    deploy:
      mode: replicated
      replicas: 2
      update_config:
        parallelism: 1
        delay: 10s
      restart_policy:
        condition: on-failure
networks:
  proxy:
    external: true
```

いくつかの説明:

- イメージはgitlab.comでホストされているプライベートレジストリにあります(ここではDocker Hubは関係ありません)。
- サービスは2つのレプリカを持つレプリケートモードになっており、1つのサービスに対して2つのタスク/コンテナが同時に実行されていることを意味します。VIP(仮想IPアドレス)はSwarmによってサービスに関連付けられており、そのサービスを対象とした各リクエストが2つのレプリカの間で負荷分散されるようになっています。
- 新しいバージョンのウェブサイトをデプロイし、サービスが更新されると、まず先に1つ目のレプリカが更新され、10秒後に2つ目のレプリカが更新されます。これにより、更新プロセス中もウェブサイトが利用可能な状態であることが保証されます。ロールバック戦略を使うこともできましたが、現時点では必要ありません。
- サービスは外部のプロキシネットワークに接続されているので、TLS終端はwwwサービスにリクエストを送信することができます(このTLS終端はSwarm上に配置された別のサービスで実行されていますが、このプロジェクトの外部にあります)。

スタックは下記のコマンドで実行できます:

```bash
$ docker stack deploy -c sophia.yml sophia_events
```

# Portainerを使って管理する

[Portainer](https://portainer.io)はDockerホストやDocker Swarmクラスタを簡単に管理できる優れたWeb UIです。以下は、Swarmで利用可能なスタックを一覧表示するPortainerインターフェースのスクリーンショットです。

![Image](https://miro.medium.com/max/1400/1*Q28WiD0-_8zqDv_CtWdqRA.png)

現在のセットアップでは以下の3つのスタックが表示されています:

- Portainer自身
- 今回のウェブサイトを実行しているサービスを含むsophia_events
- TLS終端であるtls

sophia_eventsスタックにあるwwwサービスの詳細をリストアップすると、Service webhookが有効になっていることがわかります。これはPortainer 1.19.2(執筆時点での最新バージョン)から利用可能な機能で、HTTP Postのエンドポイントを定義することで、サービスの更新をトリガーとして呼び出すことができます。詳細は後述しますが、GitLabランナーはこのwebhookの呼び出しを担当しています。

![Image](https://miro.medium.com/max/1400/1*Cxk54DFLdcfZjvGTDvm56w.png)

注意: スクリーンショットからわかるように、今回私はPortainer UIへlocalhost:8888からアクセスしています。Portainerのインスタンスは外部に公開したくないので、以下のコマンドで開いたsshトンネルを使ってアクセスしています。

```bash
ssh -i ~/.docker/machine/machines/labs/id_rsa -NL 8888:localhost:9000 $USER@$HOST
```

続けて、8888番ポートのローカルマシンをターゲットにしたすべてのリクエストが、sshを介して仮想マシンの9000番ポートに送信されます。9000番はPortainerが仮想マシン上で動作しているポートですが、このポートはExoscaleの設定でセキュリティグループによってブロックされているため、外部には開放されていません。

注意: 上記のコマンドでは、VMに接続するために使用するsshキーは、VM作成時にDocker Machineが生成したものを使用しています。

# GitLabランナー
GitLabランナーとは.gitlab-ci.ymlファイルで定義されたアクションを実行するプロセスのことです。このプロジェクトでは、仮想マシン上でコンテナとして動作する独自のランナーを定義しています。

まずは、いくつかのオプションをつけたランナーを登録します:

```bash
CONFIG_FOLDER=/tmp/gitlab-runner-configdocker run — rm -t -i \
-v $CONFIG_FOLDER:/etc/gitlab-runner \
gitlab/gitlab-runner register \
  --non-interactive \
  --executor "docker" \
  —-docker-image docker:stable \
  --url "https://gitlab.com/" \
  —-registration-token "$PROJECT_TOKEN" \
  —-description "Exoscale Docker Runner" \
  --tag-list "docker" \
  --run-untagged \
  —-locked="false" \
  --docker-privileged
```

これらのオプションの中にあるproject_tokenはGitLab.comのプロジェクトページから提供されているもので、外部ランナーの登録に使用するものです。

![Image](https://miro.medium.com/max/1400/1*fBHXnpbMKwM0yraHaj1-hw.png)

ランナーを登録したら以下の様に実行します:

```
CONFIG_FOLDER=/tmp/gitlab-runner-configdocker run -d \
--name gitlab-runner \
—-restart always \
-v $CONFIG_FOLDER:/etc/gitlab-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
gitlab/gitlab-runner:latest
```

登録して実行まですると、ランナーがGitLab.comのプロジェクトページに表示される様になります:

![Image](https://miro.medium.com/max/1400/1*7evU79ZJ4VWH6QFdT_KCgA.png)

このランナーは、新しいコミットがリポジトリにプッシュされるたびに与えられた作業、つまり.gitlab-ci.ymlファイルで定義されているテスト、ビルド、デプロイの各段階を順次実行します。

```.gitlab-ci.yml
variables:
  CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
  DOCKER_HOST: tcp://docker:2375
stages:
  - test
  - build
  - deploy
test:
  stage: test
  image: node:8.12.0-alpine
  script:
    - npm i
    - npm test
build:
  stage: build
  image: docker:stable
  services:
    - docker:dind
  script:
    - docker image build -t $CONTAINER_IMAGE:$CI_BUILD_REF -t $CONTAINER_IMAGE:latest .
    - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
    - docker image push $CONTAINER_IMAGE:latest
    - docker image push $CONTAINER_IMAGE:$CI_BUILD_REF
  only:
    - master
deploy:
  stage: deploy
  image: alpine
  script:
    - apk add --update curl
    - curl -XPOST $WWW_WEBHOOK
  only:
    - master
```

- テストの段階では、event.jsonファイルのフォーマットが正しいかどうか、イメージが欠落していないかどうかなどの事前チェックを行います。
- ビルドの段階ではイメージをビルドし、GitLabレジストリにプッシュします。
- デプロイの段階では、Portainerに送られたWebhookを使ってサービスの更新をトリガーします。WWW_WEBHOOK変数は、GitLab.comのプロジェクトページのCI/CD設定で定義しています。

![Image](https://miro.medium.com/max/1400/1*fpPmRJeAqtR6WWD_nc3xJQ.png)

**注意事項:**
このランナーはSwarm上のコンテナで実行されており、共有ランナーというGitLab上でホストされている他のプロジェクトと共有する公開ランナーを使うこともできましたが、今回はランナーが(Webhookを送信するために)Portainerエンドポイントにアクセスする必要があり、Portainerには外部からアクセスされたくはなかったため、クラスター内でランナーを動かす方が安全だと判断しました。

また、ランナーはコンテナ内で動作しているため、ホスト上で公開されている9000番ポートを介してPortainerと連絡を取るために、Docker0ブリッジネットワークのIPアドレスに対してWebhookを送信しています。そのため、Webhookは以下のような形式になっています。

http://172.17.0.1:9000/api[…]a7-4af2-a95b-b748d92f1b3b

# デプロイの流れ

新しいバージョンのサイトの更新は、以下のような流れで行われます:

![Image](https://miro.medium.com/max/1400/1*JrtraJp9Md7_HvPlEGiWWw.png)

1. ある開発者がGitLabにいくつかの変更をプッシュしたとします。この変更には、基本的には1つかいくつかの新しいイベントとイベントのスポンサーのロゴがevents.jsonファイルに含まれています。
2. GitLabランナーが.gitlab-ci.ymlで定義されているアクションを実行します。
3. GitLabランナーがPortainerで定義されたWebhookを呼び出します。
4. Webhookを受信すると、Portainerは新しいバージョンのwwwサービスをデプロイします。これはDocker Swarm APIを呼び出して行います。起動時にソケット/var/run/docker.sockがバインドマウントされているので、PortainerはAPIにアクセスできます。
このUnixソケットの使い方について詳しく知りたい場合は、[私の以前の記事](https://medium.com/better-programming/about-var-run-docker-sock-3bfd276e12fd)を参照してください。
5. これでユーザーは新しいバージョンのウェブサイトを閲覧できる様になります。

# 例
コードの一部を変更して、その変更をコミット/プッシュしてみましょう。

```bash
$ git commit -m 'Fix image'
$ git push origin master
```
下のスクリーンショットは、GitLab.comのプロジェクトページ内のコミットによってトリガーされたパイプラインを示しています。

![Image](https://miro.medium.com/max/1400/1*JeVsifm36zx_ZfjXTqHzQQ.png)

Portainer側では、Webhookを受信してサービスの更新が行われました。ここでははっきりとは確認できませんが、1つ目のレプリカが更新されている間、2つ目のレプリカから引き続きWebサイトにアクセスできる状態になっています。そして数秒後に2つ目のレプリカも同じ様に更新されます。

![Image](https://miro.medium.com/max/1400/1*joNxer_6SRvyCLj8ACnmLA.png)

# まとめ
このような小さなプロジェクトでも、CI/CDパイプラインの設定を経て、(ずっと前から私のTo Learnリストに載っていた)GitLabに慣れるための良い練習になりました。GitLabは優れたプロフェッショナル製品です。また、Portainerの最新バージョン(1.19.2)で利用できるようになった待望のWebhook機能で遊ぶとても良い機会にもなりました。今回のようなサイドプロジェクトに、Docker Swarmはとても簡単に使えて、間違いなくぴったりな選択肢だったと思います！

# 翻訳協力
Original Author:  **[Luc Juggery](https://medium.com/@lucjuggery?source=post_page-----281f80f39fdf----------------------)** 
Thank you for letting us share your knowledge! 

この記事は以下の方々のご協力により公開する事が出来ました。
改めて感謝致します。
選定担当: @upaldus
翻訳担当: @upaldus 
監査担当: takujio
公開担当: @r_pg10 

# ご意見・ご感想をお待ちしております
今回の記事は、いかがだったでしょうか？
・こうしたら良かった、もっとこうして欲しい、こうした方が良いのではないか
・こういったところが良かった
などなど、率直なご意見を募集しております。
いただいたお声は、今後の記事の質向上に役立たせていただきますので、お気軽にコメント欄にてご投稿ください。[Twitter](https://twitter.com/BabyDegu?lang=ja)でもご意見を受け付けております。
みなさまのメッセージをお待ちしております。