LoginSignup
5
4

More than 3 years have passed since last update.

【2019年4月版】Terraform+Spinnakerがよくね!? → 闇が深い。

Last updated at Posted at 2019-04-23

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

tfenvnvm ライクなバージョン管理ツールです。ホント便利。

定義ファイルの作成

今回は main.tf に全部乗せしてしまいます。
以下の手順で main.tf にガリガリ書いていきます。

Terraform バックエンドの設定

上記で作成したストレージのバケットを参照します。ストレージのバケット名に"sample_terra_spinn_04201503"と名前を付けた場合は・・・

main.tf
terraform {
  backend "gcs" {
    bucket  = "sample_terra_spinn_04201503"
    prefix  = "terraform/state"
  }
}

こんな感じです。terraformの書式にまだQiitaは対応してないのか。

GCP Provider の追加

続いて Google Cloud Platform への接続情報を追加します。各 variabledefault 値はそれぞれ作成したプロジェクトに合わせてください。

main.tf
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 を追加します。

main.tf
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 Providerk8s providerなどがローカルにインストールされます。
ただし、GCS を使用すると Google の Default Credentials が使われるので、プロジェクトの設定と合わなくなると思います。
上記のように一時的に、GOOGLE_CREDENTIALSGOOGLE_APPLICATION_CREDENTIALS を export してダウンロードしてきた account.jsonを参照するようにします。

.bashrc や.bash_profile などで恒久的に export してしまうと、他のプロジェクトに影響あるので毎度、一時的にしないとダメなのがツラいです。このディレクトリ以下だけ有効になる環境変数みたいなツールどっかにあったような。。。

サービスアカウントの作成

以下の Spinnaker のインストール手順を参考に、Terraform 設定ファイルを起こしていく作業となります。

ここではhalyardspinnaker用のサービスアカウントを作成し、それぞれに以下のようにロールを割り当てます。

  • halyard : roles/iam.serviceAccountKeyAdminroles/container.admin
  • spinnaker : roles/storage.adminroles/browser
main.tf
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 です。

"google*project_iam**"系のリソースが厄介です。今回はこれがうまくいかなかったのでオーナーのロールで動かしてます。

また、google_service_accountリソースは本家サイトのヘルプにもある通り、"eventually consistent" です。

API の終了とともにサービスアカウントが作成されるわけではないので、後続処理も遅延させる必要があります。過去の issue で local_exec provisioner 使って sleep してる技も紹介されていますが、ここでは depends_on で順番指定して、タイムアウトに間に合っているので OK という感じです。

GKE で K8s クラスタの作成

main.tf
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_clustergoogle_container_node_poolの 2 リソースで OK です。

K8s クラスタに Spinnaker 用の権限などを追加

これもいろいろ試しましたが、以下の設定で落ち着いてます。

main.tf
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.pubssh/halyard_vm_rsaとしておきます。

VM 定義の記述

以下のようにsmallな VM を作成します。

main.tf
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 の箇所だけ抜粋すると以下のようになります。

main.tf
  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 の手順も追加してます。

これはもう、ここまでくると各コマンドがうまくいくように祈るだけです。

以上で 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 クラスタのclientadmin

上記の手順ではここが思いっきり穴なので、引き続き見直していきたいと思います。

いや~大変でしたが、今回はここまでといたします!


  1. Terraform 管理対象となるアカウントや VM などといったすべてのオブジェクト。Terraform ではこのリソースその状態を主に管理します。 

5
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4