はじめに
この記事は GCP と drone を利用した CI/CD 環境構築を通して、インフラの知識を身につけるためのチュートリアル記事です。
CI/CD 環境を構築する作業を通して、インフラ未経験 1だった筆者は幅広い知識を得ることができました。これはとても良い経験でしたので、インフラの知識や単語をキャッチアップしながら CI/CD 環境を構築する記事としてまとめました。
誰でも簡単に構築できる CI/CD
最近、様々な CI/CD サービスが台頭しています。それらを利用することで、「Github で認証すればすぐパイプラインができる」といった世界になりつつあり、誰でも CI/CD 環境が手軽に構築できるようになりました。
たとえば、CircleCI では Github で認証し、 簡単な設定ファイルを書けば PR ベースでの CI/CD パイプラインを構築できます。JavaScript に限ればNow や Netlify は Github 認証さえすれば 設定ファイルすら必要なく デプロイ環境を構築できます。さらには Github Actionsといった Github 公式の CI/CD サービスまで誕生しており、私たちを取り巻く CI/CD 環境は日に日に便利になっています。
それ故、CI/CD 環境の構築はインフラの知識が無くても簡単に作れてしまうほどに、敷居がかなり下がってきました。ただし、 Github が使えれば の話です。
苦しんで取り組む CI/CD 環境構築
そんなある日、筆者は Github が無い状態で CI/CD 環境を作ることになりました。日頃 Netlify に甘えている筆者は、真っ白な土地の上に CI/CD 環境を作るに当たって、これまでの勉強不足のツケを払うかのごとく苦しむことになりました。構築手順を調べるも、コンテナ・DNS・ネットワーク・認証などといった普段触ったことのない概念にたくさん出会い、「何も分からない上に何が分からないのかも分からない」といった状態でした。
CI/CD 構築作業の経験値効率は高い
そんな状態でしたが、なんとか友人に助けてもらい無事に環境を構築できました。そのとき私は、 "CI/CD の構築は、インフラ未経験の人間が知識をキャッチアップするのに最適なイベントなのでは" と思いました。
なぜなら様々な分野の最低限の知識が求められるからです。 インフラは動く or 動かないです。 最低限の知識が 1 つでも抜けていたら動かない世界です。これをやりとげるためには動かし方を細かいところまで調べる必要があります。**つまり CI/CD 構築という作業をやりきったときの経験値はとても大きいものとなります。**この記事ではそのときに経験したことをチュートリアル形式でまとめ、これからインフラを学ぼうとする人の一助となることを願っています。
これから作る CI/CD の構成図
この記事では GCP スタック上に、CI/CD サーバーと成果物をホスティングします。
GAE 上に Gatsby 製の静的ページをアップロードできればゴールです。CI/CD には drone を使い、drone は GCE 上で動します。
構成/環境の説明
Github を 使えない環境で CI/CD を立てたときの経験をベースに記事を書いていますが、自分の手元にオンプレで運用している Git レポジトリはありません。そのため説明の便宜上 Github を使います。同様の設定は オンプレ版 GHE でも可能です。
drone
drone は Go 製の CI/CD ツールです。大きな特徴として、drone はコンテナベースのツールで、ビルドのステップごとに環境を Docker で作ります。droner の実行モデルは server と runner に分かれ、server は CI/CD の中央管理、runner は実際にタスクを実行します。drone は DB で 値や状態を保存するため、DB の設定が必要になります。しかし、公式がその辺の設定を行った Docker イメージ を配布しているので、それを利用するだけで CI/CD 環境が立ち上がります。
GCE
GCE は VM インスタンスです。AWS でいう EC2 です。drone は runner と server があるので 2 つ借ります。このインスタンスの中でコンテナ動かすので、最初から docker コマンドが使える Container-Optimized OS を利用します。これは起動時にイメージを選択することで、そのコンテナを立ち上げることができます。
GAE
GAE はフルマネージドなサーバレスプラットフォームです。フルマネージドであるがゆえ、初めから様々な最適化がほどこされており、ここにデプロイするだけでハイパフォーマンスな環境が手に入ります。デプロイも設定ファイルを 1 つ書いてコマンドを一発叩くだけです。
GCS
GCSは GCP が提供するオブジェクトストレージです。AWSでいうS3に相当します。今回の開発では直接的には利用しませんが、GAE はソースコードを GCS に置くため、意図せず利用します。デプロイ時のサービスアカウントはこの GCS にも権限を割り振らないといけないので覚えておきましょう。
Service Account
Service Accountは、アプリケーションやVMインスタンスに属している特別なアカウントです。ローカルで叩く GCP コマンドは、事前に認証しているため使えます。そのため drone のような環境からは GCP のコマンドを叩くことはできません。そのため各サービス内からコマンドを使える仕組みとして Service Account が用意されています。
Gatsby
CI/CD には関係しませんが、利用するツールなので説明します。Gatsby は JS の静的サイトジェネレーターです。このツールで吐き出される Web ページは、様々な最適化が自動で行われています。どのような最適化があるかは Web Performance 101—also, why is Gatsby so fast?を参照ください。この記事によると、Gatsby は、web サイトを爆速サイトに変換する compiler と見なせます2 。また、成果物はただの静的ページなので、環境に依存することなくどこにでもデプロイできます。
チュートリアル
それでは早速、構築していきましょう。まず GCP のプロジェクトを作成してください。こちらの記事などが参考になります。GCP ではサービスや環境はプロジェクトという単位にまとめられるため、この作業は必須です。
GCE を立てる
それでは早速 drone を動かす環境を作っていきましょう。drone の構築には VM インスタンスである GCE を使います。drone には server と runner があるので、2 つ借ります。
OS を選択する
GCE の OS を指定しましょう。ここでは、 Container-Optimized OS を選択します。
drone は Docker コンテナの上で動作します。普通の OS を利用すると docker コマンドを使えるように作業が必要ですが、Container-Optimized OS を利用すると 初めからdocker コマンドを使える環境が手に入ります。
イメージの選択
Container-Optimized OS を選択すると、どの Docker イメージを使うかを入力するフォームが現れます。drone server 用の GCE には, drone/drone:1を、drone runner 用の GCE にはdrone/agent:1を指定してください。これはDocker Hubを指定しています。
イメージ名の末尾の :1
はバージョンです。もし仮に バージョンを指定しなければ drone の v2 が出ると勝手に切り替わり、再起動時に環境が壊れる可能性もあるため、バージョンは明示的に指定しましょう。
再起動ポリシーは「権限あるユーザーとして実行」にチェックを入れましょう。このチェックを忘れるとコンテナが立ち上がりません。
Docker Volume の設定
また、ホスト ディレクトリのマウント忘れにも注意しましょう。いわゆる Docker Volume の設定です。
ここには、/var/run/docker.sock
と書きます。これを設定しなければ drone runner は動きません。仮にこれを書き忘れて drone を動かした場合、正常にアクセスができるものの、runner のタスクは動きません。公式が提供している docker-compose.yml に同様の設定をされているので、GCE の設定にも反映させましょう。
固定 IP にする
次にネットワーク インターフェースを設定し、VM インスタンスの IP アドレスを固定化します。固定化する理由は、認証のためです。Drone は認証に Github を用いた OAuth 認証を行います。その際に callbackURL(Github での認証後にアプリケーションへ戻る際の URL)を登録しなければならず、固定化されたアドレスが必要となります。
この設定は drone server 側のみで大丈夫です。
ドメインをとる
Github 認証の callback で必要となるので取得しましょう3。 インターネットに接続できるマシンには IP アドレスが割り当てられます。そのアドレスに対して FQDN4をラベルとして割り振ることができます。この IP アドレスと FQDN は DNS と呼ばれる仕組みに組として登録されます。ドメインを取得し、DNS にその FQDN と IP アドレスの組を登録することで、FQDN 経由で自分のマシンへアクセスできます。それではドメインの取得と DNS の登録を行いましょう。これは GCP の外側で行います。
ドメインの取得
ドメインの取得は、Google Domainsから行えます。これは有償ですが、無料で独自ドメインの取得サービス 6 選によると、無料で取得する方法もあるようです。
DNS への登録
ドメインを取得したら次は FQDN を DNS に登録しましょう。これもGoogle Domainsから行えます。
登録したら dig コマンド で確認しましょう。このコマンドは、DNS に問い合わせを行ってくれます。出力に ANSWER SECTION が含まれていれば成功です。
$ dig hogehoge.com
どうでしょう。実はこのままでは表示されません。DNS レコードの設定が必要です。ドメインを IP アドレスに置き換えたいので、A(Address)レコード を追加します。
再度 DNS に問い合わせましょう。
$ dig hogehoge.com
下記のように、出力に ANSWER SECTION が含まれていれば成功です。
;; ANSWER SECTION:
hogehgoe.com 9999 IN A 111.111.111.111
github から token を払い出す
グローバルナビゲーションヘッダから user setting を開きます。
新しくアプリケーションを登録しましょう。登録することで token を取得できます。
登録の際、コールバック URL の指定があります。その際に、無意識に https 始まりの url を指定しないようにしましょう。今回使っているのは http です。この設定ミスで私は時間を 1 時間近く無駄にしました。
環境変数を設定する
次に環境変数をセットします。server, runner 共に設定してください。
server の設定
Drone Documentationにある Docker での起動方法 の例を GCE 上に落とし込んでいきます。
設定の各項目がどういう意味かは Referenceにまとまっています。
項目名 | セットすべきもの | 説明 |
---|---|---|
DRONE_GITHUB_SERVER | https://github.com | github の URL です。 |
DRONE_GITHUB_CLIENT_ID | Github から払い出した access token | GitHub OAuth client id です。Github のユーザー設定ページから作成できます。 |
DRONE_GITHUB_CLIENT_SECRET | Github から払い出した secret key | GitHub OAuth client secret です。Github のユーザー設定ページから作成できます。 |
DRONE_RPC_SECRET | 何でも可能(なるべくランダムに) | server <--> runner 間の認証に使われます。 |
DRONE_SERVER_HOST | drone server のデプロイ先のホスト名 | ユーザーが CI サーバーにアクセスする際の URL のホスト名です。 |
DRONE_SERVER_PROTO | http もしくは https | ユーザーが CI サーバーにアクセスする際の URL のプロトコルです。 |
runner の設定
Drone Documentationにある Docker での起動方法 の例を GCE 上に落とし込んでいきます。
設定の各項目がどういう意味かは Referenceにまとまっています。
項目名 | セットすべきもの | 説明 |
---|---|---|
DRONE_RPC_PROTO | DRONE_SERVER_PROTO と同じ値 | ユーザーが drone server にアクセスするときに使っているプロトコル |
DRONE_RPC_HOST | DRONE_SERVER_HOST と同じ値 | ユーザーが drone server にアクセスするときの URL のホスト名 |
DRONE_RPC_SECRET | drone server で設定したものと同じ値 | server <--> runner 間の認証に使われます。 |
DRONE_RUNNER_CAPACITY | 数字 | drone runner で同時に動作させるパイプラインの数の上限 |
firewall の設定
今回は https の設定をしないため、http アクセスを受け付けるようにしたいです。http からもアクセスを受け付けるように firewall を設定しましょう。
疎通確認
ここまで来れば drone にアクセスできます。drone server の URL を叩いてみましょう。Github との認証をしたら、drone 内に入れたはずです。
ここからは drone でビルド -> デプロイをできるようにするための設定をしていきましょう。
Service Account を作る
今回は GAE にデプロイを行います。通常 GAE には gcloud コマンドを利用してデプロイします。
例えば、 gcloud app deploy ${GAEの設定ファイル} --project ${プロジェクト名}
でデプロイできます。このコマンドを使えるようにするためにはプロジェクトに対応した権限が必要なため、事前に GAE のサービスだけ作っておきましょう。コンソールから作成できます。
drone server 内からこの権限を持ってデプロイできるようにするためには、デプロイ権限を持ったサービスアカウントが必要です。
権限の強さは、GAE のデプロイ担当者にします。
そしてこの権限を drone から使えるようにするためには、認証情報を書き出します。ここでは JSON で DL してください。今後、drone 上からはこの json を読み込んで、gcloud コマンドを実行していきます。
詳しくは後述しますが、実はこの権限では弱く、さらには権限の種類も足りていません。しかし順を追って説明したいため、一旦このまま設定を続けてください。
デプロイ成果物の準備
さて、ここまでくればあとは成果物を作り、デプロイするだけです。ここでは gatsby の starter-kit で成果物を作り上げます。
npmコマンドを使うため、NodeJS を持っていない方は公式ページもしくはnvmから DL して下さい。
$ npm i -g gatsby
$ gatsby new gatsby-starter-dimension https://github.com/codebushi/gatsby-starter-dimension
これで成果物が手に入れました。
// 成果物を確認する
$ npm run develop
// 成果物をビルドする
$ npm run build
ビルドすると public フォルダができます。これが最終成果物です。いまからこのフォルダをデプロイしましょう。
drone の設定
drone ではビルドの方法や手順を .drone.yml
というファイルに書いていきます。これを Gatsby プロジェクトのルートに配置してください。yaml の詳しい書き方はこちらにまとまっています。
kind: pipeline
type: docker
name: pr
steps:
- name: gatsby-build
image: node:10
commands:
- yarn
- yarn run build
- name: gatsby-deploy
image: gcr.io/cloud-builders/gcloud
environment:
GOOGLE_TOKEN:
from_secret: google_token
commands:
- bash -c 'echo -E $${GOOGLE_TOKEN} > /tmp/credential.json'
- gcloud auth activate-service-account --key-file /tmp/credential.json
- gcloud app deploy app-dev.yaml --project musicbox-dev
trigger:
branch:
- master
- feature/*
event:
- push
ここで設定ファイルに登場する用語をざっと説明します。
設定ファイルの説明
steps
ビルドの手順です。ひとつひとつがグループだと思ってください。
ここでは gatsby-build と gatsby-deploy が行われます。
image
その step で使う docker image を選択できます。
例えば ビルドのために Node.js の実行環境が欲しければ、node:10
などを設定します。
trigger
Github でのトリガーを指定します。branch, event というオプションを使って、「どのブランチに何されたら step を実行」ということを記述できます。ブランチ名の指定には、いわゆるglob 形式を使えます。そのため feature/**
などと指定して、feature や fix ブランチ以外からの デプロイ を弾くといったこともできます。
deploy の 設定
Service Account の認証
gatsby-deploy を行うためには設定ファイルを読み込ませないといけません。それは gatsby-server 内での設定で行えます。secrets から鍵情報が入った json を読み込みましょう。該当する環境変数名を通して、drone タスクが認証するようにしています。
- bash -c 'echo -E $${GOOGLE_TOKEN} > /tmp/credential.json'
- gcloud auth activate-service-account --key-file /tmp/credential.json
```
ここでの `echo -E` に注意してください。鍵情報が入ったjsonの中身をみてみると改行の制御文字が多く含まれています。そこで、その文字がそのまま鍵情報として環境変数に渡されないように、改行を表すオプション `-E` を指定しておきましょう。
![droneenv.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89256/8be6281e-a95e-36ed-d30e-879a19f40bce.png)
### GAE の設定
GAE の振る舞いは app.yaml というファイルに書きます。ここでは次のように書いたファイルを Gatsby プロジェクトルートに配置してください。
```yaml
runtime: php55 # 静的ページのデプロイなのでruntimeは何でも良い(NodeJSでなくても良い)
api_version: 1
threadsafe: true
handlers:
- url: /(.*\..*)
static_files: public/\1
upload: public/(.*)
- url: /(.*)/
static_files: public/\1/index.html
upload: public/(.*)/index.html
- url: /
static_files: public/index.html
upload: public/index.html
- url: /(.*)
static_files: public/\1/index.html
upload: public/(.*)/index.html
```
handlers の項目はルーティングの設定です。
また成果物以外(特に node_modules)をデプロイしないために、 .gcloudignore というファイルも Gatsby プロジェクトルートに配置してください。ファイルの中身は下記の通りです。
```
# This file specifies files that are *not* uploaded to Google Cloud Platform
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
# Gatsby
.cache
# Node
node_modules
```
ここに書かれたファイルはGAEにdeployされません。
## Service Account への権限振り
ここでデプロイの設定は終わりました。ただ初回起動はちょっとした作業をしないとうまくいかないので、その作業をしていきましょう。おそらく このようなエラーがでたのではないでしょうか。
```
ERROR: (gcloud.app.deploy) User [gae-deploy@${project_name}.iam.gserviceaccount.com] does not have permission to access app [${project_name}] (or it may not exist): App Engine Admin API has not been used in project ${projectId} before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/appengine.googleapis.com/overview?project=${projectId} then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.
```
言われるがままに、表示されているリンクに飛びApp Engine Admin APIを有効にします。そしてビルドタスクを再起動しましょう。
![restart.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89256/e2f59f50-0703-7261-7326-232e6cb4b131.png)
しかし、またエラーが出るでしょう。
```
+ bash -c 'echo -E ${GOOGLE_TOKEN} > /tmp/credential.json'
2 + gcloud auth activate-service-account --key-file /tmp/credential.json
3 Activated service account credentials for: [gae-deploy@${project_name}.iam.gserviceaccount.com]
4 + gcloud app deploy --quiet --project ${project_name}
5 Services to deploy:
6
7 descriptor: [/drone/src/app.yaml]
8 source: [/drone/src]
9 target project: [${project_name}]
10 target service: [default]
11 target version: [20191104t045740]
12 target url: [https://${project_name}.appspot.com]
13
14
15 Beginning deployment of service [default]...
16 ERROR: (gcloud.app.deploy) 403 Could not list bucket [staging.${project_name}.appspot.com]: gae-deploy@${project_name}.iam.gserviceaccount.com does not have storage.objects.list access to staging.${project_name}.appspot.com.
```
今度は GCS の権限を求められます。今回は GCS を利用していませんが、おそらく GAE はソースコードを GCS に置くため、GCS の権限も要求されます。そしてどういうわけか、GCS の管理者権限を付与しないと動かなかったので、ここでは管理者権限を付与します。そうするとエラーログが変わりました。
```
ERROR: (gcloud.app.deploy) Your deployment has succeeded, but promoting the new version to default failed. You may not have permissions to change traffic splits. Changing traffic splits requires the Owner, Editor, App Engine Admin, or App Engine Service Admin role. Please contact your project owner and use the `gcloud app services set-traffic --splits <version>=1` command to redirect traffic to your newly deployed version.
Original error: PERMISSION_DENIED: The caller does not have permission
```
GAE のデプロイ権限も弱いと言われています。そのため、App Engine Admin role を付与しました。設定の方法は前述の「サービスアカウント を作る」で説明した方法を行ってください。
[役割について](https://cloud.google.com/iam/docs/understanding-roles?hl=ja)によると、
> プロジェクト roles/ appengine.deployer App Engine デプロイ担当者 すべてのアプリケーションの構成と設定に対する読み取り専用権限。 新しいバージョンの作成のみに限定された書き込み権限。既存のバージョンを変更することはできません。ただし、トラフィックを受信していないバージョンは削除できます。
とあり、デプロイ担当者の権限はかなり弱いです。
### 疎通確認
では URL にアクセスしてみましょう。
<img width="1432" alt="deployed.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89256/49457e42-26ac-f044-a07d-0cadffad7468.png">
はい、無事に表示できました。お疲れ様です。
## 正直めんどくさい設定
### 環境変数の設定
途中の煩雑な環境変数の設定は docker-compose を使えばある程度は自動化できます。環境変数の設定は[Drone 1.0 を docker-compose up する](https://matsubara0507.github.io/posts/2019-01-05-docker-compose-up-drone-1-0.html)にある設定ファイルを GCE 上で、手作業でやっていたに過ぎません。
### 構成管理
構成管理も [terraform](https://www.terraform.io/)を使えばもっと確実性や再現性が増します。ただ GCE の Container Optimized OS 特有の設定として `gce-container-declaration` を `metadata` に書き込まないといけなく、その設定が多く環境変数の取り扱いもめんどくさいのであまりオススメはしません。
もし再現性が欲しい場合は、一度作った GCE をスナップショットして書き出しても良いでしょう。
# まとめ
いかがでしたか。ただ CI/CD を作るだけでとても苦労しました。インフラの構築は、アプリケーション開発とは違った難しさがたくさんあります。特に「動く or 動かない」といった側面が強いため、ログを見てデバッグするといった戦略が取りづらく、問題の切り分けが難しかったです。私はたまたまインフラが得意な友人に恵まれており、質問したりペアプロしてもらいながら切り抜けられました。しかし「個人でやれ」と言われたら到底できなかったでしょう。私はそんな環境構築を最後までやりきったことで、これまで知らなかった様々な概念を一度に習得できました。「インフラの単語よく聞くけど、やったことないし何もわからん」といった方はぜひこの CI/CD 構築に挑戦してみてはどうでしょうか。とても学習効率の高いものです。
-
どれくらい未経験かでいうと、そもそも最近までアカウントを持っていませんでした。 ↩
-
特に画像の最適化プラグインは非常に強力で、一度その機能を調べたことがあるため興味のある方は是非ご覧ください。(gatsby-image をやめたいから勉強した話 -gatsby を amp 化したい-) ↩
-
厳密には生の IP をホスト名としてそのまま callback に使えば認証できるのですが、drone 側の環境変数として FQDN(ただしこれはホスト名としてセットする)を求められるので、ドメインを取った方が自然でしょう。それに drone はクッキーを使った認証を行うため、なんらかの一意な名前が欲しいです。 ↩
-
ドメインとドメイン名の混乱を避けるために、あえてドメインという言葉を使わずに FQDN と書いています。 ↩