なに作るの?
- GitHubのmasterブランチにプッシュされたら、
- Cloud Buildが勝手にビルド始めて、DockerイメージをGCRにプッシュ、
- そのDockerイメージを使ってGKEに自動デプロイ
みたいなものを作ります。
GCPのサービスは可能な限り、Terraformで管理していきます。
なるべく詳しい説明をなくし、スピーディに動くものができるように進めていきます。 (怠惰)
では、はりきっていきましょう!
(わかりにくいところありましたらコメントください。アジャイル的に説明を追加していきます。)
Kubernetesを知らない人は、@MahoTakaraさんの記事を読んでおきましょう。
また、今回使うコードはすべてこちらのリポジトリで公開されています。
こんな感じの自動のビルドステップが並ぶものを作っていきます。
用語
GKE(Google Kubernetes Engine)
GCB(Google Cloud Build)
GCR(Google Container Registry)
GSR(Google Cloud Source Repositories)
※略で呼ばれるのが一般的でないものもありますが、省略のため上記の用語を使います。
目次
- 前提
- とりあえずHello Worldサーバーを作る
- GCPの各サービスを有効化する
- Terraformで各サービスを作成する
- GCSとGitHubを連携する
- GCBでビルドする
- Deployment Manifestを書く
- Service Manifestを書く
- GCBでデプロイする
- 試してみる
- 課金されないようにお掃除する
前提
以下の設定や、ライブラリがインストールされていることが前提となります💁♀️
- kubectlがインストールされている
- gcpの空のプロジェクトがとりあえずある
- gcloudがインストールされている
- gcloudのconfigがセットされている
- GCPの課金が有効化されている
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"11", GitVersion:"v1.11.2", GitCommit:"bb9ffb1654d4a729bb4cec18ff088eacc153c239", GitTreeState:"clean", BuildDate:"2018-08-08T16:31:10Z", GoVersion:"go1.10.3", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"11+", GitVersion:"v1.11.7-gke.4", GitCommit:"618716cbb236fb7ca9cabd822b5947e298ad09f7", GitTreeState:"clean", BuildDate:"2019-02-05T19:22:29Z", GoVersion:"go1.10.7b4", Compiler:"gc", Platform:"linux/amd64"}
$ gcloud -v
Google Cloud SDK 234.0.0
beta 2019.01.19
bq 2.0.41
core 2019.02.08
gsutil 4.36
kubectl 2018.09.17
Updates are available for some Cloud SDK components. To install them,
please run:
$ gcloud config list
[compute]
region = asia-northeast1
zone = asia-northeast1-a
[core]
account = hogehoge@gmail.com
disable_usage_reporting = True
project = gcp-yuito-sandbox
こんな感じになっていれば大丈夫です。
とりあえずHello Worldサーバーを作る
なんでもいいですが、今回はNodejsとexpressで簡単なサーバーを作っています。
import express from 'express';
const app = express();
app.get('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
return res.send('Hello World!!!!!!!');
});
app.listen(3000, () => { console.log('Listen to 3000 port...'); });
export default app;
GCPの各サービスを有効化する
gcloudで各サービスを有効化します。
# GCB
$ gcloud services enable cloudbuild.googleapis.com
# GKE
$ gcloud services enable container.googleapis.com
# GCR
$ gcloud services enable containerregistry.googleapis.com
# GSR
$ gcloud services enable sourcerepo.googleapis.com
Terraformで各サービスを作成する
次にTerraformで各サービスを作成していきます
variable "project" {
default = "gcp-yuito-sandbox"
}
variable "region" {
default = "asia-northeast1"
}
variable "zone" {
default = "asia-northeast1-a"
}
variable "network_name" {
default = "gcp-yuito-sandbox"
}
provider "google" {
region = "${var.region}"
project = "${var.project}"
}
// Network
resource "google_compute_network" "default" {
name = "${var.network_name}"
auto_create_subnetworks = false
}
// Subnetwork
resource "google_compute_subnetwork" "default" {
name = "${var.network_name}"
ip_cidr_range = "10.127.0.0/20"
network = "${google_compute_network.default.self_link}"
region = "${var.region}"
private_ip_google_access = true
}
// Cluster
resource "google_container_cluster" "default" {
name = "${var.network_name}"
zone = "${var.zone}"
initial_node_count = 3
min_master_version = "1.11.7-gke.4"
network = "${google_compute_subnetwork.default.name}"
subnetwork = "${google_compute_subnetwork.default.name}"
enable_legacy_abac = true
master_auth {
username = ""
password = ""
}
provisioner "local-exec" {
when = "destroy"
command = "sleep 90"
}
node_config {
oauth_scopes = [
"https://www.googleapis.com/auth/compute",
"https://www.googleapis.com/auth/devstorage.read_only",
"https://www.googleapis.com/auth/logging.write",
"https://www.googleapis.com/auth/monitoring",
]
preemptible = true
machine_type = "g1-small"
}
}
// Static IP
resource "google_compute_global_address" "ip_address" {
name = "gcp-yuito-sandbox-static-ip"
}
// Cloud Build
resource "google_cloudbuild_trigger" "default" {
// 実行トリガーの設定
trigger_template {
// ブランチを設定すると「このブランチにコミットがあった時」がトリガーになります
branch_name = "master"
// 連携するGitリポジトリ。後で解説します。
repo_name = "github_YuitoSato_gcp-yuito-sandbox"
}
// Cloud Buildで使用したいymlファイル
filename = "nodejs-api/infra/staging/cloudbuild.yml"
// 変更を検知したいパス。今回はnodejs-api/srcコード以下に変更があったらビルドしてデプロイをする。
included_files = [
"nodejs-api/src/**"
]
}
TerraformをGCPに適応していきます。
$ cd terraform
$ terraform init
$ terraform plan
# 追加リソースが4つであることを確認
$ terraform apply
※ 5分くらい時間がかかります
ここでは細かい説明ば省略させていただきます。
各項目が何を表しているかは、Terraform × GCPのドキュメントがタメになると思います。
GSRとGitHubを連携する
GSRとGitHubを連携し、GitHubに変更があったらGSRに同じ変更が入るようにします(ミラーリング)
※ ここだけGitHubと連携させるためTerraformで表現できませんでした🙇♀️
プロジェクトIDには 先程Terraformで設定したリポジトリ名を設定します。
名前は揃っていれば何でも大丈夫です。
// 中略
// Cloud Build
resource "google_cloudbuild_trigger" "default" {
trigger_template {
branch_name = "master"
// ココ!!!
repo_name = "github_YuitoSato_gcp-yuito-sandbox"
}
// 中略
}
あとはプロバイダにGitHubを選択し、連携するGitHubのリポジトリを選択すれば完成です。
GCBでビルドする
次はいよいよビルドしていきます。
GCBでアプリをビルドして、そこからDockerイメージを作成、そのDockerイメージをGCRにプッシュします。
Dockerfile
まずはDockerfileを書いていきます。
今回はNodejsですが、もちろんなんでも大丈夫です。
FROM node:11.10.0
# 環境変数はproductionを設定しておいてください。
ENV NODE_ENV=production
ENV LANG C.UTF-8
ARG project_dir=/usr/src/app
ADD ./dist/server.js $project_dir/dist/
ADD ./package.json $project_dir
WORKDIR $project_dir
RUN rm -rf node_modules && \
npm i
EXPOSE 3000
# 本番環境で動かす時の起動コマンドを書きましょう。
ENTRYPOINT ["node", "./dist/server.js"]
TypescriptをコンパイルしたJSファイルをコピーして、npm installしているだけです。
cloudbuild.yml
次はcloudbuild.yml(GCBの実行ステップを記述していくファイル)を書いてきます。
steps:
- name: 'node:11.10.0'
id: 'Clean node_modules'
args: ['rm', '-rf', 'node_modules']
dir: 'nodejs-api'
- name: 'node:11.10.0'
id: 'Clean Dist Dir'
args: ['rm', '-rf', 'dist']
dir: 'nodejs-api'
- name: 'node:11.10.0'
id: 'NPM Install'
args: ['npm', 'install']
dir: 'nodejs-api'
- name: 'node:11.10.0'
id: 'Build Dist File'
args: ['npm', 'run', 'staging-build']
dir: 'nodejs-api'
- name: 'gcr.io/cloud-builders/docker'
id: 'Build Image'
args: ['build', '-t', 'gcr.io/gcp-yuito-sandbox/nodejs-api:$SHORT_SHA', '.', '-f', 'infra/staging/Dockerfile']
dir: 'nodejs-api'
- name: 'gcr.io/cloud-builders/docker'
id: 'Push to GCR'
args: ['push', 'gcr.io/gcp-yuito-sandbox/nodejs-api:$SHORT_SHA']
dir: 'nodejs-api'
1ステップ1コンテナを軸にステップを書いていきます。
name
のところは使用するイメージを記述します。
dir
はデフォルトでリポジトリのルートディレクトリですが、 dir
で設定することができます。
name
で指定したイメージをベースとしてコンテナが立ち上がり、
dir
配下にあるファイルがすべてコンテナ内にコピーされ、
args
で処理が実行されます。
Dockerイメージのビルドや、GCRへのプッシュには、gcr.io/cloud-builders/docker
というDockerイメージを使うことができます。
また SHORT_SHA
という環境変数はトリガーベースで実行した時のみ使用できるGCB用の変数だそうです。
最新のコミットハッシュの頭7文字が変数の中に格納されています。
今回はそのコミットハッシュをイメージのタグとして扱います。
ビルドを試してみる
ここまでで、Terraformでトリガーの設定が終わっているはずなので実際にmasterブランチに対してプッシュをしてみてください。
プッシュした瞬間にGCBでビルドが始まり、GCRにプッシュされたイメージが確認できたら大丈夫です。
Deployment Manifestを書く
次はDeployment Manifestを書いていきます。
Deployment Manifestはどう実際にクラスタ内の一つのアプリケーションを動かすかを定義できるアプリケーションの雛形ファイル的なやつです。
使用するイメージ名や、レプリカの数などを設定することができます。
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nodejs-api
spec:
replicas: 2
template:
metadata:
labels:
run: nodejs-api
spec:
containers:
- name: nodejs-api
- image: gcr.io/gcp-yuito-sandbox/nodejs-api:COMMIT_SHA
ports:
- containerPort: 3000
このような形で書いていきます。
詳しいドキュメントはこちら
Service Manifest
次はService Manifestを書いていきます。
Service Manifestはこのアプリケーションには、こういう感じで接続させるよーみたいな通信設定を定義することができるやつです。
ポート番号やプロトコル、ロードバランスの設定などを簡単に定義できます。
apiVersion: v1
kind: Service
metadata:
name: nodejs-api
spec:
type: LoadBalancer
ports:
- port: 3000
protocol: TCP
targetPort: 3000
selector:
run: nodejs-api
type
にLoadBalancer
って書くだけで、ロードバランスできるのはすごいですね。。。😇
GCBでデプロイする
最後にまたGCBに戻ってデプロイの設定をしていきます。
steps:
# 中略(さっき書いたものの下に書いてください)
- name: 'gcr.io/cloud-builders/gcloud'
id: 'Edit Deployment Manifest'
args:
- '/bin/sh'
- '-c'
- sed -i -e 's/COMMIT_SHA/${SHORT_SHA}/' infra/staging/nodejs-api-deployment.yml
dir: 'nodejs-api'
- name: 'gcr.io/cloud-builders/kubectl'
id: 'Apply Deployment Manifest'
args: ['apply', '-f', 'infra/staging/nodejs-api-deployment.yml']
env:
- 'CLOUDSDK_COMPUTE_ZONE=asia-northeast1-a'
- 'CLOUDSDK_CONTAINER_CLUSTER=gcp-yuito-sandbox'
dir: 'nodejs-api'
- name: 'gcr.io/cloud-builders/kubectl'
id: 'Apply Service Manifest'
args: ['apply', '-f', 'infra/staging/nodejs-api-service.yml']
env:
- 'CLOUDSDK_COMPUTE_ZONE=asia-northeast1-a'
- 'CLOUDSDK_CONTAINER_CLUSTER=gcp-yuito-sandbox'
dir: 'nodejs-api'
基本的に
kubectl apply -f hogehoge.yml
するとそのymlファイルの設定がクラスタに適応されます。
後半にさしかかり、ちょっと説明が雑になってきました、スイマセン🙇♀️
今回はDeployment
とService
のManifest
をapply
コマンドでクラスタに適応しています
しかし、その前に使用するイメージを最新のものにしないといけないので、sed
コマンドで、nodejs-api-deployment.yml
を上書きしています。
先程のプッシュ時にコミットハッシュでタグ付けをしているので、ここではコミットハッシュをイメージのタグとして上書きしています。
sed -i -e 's/COMMIT_SHA/${SHORT_SHA}/' infra/staging/nodejs-api-deployment.yml
の部分ですね。
試してみる
はい、これでCICDの設定は完了しました。
実際にアプリケーションに変更を加えてみましょう。
Hello Worldのビックリマークを減らす。
app.get('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
- return res.send('Hello World!!!');
+ return res.send('Hello World!');
});
$ git add .
$ git commit -m 'decrement !'
$ git push origin master
ビルドが始まりました。
頑張ってます。
お、終わりましたね。
ステップも完璧です。
次はkubectlコマンドでpodの状態を確認していきます。
podはクラスタ内で動いているプロセスです。僕のは3分くらいで立ち上がりました。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nodejs-api-cf554ff64-kvqrm 1/1 Running 0 1m
nodejs-api-cf554ff64-v6zn6 1/1 Running 0 1m
お、デプロイが完了しました!
何も結果返って来ないぞーって人は、 kubectl
が見るクラスタを設定し忘れているので、
$ gcloud container clusters get-credentials クラスタ名 --region リージョン
のコマンドを打ってあげてください。
次はserviceも見ていきます。
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.11.240.1 <none> 443/TCP 18h
nodejs-api LoadBalancer 10.11.250.101 34.85.35.75 3000:32767/TCP 18h
nodejs-apiへの通信ができるようになっていますね。この EXTERNAL-IP
に書いてあるホストにポート番号を設定してリクエストしてみましょう。
無事に!マークは一つになっていますね。めでたしめでたし。
課金されないようにお掃除する
遊んだらお片付けする。3歳児にもできることですね👶GKEは高いのでちゃんとお片付けしましょう。
お片付けは簡単で Terraformのコードをコメントアウトするだけです
(もしかしたらGSRは消しておかないといけないかも)
variable "project" {
default = "gcp-yuito-sandbox"
}
variable "region" {
default = "asia-northeast1"
}
variable "zone" {
default = "asia-northeast1-a"
}
variable "network_name" {
default = "gcp-yuito-sandbox"
}
provider "google" {
region = "${var.region}"
project = "${var.project}"
}
//// Network
//resource "google_compute_network" "default" {
// name = "${var.network_name}"
// auto_create_subnetworks = false
//}
//
//// Subnetwork
//resource "google_compute_subnetwork" "default" {
// name = "${var.network_name}"
// ip_cidr_range = "10.127.0.0/20"
// network = "${google_compute_network.default.self_link}"
// region = "${var.region}"
// private_ip_google_access = true
//}
//
//// Cluster
//resource "google_container_cluster" "default" {
// name = "${var.network_name}"
// zone = "${var.zone}"
// initial_node_count = 3
// min_master_version = "1.11.7-gke.4"
// network = "${google_compute_subnetwork.default.name}"
// subnetwork = "${google_compute_subnetwork.default.name}"
//
// enable_legacy_abac = true
//
// master_auth {
// username = ""
// password = ""
// }
//
// provisioner "local-exec" {
// when = "destroy"
// command = "sleep 90"
// }
//
// node_config {
// oauth_scopes = [
// "https://www.googleapis.com/auth/compute",
// "https://www.googleapis.com/auth/devstorage.read_only",
// "https://www.googleapis.com/auth/logging.write",
// "https://www.googleapis.com/auth/monitoring",
// ]
// preemptible = true
// machine_type = "g1-small"
// }
//}
//
//// Static IP
//resource "google_compute_global_address" "ip_address" {
// name = "gcp-yuito-sandbox-static-ip"
//}
//
//// Cloud Build
//resource "google_cloudbuild_trigger" "default" {
// trigger_template {
// branch_name = "master"
// repo_name = "github_YuitoSato_gcp-yuito-sandbox"
// }
//
// filename = "nodejs-api/infra/staging/cloudbuild.yml"
//
// included_files = [
// "nodejs-api/src/**"
// ]
//}
resource
と書いている奴らを全部コメントアウトします。
$ cd terraform
$ terraform apply
terraform apply
して消し去ります。
これでお掃除は完了です。
お金の管理は僕は責任を負いかねますので、もちろん自己責任でお願いします。
おわりに
お疲れ様でした!
本当はskaffold
とかhelm
とかいかにもウェイウェイしてて合コンでモテそうな技術も使おうと思ったのですが、今回の要件では不必要と判断しました。
ローカル環境をクラスタで構築して、その起動の仕方と本番環境を揃えたいみたいなニーズだと必要になってくるんでしょうか?
あと、紹介するの忘れていましたが、本記事は以下のGCPのチュートリアルをかなり参考にしています。
Cloud Build を使用した GitOps スタイルの継続的デリバリー
ぜひお時間ある方は読んでみてください〜
まだまだこの分野は難しそうですね。。。では✋