GKE + Spinnaker の構築を Terraform で行ってみた。
先週の記事で Spinnaker 構築いたしましたが手順が多い上にいろいろ展開させたいので **Terraform で構築できるようにしたらよくね?**っと思いつき、チャレンジしてみました!!が・・・Terraform + GCP の闇にガッツリはまりましたので顛末をご紹介いたします。
Terraform ですが、例の HashiCorp. 御謹製のツールで、Vagrant のクラウド版といった感じのツールです。
で、Terraform 頑張ってみましたが、そもそも Spinnaker だけでもかなりいろいろできるものなのでそれだけをサービスしちゃってる会社もあるくらい。
(たぶん、Terraform で Spinnaker がっつりやってるはずです。Spinnaker + Terraform で多数のブログが見つかりますもん。)
さらに kubernetes というか GKE に乗せるというのは割と大変でした。はっきり言って 敵はロール。
なんだか Terraform の GCP Provider と GoogleのAPI が微妙にかみ合っていない感じで・・・。
とりあえず、動作確認最優先でトライしてみました。
対象環境
- OS : Ubuntu 18.04 LTS
- Terraform : 0.11.13
- Terraform : 0.11.13 !!
- Terraform : 0.11.13 !!!
- GCP Provider : 2.5
- Kubernetes Provider : 1.6
まず、OS は Ubuntu でやってます。Windows 10 の git-bash でも試しましたがなんだか微妙です。Linux か mac 版 Terraform がおすすめです。
続いて Terraform のバージョン、大事なポイントなので 3 回書きましたが・・・そろそろ 0.12 が出るらしいです。ですがこの記事はあくまで 0.11.13 が対象です!
また issue にも過去のバージョンだとうまくいくとかいろいろ出てます。Terraform はまだまだ 1.0 ではないので毎度、破壊的更新が入っている模様です。
で、GCP Provider だけ妙に進化してて 2.5 です。でもやっぱり本体の API がなんか微妙なんだよね。。。
(特に K8s との相性が良くないかも・・・?)
というわけで以下、手順をご紹介いたします。
環境構築
1. Google Cloud Platform での準備
1-1. API の有効化
Spinnaker をインストールするにあたって以下のサービス API を有効にしておきます。
- Google Identity and Access Management (IAM) API
- Google Cloud Resource Manager API
1-1. Terraform 用サービスアカウントの作成
GCP の WEB 管理画面より Terraform 用のサービスアカウントを作成します。
またこのアカウントのロールですが、Spinnaker 用のサービスアカウントにロールを割り当てるためにどのロールを持つべきかいまだに不明なのでオーナー
で作成してしまいました。(これは要検討です。)
作成後、このサービスアカウントのアクセスキーを json 形式でダウンロードしてきます。以下、account.json
とします。
1-2. Cloud Storage でバケットの作成
Terraform は様々なリソース1 の状態管理に json ファイルを使用しますが、ローカルだと複数メンバで作業したときに衝突が発生するので GCS を利用してグローバルな状態管理を有効にします。
また GCS のバケット名はグローバルで一意である必要があります。適当に名前を付けてください。
2. Terraform のインストール
以下の手順で Terraform をインストールします。
$ git clone https://github.com/tfutils/tfenv.git ~/.tfenv
$ echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bash_profile
$ . ~/.bash_profile
$ tfenv install latest
tfenv
は nvm
ライクなバージョン管理ツールです。ホント便利。
定義ファイルの作成
今回は main.tf
に全部乗せしてしまいます。
以下の手順で main.tf
にガリガリ書いていきます。
Terraform バックエンドの設定
上記で作成したストレージのバケットを参照します。ストレージのバケット名に"sample_terra_spinn_04201503"と名前を付けた場合は・・・
terraform {
backend "gcs" {
bucket = "sample_terra_spinn_04201503"
prefix = "terraform/state"
}
}
こんな感じです。terraformの書式にまだQiitaは対応してないのか。
GCP Provider の追加
続いて Google Cloud Platform への接続情報を追加します。各 variable
の default
値はそれぞれ作成したプロジェクトに合わせてください。
variable "project_id" {
default = "my-fantastic-terra-spin-project"
}
variable "gcp_zone" {
default = "us-central1-c"
}
variable "region" {
default = "us-central1"
}
provider "google" {
credentials = "${file("account.json")}"
project = "${var.project_id}"
region = "${var.region}"
zone = "${var.gcp_zone}"
version = "~> 2.5"
}
プロジェクト名や GCP のゾーン、リージョンは K8s と VM の構築でも参照するので変数名としておきます。local でも OK かとおもいます。
Kubernetes Provider の追加
続いて GKE で作成した後の k8s を操作するには別途、K8s 専用のプラグインを使いますので、k8s provider を追加します。
provider "kubernetes" {
host = "${google_container_cluster.gke.endpoint}"
username = "${google_container_cluster.gke.master_auth.0.username}"
password = "${google_container_cluster.gke.master_auth.0.password}"
client_certificate = "${base64decode(google_container_cluster.gke.master_auth.0.client_certificate)}"
client_key = "${base64decode(google_container_cluster.gke.master_auth.0.client_key)}"
cluster_ca_certificate = "${base64decode(google_container_cluster.gke.master_auth.0.cluster_ca_certificate)}"
version = "~> 1.6"
}
google_container_cluster.gke
はまだ存在しませんが、後ほど作成する GKE のクラスタです。
こんな感じで GKE クラスタ -> Kubernetes Provider へ接続情報を渡すとイケました。
プラグインを実際にインストール
ここでターミナルにて以下のコマンドを打ちます
$ export GOOGLE_CREDENTIALS=$(cat account.json)
$ export GOOGLE_APPLICATION_CREDENTIALS="account.json"
$ terraform init
上記の terraform init
コマンドでProvider
の記述を認識して実際にGCP Provider
、k8s provider
などがローカルにインストールされます。
ただし、GCS を使用すると Google の Default Credentials が使われるので、プロジェクトの設定と合わなくなると思います。
上記のように一時的に、GOOGLE_CREDENTIALS
とGOOGLE_APPLICATION_CREDENTIALS
を export してダウンロードしてきた account.json
を参照するようにします。
.bashrc や.bash_profile などで恒久的に export してしまうと、他のプロジェクトに影響あるので毎度、一時的にしないとダメなのがツラいです。このディレクトリ以下だけ有効になる環境変数
みたいなツールどっかにあったような。。。
サービスアカウントの作成
以下の Spinnaker のインストール手順を参考に、Terraform 設定ファイルを起こしていく作業となります。
ここではhalyard
とspinnaker
用のサービスアカウントを作成し、それぞれに以下のようにロールを割り当てます。
- halyard :
roles/iam.serviceAccountKeyAdmin
とroles/container.admin
- spinnaker :
roles/storage.admin
とroles/browser
resource "google_service_account" "halyard" {
account_id = "halyard-service-account"
display_name = "halyard & Spinnaker service account"
}
resource "google_project_iam_member" "halyard-iam-keyadmin" {
depends_on = ["google_service_account.halyard"]
role = "roles/iam.serviceAccountKeyAdmin"
member = "serviceAccount:${google_service_account.halyard.email}"
}
resource "google_project_iam_member" "halyard-iam-containeradmin" {
depends_on = ["google_service_account.halyard"]
role = "roles/container.admin"
member = "serviceAccount:${google_service_account.halyard.email}"
}
resource "google_service_account" "spin-gcs" {
account_id = "spin-gcs-service-account"
display_name = "halyard & Spinnaker cloud storage service account"
}
resource "google_project_iam_member" "SpinGCS-iam-storageadmin" {
depends_on = ["google_service_account.spin-gcs"]
role = "roles/storage.admin"
member = "serviceAccount:${google_service_account.spin-gcs.email}"
}
resource "google_project_iam_member" "SpinGCS-iam-browser" {
depends_on = ["google_service_account.spin-gcs"]
role = "roles/browser"
member = "serviceAccount:${google_service_account.spin-gcs.email}"
}
display_name
は説明表示用なので適当で OK です。
"googleproject_iam*"系のリソースが厄介です。今回はこれがうまくいかなかったのでオーナー
のロールで動かしてます。
また、google_service_account
リソースは本家サイトのヘルプにもある通り、"eventually consistent" です。
API の終了とともにサービスアカウントが作成されるわけではないので、後続処理も遅延させる必要があります。過去の issue で local_exec provisioner 使って sleep してる技も紹介されていますが、ここでは depends_on で順番指定して、タイムアウトに間に合っているので OK という感じです。
GKE で K8s クラスタの作成
variable "gke_cluster_name" {
default = "standard-cluster-1"
}
resource "google_container_cluster" "gke" {
depends_on = ["google_service_account.halyard",
"google_service_account.spin-gcs",
]
name = "${var.gke_cluster_name}"
location = "${var.gcp_zone}"
description = "K8s build by Terraform"
remove_default_node_pool = true
initial_node_count = 1
}
resource "google_container_node_pool" "spinnaker" {
name = "default-node-pool"
location = "${var.gcp_zone}"
cluster = "${google_container_cluster.gke.name}"
node_count = 3
node_config {
preemptible = true
machine_type = "n1-standard-4"
metadata {
disable-legacy-endpoints = "true"
}
oauth_scopes = [
"logging-write",
"monitoring",
]
}
}
とりあえず K8s のハコはgoogle_container_cluster
とgoogle_container_node_pool
の 2 リソースで OK です。
K8s クラスタに Spinnaker 用の権限などを追加
これもいろいろ試しましたが、以下の設定で落ち着いてます。
resource "kubernetes_cluster_role_binding" "client-admin-binding" {
metadata {
name = "client-admin"
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "ClusterRole"
name = "cluster-admin"
}
subject {
kind = "User"
name = "client"
}
}
resource "kubernetes_namespace" "spinnaker" {
metadata {
annotations {
name = "spinnaker"
}
name = "spinnaker"
}
}
resource "kubernetes_service_account" "spinnaker-sa" {
depends_on = ["kubernetes_namespace.spinnaker"]
metadata {
name = "spinnaker-service-account"
namespace = "spinnaker"
}
}
resource "kubernetes_cluster_role_binding" "spinnaker-binding-admin" {
depends_on = ["kubernetes_namespace.spinnaker"]
metadata {
name = "spinnaker-admin"
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "ClusterRole"
name = "cluster-admin"
}
subject {
kind = "ServiceAccount"
name = "spinnaker-service-account"
namespace = "spinnaker"
}
}
resource "kubernetes_cluster_role" "spinnaker-role" {
metadata {
name = "spinnaker-role"
}
rule {
api_groups = [""]
resources = ["namespaces", "configmaps", "events", "replicationcontrollers", "serviceaccounts", "pods/log"]
verbs = ["get", "list"]
}
rule {
api_groups = [""]
resources = ["pods", "services", "secrets"]
verbs = ["create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"]
}
rule {
api_groups = ["autoscaling"]
resources = ["horizontalpodautoscalers"]
verbs = ["list", "get"]
}
rule {
api_groups = ["apps"]
resources = ["controllerrevisions", "statefulsets"]
verbs = ["list"]
}
rule {
api_groups = ["extensions", "apps"]
resources = ["deployments", "replicasets", "ingresses"]
verbs = ["create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"]
}
# These permissions are necessary for halyard to operate. We use this role also to deploy Spinnaker itself.
rule {
api_groups = [""]
resources = ["services/proxy", "pods/portforward"]
verbs = ["create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"]
}
}
resource "kubernetes_cluster_role_binding" "spinnaker-role-bindings" {
depends_on = ["kubernetes_namespace.spinnaker", "kubernetes_service_account.spinnaker-sa"]
metadata {
name = "spinnaker-role-binding"
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "ClusterRole"
name = "spinnaker-role"
}
subject {
kind = "ServiceAccount"
name = "spinnaker-service-account"
namespace = "spinnaker"
}
}
というか、ここでのキモはやっぱり client-admin-binding
ですね。。。こうするしかないのかなぁ・・・?
GCE で Halyard VM の作成
SSH キーの生成
まず、terraform が vm に ssh 接続するための ssh キー をパスワードなしで生成しておきます。
ssh-keygen
で作成したキーをssh/halyard_vm_rsa.pub
とssh/halyard_vm_rsa
としておきます。
VM 定義の記述
以下のようにsmall
な VM を作成します。
data "google_compute_image" "ubuntu_image" {
name = "ubuntu-1404-trusty-v20190410"
project = "ubuntu-os-cloud"
}
variable "gcp_account_name" {
default = "terraform-halyard"
}
resource "google_compute_instance" "halyard" {
depends_on = ["google_container_node_pool.spinnaker"]
name = "${var.halyard_vm_name}"
machine_type = "g1-small"
boot_disk {
initialize_params {
image = "${data.google_compute_image.ubuntu_image.self_link}"
}
}
network_interface {
network = "default"
access_config = {}
}
service_account {
email = "${google_service_account.halyard.email}"
scopes = ["cloud-platform"]
}
metadata {
"block-project-ssh-keys" = "true"
"sshKeys" = "${var.gcp_account_name}:${file("ssh/halyard_vm_rsa.pub")}"
}
provisioner "remote-exec" {
inline = []
connection {
type = "ssh"
user = "${var.gcp_account_name}"
private_key = "${file("ssh/halyard_vm_rsa")}"
timeout = "5m"
}
}
}
metadata
で pub キーを渡し、remote-exec provisioner で pri キーを使用して接続します。
Spinnaker のインストール手順を追加
上記の remote-exec provisioner では inline
の設定が空っぽになっておりました。
ここに halyard の手順書にある通り、怒涛の設定コマンドを追記します。remote-exec provisionar の箇所だけ抜粋すると以下のようになります。
provisioner "remote-exec" {
inline = [
"KUBECTL_LATEST=$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)",
"curl -LO https://storage.googleapis.com/kubernetes-release/release/$KUBECTL_LATEST/bin/linux/amd64/kubectl",
"chmod +x kubectl",
"sudo mv kubectl /usr/local/bin/kubectl",
"curl -O https://raw.githubusercontent.com/spinnaker/halyard/master/install/debian/InstallHalyard.sh",
"sudo bash InstallHalyard.sh --user ${var.gcp_account_name} -y",
". ~/.bashrc",
"gcloud config set container/use_client_certificate true",
"gcloud container clusters get-credentials ${var.gke_cluster_name} --zone=${var.gcp_zone}",
"GCS_SA_DEST=~/.gcp/gcp.json",
"mkdir -p $(dirname $GCS_SA_DEST)",
"gcloud iam service-accounts keys create $GCS_SA_DEST --iam-account ${google_service_account.spin-gcs.email}",
"hal config version edit --version $(hal version latest -q)",
"hal config storage gcs edit --project $(gcloud info --format='value(config.project)') --json-path $GCS_SA_DEST",
"hal config storage edit --type gcs",
"hal config provider docker-registry enable",
"hal config provider docker-registry account add my-gcr-account --address gcr.io --password-file $GCS_SA_DEST --username _json_key",
"CONTEXT=$(kubectl config current-context)",
"TOKEN=$(kubectl get secret --context $CONTEXT ${kubernetes_service_account.spinnaker-sa.default_secret_name} -n spinnaker -o jsonpath='{.data.token}' | base64 --decode)",
"kubectl config set-credentials $${CONTEXT}-token-user --token $TOKEN",
"kubectl config set-context $CONTEXT --user $${CONTEXT}-token-user",
"hal config provider kubernetes enable",
"hal config provider kubernetes account add my-k8s-account --docker-registries my-gcr-account --provider-version v2 --context $CONTEXT",
"hal config deploy edit --account-name my-k8s-account --type distributed",
"hal deploy apply",
]
connection {
type = "ssh"
user = "${var.gcp_account_name}"
private_key = "${file("ssh/halyard_vm_rsa")}"
timeout = "5m"
}
}
通常の手順に加えて、以下の RBAC の手順も追加してます。
- https://www.spinnaker.io/setup/install/providers/kubernetes-v2/#optional-create-a-kubernetes-service-account
- https://www.spinnaker.io/setup/install/providers/kubernetes-v2/#optional-configure-kubernetes-roles-rbac
これはもう、ここまでくると各コマンドがうまくいくように祈るだけです。
以上で terraform の設定ファイルは作成完了です!
デプロイと接続
Terraform の実行
以下の手順で Terraform を実行します。
$ terraform plan
$ terraform apply
terraform plan
で実行計画が表示されます。追加、変更、削除など操作予定が表示されますので確認いたします。
terraform apply
で上記の実行計画が再度、表示され、問題なければ yes
と入力します。
しばらく待てば、halyard 用 VM と Spinnaker クラスタの出来上がりです。
Halyard VM に接続
以下のコマンドで Halyard 用 VM に接続します。
例によって ブラウザから localhost が見れる環境 からです!(トラウマ)
$ gcloud compute ssh $HALYARD_HOST \
--project=$GCP_PROJECT \
--zone=us-central1-f \
--ssh-flag="-L 9000:localhost:9000" \
--ssh-flag="-L 8084:localhost:8084"
VM につながったら、以下のコマンドで Spinnaker に接続します。
$ hal deploy connect
forwarding
の表示が出たら、ローカルマシンのブラウザを立ち上げましょう。http://localhost:9000
で Spinnaker の管理画面が表示されたら OK です。
お疲れさまでした!
Spinnaker の破棄
以下のコマンドで Terraform で構築した GCP の環境を破棄できます。
$ terraform destroy
ただし、GKE クラスタの削除にはかなり時間がかかる模様です。
デフォルトのタイムアウト(5m)では(たぶん)間に合わないので、その際は手作業で削除しましょう。
反省点というか問題点
冒頭にも記載しましたがいくつか、ロールで強行突破してしまっている点があります。
- Terraform 用のサービスアカウントが
オーナー
- GKE クラスタの
client
がadmin
上記の手順ではここが思いっきり穴なので、引き続き見直していきたいと思います。
いや~大変でしたが、今回はここまでといたします!
-
Terraform 管理対象となるアカウントや VM などといったすべてのオブジェクト。Terraform ではこのリソースとその状態を主に管理します。 ↩