Kubernetes | AI tech studio https://cyberagent.ai AI事業本部は、サイバーエージェントグループのデジタルマーケティング分野におけるサービス開発を行う組織です。 Thu, 12 Aug 2021 22:49:22 +0000 ja hourly 1 https://wordpress.org/?v=5.3.26 MLflowをGoogle Kubernetes Engineで動かす快適な実験管理ハンズオン https://cyberagent.ai/blog/research/15272/ Thu, 12 Aug 2021 22:31:10 +0000 https://ai-blog-admin.cyberagent.ai/?post_type=blog&p=15272 AI Labの岩崎(@chck)です、こんにちは。今回は前記事よりも実践的な、AI Labにおける実験管理システムの話をしたいと思います。ここでいう実験とは、データを収集・加工し、統計や機械学習を用い、設定したタスクや仮説を明らかにすることです。実験管理とはその評価や使ったパラメータ及び実験コードを再現できる形で保管することを指します。

対象読者

  • 個人や大学、企業所属でJupyterLab上の実験管理に苦労している方
  • チームでKaggle等のデータ分析コンペに参加している方
  • Kubernetes、GCP、Terraformといったキーワードに興味のある方

tl;dr

MLflowをGKEに載せることで、高可用でユーザ認証を持つMLflow Tracking Serverを作りました。更にTerraformによる1command構築を目指しました。中規模以上の研究室を想定し、Load Balancingにより各ユーザで独立した実験管理を実現しました。個人や少人数チームな場合は、GKEよりもManagedなコンテナサービングサービスであるCloud RunでCloud IAPというユーザ認証が使えるようになったので、本記事をベースにそちらの構成で試すとシンプルで良いでしょう。

はじめに

皆さんは普段どのように実験管理していますか。AI Labには現在30名弱のResearcherが在籍しており、各人が大学のゼミのようにメインテーマを持って日々R&Dに励んでいます。実験のための計算リソースやデータ管理にGCP・AWS・オンプレが用意されており、個々が自由に使えるようになっています。ありがたいことにLabメンバーの増加に伴い、実験やデータ管理を各々が1から行うことで、似た構成のインフラが複数立ち、ノウハウの不透明化やメンテコストが目立つようになりました。そこで今回はResearcherが共通で使えるMLflow Clusterを構築してみます。

2021年現在、実験管理サービスといえばどんなものが思い浮かぶでしょうか。OSSであるMLflowやTensorBoard・Keepsakeの他、SaaSであるNeptune.ai、Comet.ml、Weights & Biases [1]あたりが有名ですね。ワンマンプロジェクトであればSaaSで十分事足りると思いますが、複数人での実験結果の共有や無料版での制限以上のことをやろうとするとMLflow等を自前で立てようと検討するはずです。そしてサーバーを立て、依存するDBやStorageを用意し、PublicにHostingするのであればユーザ認証を行い、HTTPS対応も一緒に…と予想以上に準備が大変なことに気づくでしょう。本来やりたいのはインフラのメンテではなく実験管理なのに、と。そう、各人がインフラ構築やメンテをする必要はなく、共通で1つの実験管理サービスをチームやゼミで使い回せたらそれでいいのです。早速始めましょう。

Architecture

今回はGCPを想定し、その全体構成図がこちらです。MLflowをGKE上に展開し、ユーザからのアクセスはCloud Identity Aware Proxyを通すことで、Whitelist形式のユーザ認証を実現します。更にIngressによるLoad BalancingによりUserやProject単位でMLflow Serviceごと分離します。依存するDBとStorageのInstanceとBucketは共通で持ちますが、DatabaseやStorage pathは前述したUserごとに分けます。これによりインフラリソースを必要最小限に保ちつつ、ユーザの実験管理スペースが干渉しない形を目指しています。これはEconomicsやCV、NLP、HCIと研究領域が多岐にわたるAI Labをベースにしているため、読者の皆さんの環境ではこの限りではなく、Userごとに分離しないシンプルな1 MLflow Serviceも良いと思います。

ハンズオン

ここからはTerraformの実コードを通じて、GCP上に前述した実験管理クラスタを立ててみます。コストの概算ですが、ベースがGKEとStorage、Cloud SQLで$76/month、ユーザ1人増えるたびに、+$10です。

https://cloud.google.com/products/calculator#id=5bffe958-d86b-43d7-844f-162bae9675fe

利用人数によっては可用性を下げ、Cloud SQLをServerのPersistent Diskに変更することで$76/month → $14/monthくらいに落とすことができます。

https://cloud.google.com/products/calculator#id=90e6e7bf-2b6b-48e0-964c-90c6e8e4eda5

値段感もわかったところで、GCPのProjectを用意しましょう。このハンズオンに使ってもらうのはおこがましいですが、初GCPの方は$300の無料トライアルクレジットが付与されるのでそれを利用しても良いでしょう。ここからはGithubのコードを使って進めますので適宜cloneして参照してください。

https://github.com/chck/mlflow-k8s

[mlflow-k8s] tree .
.
├── Dockerfile
├── Makefile
├── README.md
├── docker-compose.yml
├── infra
│   ├── 01_setting.tf
│   ├── 02_serviceaccount.tf
│   ├── 03_gke.tf
│   ├── 04_deployment.tf
│   ├── 05_network.tf
│   ├── 06_iap.tf
│   ├── 07_storage.tf
│   ├── 08_database.tf
│   ├── manifests
│   │   ├── backend_config.yaml
│   │   └── managed_certificate.yaml
│   └── secrets
├── poetry.lock
├── pyproject.toml
└── scripts
    ├── Makefile
    ├── poetry.lock
    ├── pyproject.toml
    └── tracking_sample.py

依存ツールのインストール

まずはREADME.mdにあるように、必要な依存をインストールしましょう。Macの例で申し訳ありませんが、LinuxやWindowsでも似たような方法で揃えられるはずです。

OAuthの設定

Architectureの説明で、「ユーザからのアクセスはCloud Identity Aware Proxyを通すことで、Whitelist形式のユーザ認証を実現」とあったと思いますが、その前提のOAuthの設定です。Cloud IAPは、GCPが用意しているOAuth認可によるProxyで、そのhostへアクセスした際にGoogle Loginが求められ、許可されたGoogle Accountでない限りはアクセスできないという仕組みです。

まずはOAuth同意画面を設定します。GCP Projectで1つなので、既に設定してある場合は飛ばしてもらって構いません。https://console.cloud.google.com/apis/credentials/consent から、EmailやApplication nameを入力し、保存すればOKです。なお、ここで設定した情報がApplicationの認証ページに表示されるため、公開情報のみ入力してください。

OAuth Credentialsのページに移動します。 
Create credentials → OAuth Client ID → Application type → Web applicationから、名前をmlflowとし、Createをクリックします。OAuth Client ID とClient Secretが生成され、OAuth Clientの画面に表示されます。
OK をクリックし、作成したClientを選択します。Client ID をクリップボードにコピーします。
Authorised redirect URIs フィールドに次の形式でリダイレクト URL を追加します。
https://iap.googleapis.com/v1/oauth/clientIds/CLIENT_ID:handleRedirect
ここで、CLIENT_ID は先程コピーした OAuth Client ID です。
ページの上部にある DOWNLOAD JSON ないしダウンロードアイコンをクリックし、JSONをダウンロードします。

このJSONはIAPの設定時にTerraformから参照するため、infra/secrets フォルダに oauth_client_secret.json という名前で置いておきましょう。なお、このフォルダは.gitignoreに指定しているので、mlflow-k8s repoで試す場合にcredentialsがgit remoteに上がる心配はありません。

ドメインの準備

Cloud IAPによるユーザ認証にはSSL/TLSが必要なので、任意のドメイン取得サービスから独自ドメインを取っておきましょう。会社や研究室でドメインが割り当てられている場合や既にドメインを持っている方は、そちらを使えば大丈夫です。本稿では仮にmlflow.sandbox.devとします。

Terraformの設定

次に、Terraformを介してGCPを操作するためのService Accountを作成します。TerraformとはGCPやAWSといったインフラの構築をコードで管理できるHashiCorp社製のOSSです。ポチポチで構築した場合と比べ、構築手順の再現性や設定の可視性という点で優位なため、今回はこちらを利用していきます。用意していただいたGCPの画面の左側のメニューからIAM & Admin → Service Accountsと進み、Create service accountから、Terraform用のService Accountを作成しましょう。本稿では mlflow-terraform と名付けますが、目的や所属によってxxx-terraformなど、何を司るaccountなのかがわかる名前にすると良いでしょう。

 

無事に作成できたらService Account 一覧に出てくるので、右の︙からManage keys → Add key → Create new keyからJSON形式のPrivate keyをDownloadします。

これをTerraformから参照することで、GCPの各サービスへのアクセスを可能にします。Privateとあるように、GCPへのアクセス権のあるKeyなのでPublicには上げないようにしましょう。このService Accountにはまだ何のRoleも付与していないので、Terraformが期待するRoleをIAMとして付与します。Service Accountとして発行されたEmail(mlflow-terraform@your-project-id.iam.gserviceaccount.com)に、IAM → Add から EditorとSecurity Admin の 2 Roleを付与します。ハンズオン的に簡単のため、この2つを付与していますが、Roleは必要最小限が基本なので、強すぎると思った方は適宜調整してください。

 

ここでREADME.mdに戻ります。Get Startedという項の通りに、.env.exampleをcopyして.envを編集します。

まずYOUR_GCP_PROJECTは自身のGCPのProject IDに置き換えます。
GCPのProject IDは、https://console.cloud.google.com/home/preview?project=your-project-id のproject=以下の部分のように、GCP ProjectのURLやGCPのDashboardから確認することができます。GOOGLE_APPLICATION_CREDENTIALS=/path/to/gcp-terraform-credentials.jsonには先程ダウンロードしたJSONの絶対パスを記述します。
TF_GCS_BUCKETについて、Terraformでは管理下のインフラの状態をstateとして持ち、構築時にはstateとの差分更新という戦略を取るのですが、今回そのstateの置き場所にGCSを使います。Bucketは何でもいいのですが、慣習的にbucketには-tfstateというsuffixをつけ、YOUR_PREFIXの部分はmlflowなり、mlflow-k8sなりに置き換えてください。(TF_GCS_BUCKET=YOUR_PREFIX-tfstate)
MLFLOW_GCS_BUCKETは、Terraformのstateが置かれるTF_GCS_BUCKETとは異なり、MLflowで実験管理する上でArtitectと呼ばれる画像などのバイナリ置き場になります。別のバケット名を命名しましょう。
MLFLOW_DOMAINには、ドメインの準備の項で用意した各々のドメインを記述します。本稿ではmlflow.sandbox.devとしています。

ここまで変更できたらREADMEで入れたdirenvの direnv allow . コマンドによって.envに定義した環境変数をそのディレクトリ下にいる間、自動で読み込むことができます。export要らずで便利ですね。

次に、Terraformの実体であるtfファイルに皆さんに用意して頂いた環境変数依存な部分があるので、以下のコマンドで置換します。Makefile内にmake tfコマンドの実体がありますが、内部で使っているsedのsyntaxはGNU sedではなくMac sedなので、お手元の環境によっては差し替えてください。

make tf

make tf までできたらTerraformを動かす前に、前述したterraformのstateを管理するために、GCSのBucketを作ります。Makefileに作成処理をまとめているので次のコマンドを叩きましょう(依存ツールのインストール項、要Google Cloud SDK)。

make bucket

無事にBucketが作られたら、infraフォルダに移動し、Terraform コマンドを叩いてみましょう。Terraformの依存packageとstateファイルの準備が行われます。

cd infra
terraform init

stateファイルができたところで、インフラ構築の実行計画を以下のDry-Runコマンドで確認します。

terraform plan

30近いリソースがAddされることがわかります。これらはinfra以下の各tfファイルを参照しており、各リソースの設定が記述されています。あとは以下の実行コマンドを叩いて内容を確認後console上でyesを入力し、数十分待つだけで実験管理クラスタが立ち上がります。

terraform apply

初回実行時に、以下のようにAPIが無効なエラーが出た場合は、ログで案内されているURLから有効化し、再度applyコマンドを実行してください。おそらくCloud DNS、Cloud SQLあたりで出ると思います。

Error: Error, failed to create instance mlflow-k8s: googleapi: Error 403: Cloud SQL Admin API has not been used in project xxx before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/sqladmin.googleapis.com/overview?project=xxx then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry., accessNotConfigured

また、GKEクラスタが立ち上がったら Makefileのあるdirectoryで make creds を叩くとローカルにGKEクラスタの設定を読み込んでくれるので、kubernetesに面識がある方はkubectlコマンドからdebugも可能です。ネットワーク周りが怪しい場合はGKEに割り振られたPublic IPとDNSの紐付けなど、用意したドメインのDNS周りの設定を見直してみましょう。Timeoutになった場合は、インフラが立ち上がりきっていない可能性があるので、数十分待ってから再度applyコマンドを実行してみてください。

Terraformによるインフラ構築を待っている間、各リソースごとにポイントを紹介します。はじめに、Terraformファイルの読み方を簡単に説明します。.tf拡張子が対象で、ファイルの分け方に厳密なルールはありませんが、今回はわかりやすくするためにGCPのサービスごとに分けています。

[mlflow-k8s] tree infra
infra
├── 01_setting.tf
├── 02_serviceaccount.tf
├── 03_gke.tf
├── 04_deployment.tf
├── 05_network.tf
├── 06_iap.tf
├── 07_storage.tf
├── 08_database.tf
├── manifests
│   ├── backend_config.yaml
│   └── managed_certificate.yaml
└── secrets
    └── oauth_client_secret.json

01_settings.tfを開いてみましょう。

terraform {
  required_version = "~> 1.0.0"

  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 3.77.0"
    }
    google-beta = {
      source  = "hashicorp/google-beta"
      version = "~> 3.77.0"
    }
    local = {
      source  = "hashicorp/local"
      version = "~> 2.1.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.1.0"
    }
    kubectl = {
      source  = "gavinbunney/kubectl"
      version = "~> 1.11.1"
    }
    postgresql = {
      source  = "cyrilgdn/postgresql"
      version = "~> 1.13.0"
    }
  }

  backend "gcs" {
    bucket = "_YOUR_BUCKET"
    prefix = "mlflow-k8s"
  }
}

variable "common" {
  type = map(string)
  default = {
    project = "_YOUR_PROJECT"
    region  = "asia-northeast1"
    zone    = "asia-northeast1-c"
  }
}

locals {
  common = var.common
  users  = var.users
}

variable "users" {
  type = set(string)
  default = [
    "user1", "user2", "projectz",
  ]
}

provider "google-beta" {
  project = local.common.project
}

provider "google" {
  project = local.common.project
}

JSONのような構文で、terraformやvariable、locals、providerといったkeyのようなものがterraformの予約語です。01_settings.tfでは、その名の通りTerraformの設定や依存、全体で使いそうな変数をまとめて定義しています。terraform句で操作したいリソースに必要なパッケージをVersion付きで指定します(requirements.txtのようなもの)。今回はGCPでMLflowを動かしたいという目的なので、googleやkubectl、postgresqlといった依存を加えています。backend “gcs” という部分は、Get Startedで説明したterraformのstateファイルの置き場所を指定しています(AWSでやるならbackend ”s3”ですね)。

variable句はterraform内で参照できる変数の定義で、typeによってhashmap型だったりset型など自由に記述できます。ここではGCPのProject IDやregionの他、複数人でのMLflowを想定しているので、そのUser IDをまとめています。参照するときにはvar.common等で呼べる他、locals句を挟むことでaliasのように、local.commonで呼べるようになります。今回の例ではlocalsの恩恵は少ないのですが、例えばdevとprdで変数だけ一部変更して同じインフラ構成にしたい場合、以下のように記述すると、TerraformのWorkspace機能を利用して1つのterraformファイルで挙動を変えることができるようになります。

variable "common" {
  type = map(map(string))
  default = {
    dev = {
      project = "_YOUR_DEV_PROJECT"
      machine_type = “e2-micro”  # コスパ重視
    }
    dev = {
      project = "_YOUR_PRD_PROJECT"
      machine_type = “e2-highmem-2”  # 性能重視
    }
  }
}

locals {
  common = var.common[terraform.workspace]
  users  = var.users
}

次に、07_storage.tfを開いてみましょう。

resource "google_storage_bucket" "artifact" {
  name          = "_YOUR_MLFLOW_BUCKET"
  location      = "ASIA"
  storage_class = "MULTI_REGIONAL"
}

このresource句が実際に操作を行う構文で、Terraform側で用意されている “google_storage_bucket” resourceに対して、”artifact” という任意の名前をつけています。このresourceが必要とする引数(name、location、storage_classなど)を元に、Storageが作成されます。GCPのストレージ作成画面から必要項目を入力するようなイメージです(基本的にフォーム入力とGCloud SDK、Terraform Resource間の引数が対応しているはず)。

 

tfファイルには01_から08_とprefixをつけているものの、Terraform的には01から逐次的に実行されるわけではなく、terraform planやterraform apply時に配下のtfファイルをまとめて読み込んだ上で実行順を決定してくれます。

Terraformの概要が説明できたところで、ポイントを端折りつつ内容の解説に入ります。まずは02_serviceaccount.tfの resource “google_service_account_iam_binding” “wi” の roles/iam.workloadIdentityUser についてです。

resource "google_service_account_iam_binding" "wi" {
  service_account_id = google_service_account.sa.name
  role               = "roles/iam.workloadIdentityUser"
  members            = formatlist("serviceAccount:${local.common.project}.svc.id.goog[%s]", ["${var.deployment.k8s_namespace}/${var.deployment.ksa_name}"])
}

GKE上のNodeからManaged DBなどの他のGCP Serviceにアクセスするためには、a)対象Instanceを同一VPCネットワークに置く方法、b)Service AccountのCredentialをNodeに置いて認証、c)Workload Identityによる認証、の大きく3パターンがあります。設定の煩雑さやセキュリティ上の観点から、Googleはc)Workload Identityを推奨しているため、本稿ではその方法を取ります。Workload Identity自体の説明は本稿の目的と離れてしまうため割愛します。

次に、03_gke.tfの resource “google_container_node_pool” “preemptible_nodes” のnode_config内、preemptible = trueです。

node_config {
    preemptible  = true
    machine_type = var.gke.machine_type
    disk_size_gb = var.gke.disk_size_gb
...

GCPのPreemptible Instanceとは、従来のinstanceと比べ24時間以内に1度は落ちる等、可用性を落とすことで最大8割OFFという格安で利用できるinstanceです。安定性が求められるようなProduction Serviceでの利用は向いていませんが、今回のような用途ではコスパ最強です。GCPのマネージドサーバーにはGKEの他にApp EngineやCloud Runがありますが、唯一このpreemptibleが利用できるGKEを選ぶ利点の一つにこれがあります。[2]

04_deployments.tfの resource “kubernetes_deployment” “mlflow” です。Kubernetes Clusterを構成するMLflow Containerの実体を定義しています。

resource "kubernetes_deployment" "mlflow" {
  for_each = local.users
  depends_on = [
    google_container_cluster.primary,
    kubectl_manifest.backend_config,
    kubernetes_service.internal,
  ]
  metadata {
    name = "mlflow-${each.value}"
  }
  spec {
    selector {
      match_labels = {
        tier = "mlflow-${each.value}"
      }
    }
    template {
      metadata {
        labels = {
          tier = "mlflow-${each.value}"
        }
      }
      spec {
        service_account_name = kubernetes_service_account.ksa.metadata[0].name
        node_selector = {
          "iam.gke.io/gke-metadata-server-enabled" = "true"
          "cloud.google.com/gke-nodepool"          = google_container_node_pool.preemptible_nodes.name
        }
        container {
          name  = "mlflow"
          image = "chck/mlflow:1.17.0-patch1"
          args = [
            "--static-prefix=/${each.value}",
            "--backend-store-uri=postgresql://$(DB_USER):$(DB_PASS)@$(DB_HOST)/u_${each.value}",
            "--default-artifact-root=gs://${google_storage_bucket.artifact.name}/${each.value}/mlruns",
            "--gunicorn-opts=--worker-class=gevent --access-logfile=- --timeout=180 --log-level=debug",
          ]
          port {
            container_port = 5000
          }
          env_from {
            secret_ref {
              name = kubernetes_secret.mlflow_secret.metadata[0].name
            }
          }
          readiness_probe {
            http_get {
              path = "/health"
              port = 5000
            }
          }
        }
        container {
          name    = "cloud-sql-proxy"
          image   = "gcr.io/cloudsql-docker/gce-proxy:1.23.1-alpine"
          command = ["/cloud_sql_proxy", "-instances=${var.common.project}:${var.common.region}:${var.database.instance_name}=tcp:5432"]
          security_context {
            run_as_non_root = true
          }
        }
      }
    }
  }
}

2行目の for_each = local.users により、for_eachのついたresourceはlocal.usersの分だけ繰り返され、結果的に各user(今回はuser1, user2, projectz)のresource “kubernetes_deployment”を1度に作成できます。

参照するdocker imageは、MLflow Tracking Serverにstatic-prefixというserver rootを変更するoptionを付けて立ち上げた場合に、mlflow clientからapiが届かない問題を解消するpatchを当てたものです。–static-prefix=/user1 という引数がuser毎に切り分ける実験管理クラスタの実験スペースの1pathを示します。backend-store-uriとdefault-artifact-rootはMLflowの依存middlewareを指定していて、DB_USERなど環境変数化されている部分は、env_from.secret_ref 項のkubernetes_secretからセキュアに参照されます。

同deployment上にもう一つ、cloud-sql-proxyというcontainerも定義されています。

container {
  name    = "cloud-sql-proxy"
  image   = "gcr.io/cloudsql-docker/gce-proxy:1.23.1-alpine"
  command = ["/cloud_sql_proxy", "-instances=${var.common.project}:${var.common.region}:${var.database.instance_name}=tcp:5432"]
  security_context {
    run_as_non_root = true
  }
}

これはMLflowのbackend-store-uri、DBとして利用するCloud SQLへの接続を、Workload Identityとこのproxyによって実現しています。[3]

次に 05_network.tfの resource “kubernetes_ingress” “mlflow” です。

resource "kubernetes_ingress" "mlflow" {
  depends_on = [
    google_container_cluster.primary,
    google_compute_global_address.mlflow_ip,
    kubectl_manifest.managed_certificate,
  ]
  metadata {
    name = "mlflow-ingress"
    annotations = {
      "kubernetes.io/ingress.allow-http"            = "false"
      "kubernetes.io/ingress.global-static-ip-name" = google_compute_global_address.mlflow_ip.name
      "networking.gke.io/managed-certificates"      = var.network.certificate_name
    }
  }
  spec {
    rule {
      http {
        dynamic "path" {
          for_each = local.users
          content {
            path = "/${path.value}"
            backend {
              service_name = "service-${path.value}"
              service_port = 5000
            }
          }
        }
        dynamic "path" {
          for_each = local.users
          content {
            path = "/${path.value}/*"
            backend {
              service_name = "service-${path.value}"
              service_port = 5000
            }
          }
        }
      }
    }
  }
}

Architectureの説明に、「ユーザからのアクセスはCloud Identity Aware Proxyを通すことで、Whitelist形式のユーザ認証を実現」とあったと思いますが、その定義がこちらです。再度の説明になりますがIAPは、GCPが用意しているOAuth認可によるProxyで、許可されたGoogle Accountでない限りはアクセスできないという仕組みです。GKEクラスタの管理側はUI上でアクセス可能なUserないしGoogle GroupsによるメーリスをIAP-secured Web App Userとして適宜追加することができます。

data "kubectl_path_documents" "backend_config" {
  pattern = "./manifests/backend_config.yaml"
  vars = {
    backend_config_name = var.iap.backend_config_name
    secret_name         = var.iap.secret_name
  }
}
resource "kubectl_manifest" "backend_config" {
  depends_on = [
    google_container_cluster.primary,
  ]
  count     = length(data.kubectl_path_documents.backend_config.documents)
  yaml_body = element(data.kubectl_path_documents.backend_config.documents, count.index)
}

 

今回のGKEクラスタのKubernetesリソースへのアクセス順はざっくり、Ingress(SSL, LoadBalancing) → Service(IAP) → Node(MLflow Container)ですが、そのKubernetes Service(IAP) に紐付けるために予め作成しておいたOAuth用のclient.jsonをkubernetes backend configに登録しておきます。04_deployment.tfや05_network.tfでは resource “kubernetes_deployment” や resource “kubernetes_ingress” などのTerraform builtinのresourceを用いて定義しましたが、backend configはTerraform由来のresourceが用意されていないため、kubernetes従来のyamlによる定義を行います。これは有志によるkubectlのTerraform Providerを利用していて、Terraformはこのように必要に応じてPluggableにProviderをUserが実装できるのも魅力です。

以上でtfファイルの説明は終わりです。さて、そろそろ terraform apply も終わりMLflowクラスタが構築されたか確認してこの記事を締めます。まずは 指定したdomainを含めたURLである $MLFLOW_DOMAIN/user1 にブラウザからアクセスし、MLflowのTop画面が表示されるかを確認しましょう。MLflowの画面ではなく、以下のようなBlocking画面が出たらIAPが働いている証拠なので心配無用です。

https://console.cloud.google.com/security/iap からBackend Services → service-user1 をクリックし、右に現れるメンバー編集UIから今自身がアクセスしているGoogle AccountをIAP-secured Web App Userとしてメンバー追加しましょう。追加できたら反映されるまで一息待ってから再度元のURLにアクセスし、以下のようなMLflowの画面が現れたら成功です。

実験結果を共有する側のメンバーはGoogle Groupsのメーリスなりでまとめておいて、そのメーリスのアドレスをIAP-secured Web App Userに指定すると、グループに対してアクセス権を付与できるので便利です。共有設定はこれでOKで、ログの飛ばし方を説明します。ブラウザ表示とは別のアクセスルートを辿るため、IAPを許可する情報も新たに設定します。

アクセス用Service Accountの設定

先程TerraformからGCPを操作するためのService Accountを設定しましたが、今度はMLflow ClientからServerにアクセスするためのService Accountを作ります。まずは同じようにIAM -> Service Accounts -> Create service accountと進み、mlflow-k8s という名前で作成します。roleは、ArtifactのUploadのためにStorage Object Creator を付与します。更に、Cloud IAPの設定ページから default/service-user1 に対し、該当のService AccountにIAP-secured Web App User を付与しましょう。

そして同じようにService Account 一覧の︙からManage keys → Add key → Create new keyからJSON形式のPrivate keyをDownloadし、その絶対パスをメモしておきます。

実験ログのトラッキング

ここまでできたらmlflow-k8sのscriptsフォルダに移動します。

cd scripts

必要情報が3つあるので.env.exampleを参考に、.envに環境変数を追加します。mlflow-k8s rootの.envとは別物なので注意しましょう。

cp .env.example .env

vi .env

=================
MLFLOW_CLIENT_ID=YOUR_MLFLOW_CLIENT.apps.googleusercontent.com
MLFLOW_TRACKING_URI=https://${YOUR_MLFLOW_DOMAIN}/user1/
GOOGLE_APPLICATION_CREDENTIALS=YOUR_LOCAL_CREDENTIAL_PATH

MLFLOW_CLIENT_IDには、terraform設定時にセットしたinfra/secrets/oauth_client_secret.json内のweb.client_idをコピペしましょう。MLFLOW_TRACKING_URIは、今回DeployしたMLflowのURL/user1/に、GOOGLE_APPLICATION_CREDENTIALSは先程メモしておいたToken key.jsonの絶対パスを使います。.envが設定できたら、依存をインストールし、tracking_sample.pyを実行してみます。

direnv allow .
poetry install
poetry run python tracking_sample.py

エラーなく実行されたら[4]、MLflowの画面に戻って、clientからlogが飛ばせているか確認して、この記事を締めたいと思います。tracking_sample.pyを動かす前と比べ、送ったログが表示されていることがわかります。

終わりに

今回対象にした実験管理は、ML Lifecycleにおけるほんの一部分です。データやモデル、パイプライン自体の共通管理については次回にご期待ください。この設計は一例なので、各々のライフスタイルに合わせてカスタマイズし、快適な実験生活を送りましょう。なお、ハンズオンをここまでお試し頂いた方はありがとうございました。大体理解できたのでインフラを一旦消したい、という場合は terraform destroyでapplyしたリソースをまとめて削除できます。PyData.Tokyoを通じてAI LabのMLOps事例として本件も解説している、CyberAgent AI Labを支えるCloud実験環境もぜひご覧ください:)


  1. 前回の記事で少しだけ紹介しました ↩
  2. 今回はpreemptible nodeで安く長くをテーマにしていますが今年出たばかりのGKE Autopilotも気になります ↩
  3. この方針だとコネクションがPod数だけ増えて気になる場合は、Service経由で接続するように変更しても良いでしょう(参考: https://unching-star.hatenablog.jp/entry/2019/02/22/000154) ↩
  4. 権限周りのエラーが出た場合は、GOOGLE_APPLICATION_CREDENTIALSで指定しているService Accountのroleを見直してみてください ↩
]]>
新規プロダクトでKubernetesを中心にCloudNativeなアーキテクチャのインフラを導入した話 https://cyberagent.ai/blog/pr/kubernetes/10219/ Mon, 26 Aug 2019 13:44:39 +0000 https://cyberagent.ai/techblog/?p=4500 はじめに

アドテク本部Airtrackチームの横山(@nnao45)です。

チーム内ではScala書いたり〜K8Sと遊んだり〜AWSったり〜しています。
この度Airtrackチームの新規プロダクトでKubernetesを採用し、本番環境に投入したのでその知見を共有させた頂きます。

おしながき

  • 目指したアーキテクチャ
  • Kubernetes周り
  • ミドルウェア
  • CI/CD
  • 監視
  • 感想

目指したアーキテクチャ

妥協はしないが身の丈にあったコンテナプラットフォーム って感じですかね。

Kubernetes周り

サービスレイヤひとまわり

Airtrackチームで広告系完全新規プロジェクトの立ち上げに
Kubernetes中心にクラウドネイティブなアーキテクチャ意識して設計しました。

描れている4つの六角形のやつが今回僕らの広告系のプロダクトでサービスとして動かしているサーバで、このうち2つをフロント、2つをサーバサイドの関係にあります。

このサービスを中心に組み立てるにあたって設計で迷った次の点をまずは紹介させて頂きます。

お悩みポイント

  • 結構スモールスタートだけど、なんでk8sで採用したの?
  • KubernetesのサービスはIngress?Nodeport?
  • ログの置き場所をどうするか in AWS

結構スモールスタートだけど、なんでk8sで採用したの?どうだった?

とりあえず全く新規で始めるので、ワークロードはコンテナで考えたかったのは間違いないです。インフラの依存性を安全に切り離せるのはもちろん、特にDockerはアプリをリリースするのとインフラの(上澄みの)ライフサイクルを併せられるので、アプリエンジニアの手に馴染みやすいのも大きな利点です。

そしてAirtrackではほぼ全てのコンポーネントがAWS上にあり、必然的にECS Fargateも選択肢に入ってきました。
正直、ぶっちゃけFargateでも良かったです。EKS vs Fargateは以下のブログが詳しい。

https://www.bmc.com/blogs/aws-ecs-vs-eks/

ALBからpodに落ちるまでのホップ数(EKSというかKubernetes)や、他のAWSサービスとの連携などはFargateの方が良いでしょうね。

結果的にEKSを採用した理由として、

  • OSSコミュニティの盛り上がり、拡張性の強さ。
  • そこまでレイテンシにシビアなサービスではなかった
  • @nnao45君がKubernetesの方が圧倒的に慣れてた、また他のメンバーのKubernetesへの勉強も進んでいて、社内事例も多かったので。
  • 採用面で強く出れそう。なお、AirtrackチームではKubernetesをバリバリ触ってみたいエンジニアを絶賛募集していまs(ry
  • 将来的にオンプレやGKEへの移行という話になってもスムーズに行きそう & 他のチームでGKEとかオンプレk8s動かしてた人も入りやすくなりそう

という少々生々しい感じで、多分性能面というよりかはチームビルディングの過程による色が強かったかもしれません。
また載せたサービス群がそれほど多くないのでK8Sではtoo muchではなんていう懸念もありましたが、
後述するCD面やクラスターの拡張性の面でこの規模でさえK8Sから得られる恩恵は意外に大きかったです。

KubernetesのサービスはIngress?Nodeport?

うーんどっちもどっちって感じでした。
「Ingressはまだ使うな」という論と「alb-ingress-controller強いからイケる」という論があり、難しかったです。
alb-ingress-controllerは僕の予想それでもポツポツと本番投入しているプロダクトは出てきてるんじゃないかな?と思っています

https://github.com/kubernetes-sigs/aws-alb-ingress-controller

annotationでalbの設定を投入できたり、証明書を紐付けたりできるみたいで悪くなさそうですね。
ただ、僕は個人的にはEKSとしてはNodeportによるサービスバインドが優れていると感じました。

1. AWSの世界とKubernetesの世界を分離したかった

これは非常に重要な観点と感じていて、チームとして大きなメリットを得ることができます。
IngressによりALBまで管理することによって「Kubernetesのmanifestに何かしらのアクションをしないとALBができない(=サービスコントロールができない)」という状態になります。

IngressでALBをmanifest管理できるのは確かにメリットかも知れませんが、ALBを「AWSリソースを立てる」という手立てで管理にしておくとVM時代と同様な手順で「AWSが触れる人」でもサービスコントロールできるようになります。
マイグレのような対応をするときにクラスターを複製するときに、ALBも複製されたり色々面倒だったりします。

2. 適当なローカルクラスタを作りやすくなる。

例えばminikubeで本番環境と全く同じ検証環境を作成したくなっても、VMとDBだけ用意すればよく、そしてVMとDBは頑張れば1個の物理マシンで済ませられるので検証に便利です。
クラウドベンダのLBがあること前提の環境にしてしまうより差分が少なくなるはずです。

ログの置き場所をどうするか in AWS

当然標準出力だろという話に思えた。

https://12factor.net/logs

しかし、ローンチ時点でサーバサイド内で分析したいログとそうでないログがあり、それをログの出力内容としてjson化してタグで分別するのが厳しそうでした。
よって分析ログだけ物理ログとしてどっかに置いておいてfluentdで飛ばそうとなりました。しかし・・・。

AWS EKS, Volumes architecture in a statefull app. in multiple AZ’s

単純にEBSマウントしログを置くとAZ跨ぎで実装することが出来ません。
今回、僕らの環境ではマルチAZな構成にしているのでこれにぶち当たってしまいました・・・。

マルチAZ構成でのPersistent VolumeはEFSによって構成すれば、マウントするpodがどこのAZにいても大丈な構成にしました。
今後、このEFSがAZをまたぐ構成がクリティカルな負荷になる可能性はありますが、その時はnode taintも視野に入れて採用に至りました。

しかし・・・・・・

https://github.com/kubernetes-incubator/external-storage/tree/master/aws/efs

なんとこの公式のEFS Dynamic Provisioner、動かない >< 理由がわからん。
頑張ってググったところ、helmの方が動くようなので、此方のEFS Dynamic Provisionerを使用しました。

https://github.com/helm/charts/tree/master/stable/efs-provisioner

これにより、分析ログはこのEFS上に配置してfluentdでtailすることによって解決しました。

最後に注意点ですが、こういうkubelet配下にないログの場合はローテーションを考えなければいけません。

今回の構成では他のプロダクトでも使っているJavaの伝統的なログマネージメントライブラリのlogbackを使って実現しています。

ミドルウェア

ミドルウェア周りは大量にあるので、特筆点のみ紹介します。

Aurora(データ移行)

リレーショナルなデータベースにはAuroraを選びました。強いからです。

https://aws.amazon.com/jp/rds/aurora/
インフラ観点で特別な事はしてないのですが、今回特筆する点といえばproduction→stagingへのデータ移行の点でしょうか。

今回の環境では結構インプレッションの上下が激しく、データに関してもstagingはproduction同等ものを用意しました。
ここに関してはそのうち①いつでも発火できるジョブ②早朝に回るバッチとしてdockerコンテナがrunする仕組みをkubeのCronJobで実現する予定です。

docker自体はシェル叩くだけのコンテナですが、Auroraでは叩くシェルも非常に単純なシェルで実現できます。
何と、ほぼ全てのデータ移行がSQL4行で終わっています。

# テーブル名を全部持ってくる。
SHOW tables FROM ${DATABASE_NAME}
# テーブルごとにproductionのデータをs3に保存する。
SELECT * FROM ${DATABASE_NAME}.${TABLE} INTO OUTFILE S3 's3://${S3_AURORA_BUCKET}/${TABLE}/table' OVERWRITE ON
# テーブルごとにstagingのテーブルをTRUNCATEする。
TRUNCATE TABLE ${DATABASE_NAME}.${TABLE}
# テーブルごとにstagingのテーブルをs3からロードする。
LOAD DATA FROM S3 PREFIX 's3://${S3_AURORA_BUCKET}/${TABLE}/table' INTO TABLE ${DATABASE_NAME}.${TABLE}

このS3にSELECT文のデータを送り、LOADするのはAuroraバージョンv1.13から使えるんだそう。

ネットでは色々情報が錯綜してますが、今回使用したAurorav2.03系でもこの機能は(上記のクエリを発行するだけなら)使えます。
そんなとこです。

Botkube

僕が気に入ってるミドルウェアの一つにbotkubeがあります。

https://github.com/infracloudio/botkube

僕自身コントリビューションもしているので是非使ってみてください。

このツールはslackからreadonlyなオペレーションをchatで投げれるのと、kubernetesのイベント監視をしてくれます。小回りがきいて便利です。

 

bokube君のおかげで、kubectlをslackからクラスタに投げたり、getコマンドをチームで共有、履歴保存できて重宝しています。

Fluentd

fluentdですが、野良プラグインが使いたかったりしたので、今回は野良レポジトリのを応用してビルドしております。

fluent-plugin-docker

FROM fluent/fluentd:latest-onbuild
WORKDIR /home/fluent
COPY /entrypoint.sh /home/fluent/entrypoint.sh
ENV PATH /home/fluent/.gem/ruby/2.3.0/bin:$PATH

# We need root access to read container logs
USER root
RUN apk --no-cache --update add build-base ruby-dev && \
gem install fluent-plugin-s3 fluent-plugin-docker && \
rm -rf /root/.gem/ruby/2.3.0/cache/*.gem && gem sources -c && \
apk del build-base ruby-dev && rm -rf /var/cache/apk/*

ENTRYPOINT ["sh", "entrypoint.sh"]

<追記 20190828>

↑の fluent/fluentd:latest-onbuild は古いイメージを使っているとご指摘いただいたので、asspで直しました😌いんたーねっつはあったけえ

FROM fluent/fluentd:v1.7-debian-1
WORKDIR /home/fluent
COPY entrypoint.sh /home/fluent/entrypoint.sh
ENV PATH /home/fluent/.gem/ruby/2.3.0/bin:$PATH

# We need root access to read container logs
USER root
RUN buildDeps="sudo make gcc g++ libc-dev" && \
    apt-get update && \
    apt-get install -y --no-install-recommends $buildDeps &&  \
    sudo gem install fluent-plugin-s3 && \
    sudo gem install fluent-plugin-docker && \
    sudo gem sources --clear-all && \
    SUDO_FORCE_REMOVE=yes \
       apt-get purge -y --auto-remove \
                     -o APT::AutoRemove::RecommendsImportant=false \
                     $buildDeps && \
    rm -rf /var/lib/apt/lists/* && \
    rm -rf /tmp/* /var/tmp/* /usr/lib/ruby/gems/*/cache/*.gem

ENTRYPOINT ["sh", "entrypoint.sh"]

参考:https://github.com/fluent/fluentd-docker-image

</追記 20190828>

参考:https://github.com/ccpgames/kubernetes-fluentd-s3

余談ですが、コンテナで動かすfluentd_confを書く場合は、環境変数を埋め込める記法を覚えておくとセキュアです。

<match **>
  @type s3
  log_level info
  s3_bucket "#{ENV['S3_LOGS_BUCKET_NAME']}"
  s3_region "#{ENV['S3_LOGS_BUCKET_REGION']}"
  s3_object_key_format %{path}%{time_slice}/cluster-log-%{index}.%{file_extension}
  path "#{ENV['S3_LOGS_BUCKET_PREFIX']}"
  buffer_path /fluentd/log/s3-buffer

  flush_interval 60s

  time_slice_format %Y/%m/%d
  time_slice_wait 10m
  utc

  <instance_profile_credentials>
    ip_address 169.254.169.254
    port       80
  </instance_profile_credentials>

</match>

#{ENV['<環境変数名>']} を使うことによって、環境変数から引っ張ってこれます。

Gatling as a Service

少し番外編として負荷試験ツールも紹介しておきます。

以下の理由でgatlingをas a serviceとして作りたいって思いたちました。

  • 負荷試験サーバってtoo fatの金食い虫
  • 別にずっと流す必要がない
  • 環境が汚れがち

つーことで作りました。

構成としては図にするまでもなかったのですが、Codebuild + Gatling + Dockerです。

つまりCodebuildでdocker runするだけです。

他のAWSのサービスではなくCodebuildでGatlingを実行する理由ですが、

  • lambdaはログがだるい
  • aws batchみたいにECSサービスを立ち上げるまでもない
  • aws barchみたいに定期的に実行したいわけでもない
  • 「dockerで」揮発的にジョブ実行したい(移植可能な状態にしたい)
  • gatling自体がシェルとかsbt越しで実行される事を期待されている

な感じです。

記念に使ったDokerfileを置いておきますが、インストールしてるだけです。

# Gatling is a highly capable load testing tool.
#
# Documentation: https://gatling.io/docs/3.0/
# Cheat sheet: https://gatling.io/docs/3.0/cheat-sheet/

FROM openjdk:8-jdk-alpine

# working directory for gatling
WORKDIR /opt

# gating version
ENV GATLING_VERSION 3.0.3

# create directory for gatling install
RUN mkdir -p gatling

# install gatling
RUN apk add --no-cache --update tzdata zip curl wget bash libc6-compat && \
  mkdir -p /tmp/downloads && \
  wget -q -O /tmp/downloads/gatling-$GATLING_VERSION.zip \
  https://repo1.maven.org/maven2/io/gatling/highcharts/gatling-charts-highcharts-bundle/$GATLING_VERSION/gatling-charts-highcharts-bundle-$GATLING_VERSION-bundle.zip && \
  mkdir -p /tmp/archive && cd /tmp/archive && \
  unzip /tmp/downloads/gatling-$GATLING_VERSION.zip && \
  mv /tmp/archive/gatling-charts-highcharts-bundle-$GATLING_VERSION/* /opt/gatling/ && \
  rm -rf /tmp/*

# setup TZDATA
RUN cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    echo "Asia/Tokyo" > /etc/timezone

COPY entrypoint.sh /opt/gatling/entrypoint.sh
COPY user-files /opt/gatling/user-files

# change context to gatling directory
WORKDIR  /opt/gatling

# set environment variables
ENV PATH /opt/gatling/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ENV GATLING_HOME /opt/gatling

ENTRYPOINT ["/opt/gatling/entrypoint.sh"]

参考: https://github.com/denvazh/gatling/blob/master/3.0.3/Dockerfile

cloudbuildにbuildが走るとslackに通知してくれてレポートも飛ばしてくれるようにしてます、普通にzipコマンドで固めてcurlで飛ばしてるだけですがこうすると便利なのでオススメです(^^)!

CI/CD

今回の構成では以下のようなCI/CD基盤としました。GitOpsですね(正確には少し違うのだけれど)。

ほぼ図に全て書いてあるので、今回も以下の特筆点だけ。

  • ぼくがかんがえたちょっとつよいぎっとおぷすもどき
  • CircleCIからk8sレポジトリへはプルリク、ただしdevelopmentはマージコミット
  • マニフェスト管理にはkubectlのみ、helm、kustomizeは使わない
  • kubectl発火はCodepipeline/Codebuild

app、k8sどちらもdevとstgとprdを表すブランチへのマージコミットを起点にCI/CDが発火されます。

ぼくがかんがえたちょっとつよいぎっとおぷすもどき

  • 大部分のオペレーションからkubectlの排除
  • アプリを触る人が、Githubだけでリリースが完結する
  • manifestの変更だけでもリリースできる
  • リリースの可否もslackを見ればいい(=デバック以外でkubectlを使わせない)
  • git pushからリリースまで大部分をマネージド、自動化することで開発者へのストレスをゼロに近づける

ほぼほぼこれらは実現でき、非常に好評でした(^_^)v

CircleCIからk8sレポジトリへはプルリク、ただしdevelopmentはマージコミット

最初、ScalaとVueのコンテナのコードのテストだけをCircleCIに任せていたのですが、
ここからLambdaとか何か生やしたりするのが面倒だったのでCircleCIにdocker buildまでさせてECRにプッシュしています。
注意点はECRにイメージがあるかどうか怖いので、CIの最後にそのコンテナのpullにより実在することを保証しています

devはプルリクじゃなくてマージコミットを(=アプリのCIがOKならそのままクラスタに反映)した理由は、単純に「devはリリースタイミングはCIが通ったら行ったらいつでも」であったので、それならプルリクにする必要がなかったからです。

また余談ですが、実はプルリクに工夫をしていまして、何とそのイメージタグにどういうアプリの変更が含まれたかの情報を入れています。

これスミマセン、アプリレポジトリ側のCIの時で使っているシェル芸です…!

BEFORE_MERGED_COMMIT_HASH=$(git log -1 --pretty=format:"%P" | awk '{print $1}')
git log --pretty="%H %s" ${BEFORE_MERGED_COMMIT_HASH}...${CIRCLE_SHA1} --no-color | sed "s@^@https://github.com/xxxx/yyyy/commit/@g" > tee /tmp/commit_list.md

詳しくは長くなるので書きませんがこれは凄い便利なのでこの情報をつけてプルリクを自動生成すると運用する人に凄い喜ばれます✌︎(‘ω’✌︎ )

echo -e "${COMMENT}\n\n## Git History\n$(cat /tmp/commit_list.md)" | hub pull-request -b ${ORG}:${MERGE_BRANCH} -h ${ORG}:${BRANCH} -F -

マニフェスト管理にはkubectlのみ、helm、kustomizeは使わない

これは賛否両論なとこでしょう^^;

個人的に今回の規模であれば、helmなどによるテンプレーティングよりもむしろ単純に kubectl apply -f できない、また反映される実物はレポジトリ内で閲覧できないという点の方デメリットに感じての事です(ちなみにhelm templateでmanifestを持ってきたりしてはいます)。

マニフェストの内容も変えるとしたらイメージタグやレプリカくらいなので、これはこれで良かったと感じています。

またアプリのビルドが成功したらマニフェストのイメージを変更するわけですが、これも実はkubectlコマンドでできます。

kubectl set image \
        -f <イメージを変えたいマニフェスト> \
        <イメージを変えたいデプロイメント>=<変えたいイメージ> \
        --local \
        -o yaml

–localでapiと通信しない、-o yaml でyamlで出力する、これを使う事でsedでシェル芸しなくてもイメージ変更することができます。

 

kubectl発火はCodepipeline/Codebuild

色々考えましたが、AWSメインで要件を満たしていたので採用しました。

ブログを書いてる時点ではcodepipelineによってsourceステージを指定すると、master以外のブランチを指定できるようになるのでcodepipelineからcodebuildを呼び出す構成になっています。

工夫点としては、k8sのmanifest側のディレクトリ構成が

❯❯❯ tree -NC ./
./
├── kubesec
│   ├── hoge_secret.yaml
│   └── piyo_server_secret.yaml
└── manifests
    ├── mayo_dep.yaml
    ├── sio_cm.yaml

みたいにしております。今回kubesecを使っていて、単純にkubectlをぶちかませばいい構成ではないので、最終的に発火しているbuildspec.yml(=dockerのENTRYPOINT)はこんな感じです。

version: 0.2

phases:
  build:
    commands:
       - aws eks update-kubeconfig --name <EKSのクラスター名>
       - kubectl get all --all-namespaces
       - kubectl apply --dry-run -f manifests && kubectl apply -f manifests
       - ls -1 kubesec | while read manifest; do echo '---'; kubesec decrypt kubesec/$manifest;done | kubectl apply -f -
       - kubectl get all --all-namespaces

ポイントは、

  • applyする前に kubectl apply --dry-run してテストしておく。
  • 4行目、シェル芸をかまして kubesec decrypt をかましてから、複数のyamlを一枚の標準出力にするために文頭に --- 入れて〜最後に kubectl apply しています。これでkubesecで kubesec encrypt されて暗号化されたマニフェストをおくだけで展開できます。

そんな感じだわよ

監視

AWSとkubeの監視についてはDatadogに一任をしています。
datadog-agentはhelm templateでmanifestだけ持ってきてkubectlで適用しているだけです。

ダッシュボードですが、datadogはワンクリックでこんな凄いダッシュボードが出来上がります・・・はっきり言ってこれは神

How to monitor Kubernetes + Docker with Datadog

感想

俺たちの旅はまだ始まったばかりだ!!@nnao45、ならびにAirtrackチームの次回作にryって感じで、とりあえずの初期状態は作れたかなと思っております。
プロダクトが大きくなる見込みも十分にあるので、もし大きくなりアーキテクチャを見直すことになったらまた記事を書きたいなと思っております!

]]>
Cluster of Kubernetes Clusters ~connecting palm-sized personal Kubernetes clusters with 100 Raspberry Pi~ https://cyberagent.ai/blog/pr/kubernetes/4340/ Sat, 19 Jan 2019 06:00:50 +0000 https://cyberagent.ai/techblog/?p=4340 Hello.

I’m @amsy810, and I work at CyberAgent, Inc.

 

The era of palm-sized personal Kubernetes clusters is here!

Everyone over the age of a middle school student has a personal Kubernetes cluster, right?

We held our 3 Day Cooking -Raspberry Pi with Kubernetes- event again.

This time, we created a palm-sized personal Kubernetes cluster with a hundred Raspberry Pi.

 

Preparation

Like last time, we purchased a hundred Raspberry Pi and other various components in advance.

 

Checking all of them was hard work.

 

 

Learning

This time, we created 30 physical personal kubernetes clusters with 100 RaspberryPi.

 

 

After that, starting with etcd configuration, and further setting up kube-apiserver, kube-controller-manager, kube-scheduler, kubelet, kube-proxy, flannel, the interns were able to understand Kubernetes architecture on a deep level in these 3 days.

 

Challenges

On the final day, we aggregated the personal Kubernetes clusters.

 

Captain Kube’s chivalry is something to behold.

And he is so cute!

He was a real lifesaver to Phippy!

The cluster looks like this from the top

And like this from the side.

This time, we gathered interested interns and learned about Kubernetes.
We are excited to work with them in the future!

]]>
【イベントレポート】JapanContainerDays v18.12に参加致しました! https://cyberagent.ai/blog/pr/report_event/5016/ Fri, 07 Dec 2018 06:33:11 +0000 https://cyberagent.ai/?p=5016 みなさんこんにちは!

12/4-5に開催されたコンテナ技術のカンファレンス「JapanContainerDays v18.12」にて、

アドテクスタジオの青山・長谷川・山田・和田の4名が登壇致しました 😀

 

本イベントは開催2回目にして約700名を動員するカンファレンスで、

会場は終日大変盛り上がっておりました!

本日はその様子をお届け致します!

  

 

オープニングアクトは運営のインプレスの鈴木様より。

今年の4月にエンジニア中心のテックカンファレンスとして始まった本イベントですが、

2日間で60を超えるセッション、来場者700名と大規模な内容になっており、

たくさんのサポートコミュニティに支えられていることへの感謝を述べられていらっしゃいました。

今話題のクラウドネイティブですが、海外に比べるとまだ日本は遅れているのも事実、

ここにいる全員で日本勢を盛り上げていきたいという熱い気持ちもお話しされていました。

 

2日間に渡るセッションでは、弊社社員も多数登壇致しました🙌

どのセッションもたくさんの参加者で溢れておりました!

 

 

▼本番環境のKubernetesマニフェストに 最低限必要な 7 のこと/青山真也

 

 

 

▼runc だけじゃない low level runtime 徹底比較/長谷川誠

 

 

▼レガシーシステムのコンテナ化に挑戦した話/和田亨

 

 

▼ミドルウェア〜Webアプリまで全てをHelm化したサービスの運用事例/山田直行

 

今回のイベントにはたくさんのスポンサーブースや書籍コーナーがありましたが、アドテクスタジオとしてもブースを出し、

たくさんの参加者の方とお話をさせて頂きました🎉

 

前回好評だったブースにて「おうちKubernetes」が抽選で当たるという企画を

今回もさせて頂いたのですが、こちらも大 盛 況でした 🎉

応募頂いた皆様には追ってご連絡をさせて頂く予定です😀

 

▼賑わう会場…!

 

▼ContainerDaysマカロンも配られました!

 

1日目の夜に行われたAfterPartyでは、各社によるHiring LT大会が大盛り上がり!

飛び込みLTも多数あり、社長自らエンジニアを口説くシーンもたくさんありました!(笑)

 

スポンサーとして参加させて頂いた側としても、運営/参加者の皆様の熱量を感じ、とても素敵なカンファレンスに感じました。

 

これからもサイバーエージェントアドテクスタジオは

クラウドネイティブに力を入れていき、コミュニティの皆様と一緒に界隈を盛上げていきたいとおもいます!

 

改めて運営の皆様、登壇者の皆様、スポンサー企業の皆様、お疲れ様でした!😄

]]>
[Kubnernetes] kubelet の runtime を docker から cri-o に置き換える https://cyberagent.ai/blog/tech/4036/ Fri, 02 Feb 2018 00:50:48 +0000 https://cyberagent.ai/techblog/?p=4036 みなさんこんにちは。

アドテク本部の makocchi です。

 

kubernetes で使われている container の runtime にはみなさん何をお使いでしょうか?

ほぼ全ての人が docker と答えると思います。実際 Google Kubernetes Engine(GKE) も runtime には docker が使われています。(2018年01月現在)

しかし kubernetes はこの container の runtime を自由に選択することができます。

kubernetes と container runtime の間は CRI(Container Runtime Interface) を通して行われます。

つまり CRI に対応している runtime であれば何でも良いということになります。

CRI に対応している代表的な runtime には docker、cri-o、rkt、cri-containerd があります。今後は様々な runtime が増えてくるかもしれないですね。

 

cri-o について

cri-o は Kubernetes Incubator Project として開発されています。

CRI に対応しているのはもちろんですが、もう一つの標準規格である OCI(Open Container Initiative) にも準拠しています。

docker の image は既に OCI に準拠していますので、docker build した image を cri-o も使用することができます。

※ http://cri-o.io/ より抜粋

 

それではさっそく kubernetes の runtime として cri-o を使用してみましょう。

kubernetes 1.8 から cri-o が stable になりましたので、1.8 以上の cluster を用意します。

今回は 1.9 の kubernetes Cluster(Ubuntu) を用意しました。(kubernetes の環境の構築の手順はここでは省略します)

$ kubectl version --short
Client Version: v1.9.1
Server Version: v1.9.1

 

現状の runtime を確認しておきます。

$ kubectl get node cri-test-node -o jsonpath='{.status.nodeInfo.containerRuntimeVersion}{"\n"}'
docker://1.12.6

 

kubelet と dockerd は落としておきます。

$ sudo systemctl stop kubelet
$ sudo systemctl stop docker
※systemd 配下で動かしている場合

 

runc, crio の install

 

runc の install

現時点(2018年1月)での最新である v1.0.0-rc4 を使います。

$ sudo curl -o /usr/bin/runc -s -L https://github.com/opencontainers/runc/releases/download/v1.0.0-rc4/runc.amd64
$ sudo chmod +x /usr/bin/runc
$ /usr/bin/runc --version
runc version 1.0.0-rc4
spec: 1.0.0

 

crio の install

cri-o は binary の filename が crio となるようです(ややこしい)

crio の binary は用意されていないので自分で作る必要があります。

tutorial によると go 1.8.5 が指定されていますので、なるべく 1.8.5 で作るのがいいでしょう。

go の環境が整ったらまずは crictl を build します。

$ go get github.com/kubernetes-incubator/cri-tools/cmd/crictl
$ ${GOPATH}/bin/crictl --version
crictl version 1.0.0-alpha.0

その次に crio を build します。

$ sudo apt-get update && sudo apt-get install -y libglib2.0-dev \
                                                 libseccomp-dev \
                                                 libapparmor-dev \
                                                 libgpgme11-dev \
                                                 libdevmapper-dev \
                                                 make \
                                                 gcc \
                                                 git
$ go get -d github.com/kubernetes-incubator/cri-o
$ cd $GOPATH/src/github.com/kubernetes-incubator/cri-o
$ make install.tools
$ make

続いて install します。

$ sudo make install
install -D -m 755 bin/crio /usr/local/bin/crio
install -D -m 755 bin/conmon /usr/local/libexec/crio/conmon
install -D -m 755 bin/pause /usr/local/libexec/crio/pause
install -d -m 755 /usr/local/share/man/man5
install -d -m 755 /usr/local/share/man/man8
install -m 644 docs/crio.conf.5 -t /usr/local/share/man/man5
install -m 644 docs/crio.8 -t /usr/local/share/man/man8

続いて config を配置します。

$ sudo make install.config
install -D -m 644 crio.conf /etc/crio/crio.conf
install -D -m 644 seccomp.json /etc/crio/seccomp.json
install -D -m 644 crio-umount.conf /usr/local/share/oci-umount/oci-umount.d/crio-umount.conf
install -D -m 644 crictl.yaml /etc

crio の config は /etc/crio/crio.conf のようですね。

default ではこのような内容です。

$ grep -v "#" /etc/crio/crio.conf | grep -v "^$"
[crio]
root = "/var/lib/containers/storage"
runroot = "/var/run/containers/storage"
storage_driver = ""
storage_option = [
]
[crio.api]
listen = "/var/run/crio/crio.sock"
stream_address = ""
stream_port = "10010"
file_locking = true
[crio.runtime]
runtime = "/usr/bin/runc"
runtime_untrusted_workload = ""
default_workload_trust = "trusted"
no_pivot = false
conmon = "/usr/local/libexec/crio/conmon"
conmon_env = [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
]
selinux = false
seccomp_profile = "/etc/crio/seccomp.json"
apparmor_profile = "crio-default"
cgroup_manager = "cgroupfs"
hooks_dir_path = "/usr/share/containers/oci/hooks.d"
default_mounts = [
]
pids_limit = 1024
enable_shared_pid_namespace = false
log_size_max = -1
[crio.image]
default_transport = "docker://"
pause_image = "kubernetes/pause"
pause_command = "/pause"
signature_policy = ""
image_volumes = "mkdir"
insecure_registries = [
]
registries = [
]
[crio.network]
network_dir = "/etc/cni/net.d/"
plugin_dir = "/opt/cni/bin/"

[crio.image] の registries のところは default では空になっています。

# registries is used to specify a comma separated list of registries to be used
# when pulling an unqualified image (e.g. fedora:rawhide).
registries = [
]

多くの場合は docker hub から image を取ってくると思うので、registries に docker.io を書いておくのがいいでしょう。

registries = [
  "docker.io"
]

もしくは crio の起動時に –registry で渡してあげてもいいと思います(書かなくても image は取ってこれます)

 

crio の起動 (systemd)

 

今回は crio を systemd 経由で起動させます。

crio の引数は自由に設定してください。ここでは loglevel を debug にしています。

$ sudo sh -c 'echo "[Unit]
Description=OCI-based implementation of Kubernetes Container Runtime Interface
Documentation=https://github.com/kubernetes-incubator/cri-o

[Service]
ExecStart=/usr/local/bin/crio --log-level debug
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target" > /etc/systemd/system/crio.service'

$ sudo systemctl daemon-reload
$ sudo systemctl enable crio
$ sudo systemctl start crio

 

crio の動作確認

 

動作を確認してみましょう

version の確認

$ sudo crictl version
Version:  0.1.0
RuntimeName:  cri-o
RuntimeVersion:  1.9.0-dev
RuntimeApiVersion:  v1alpha1

 

下記のようにエラーが出る場合は crio が正常に起動していないと思われます。

FATA[0000] getting the runtime version failed: rpc error: code = Unavailable desc = grpc: the connection is unavailable

journalctl -u crio 等で log を確認しましょう。

image の操作

crictl で image を pull してみます。

その前に /etc/containers/policy.json という file が無いと怒られるので sample を github から持ってきます。

$ sudo mkdir -p /etc/containers
$ sudo curl -L -s -o /etc/containers/policy.json https://raw.githubusercontent.com/kubernetes-incubator/cri-o/master/test/policy.json

 

crictl pull で image を local に持ってくることが可能です。

crictl images で現在 local にある image 一覧を表示できます。

$ sudo crictl pull docker.io/nginx:alpine
Image is update to date for docker.io/library/nginx@sha256:a4ac36c250ec97bb6108e04e3cdd2848e338ee197737cebd39c960d7e3237775

$ sudo crictl images
IMAGE                     TAG                 IMAGE ID            SIZE
docker.io/library/nginx   alpine              bb00c21b4edf4       18MB

 

先程落としてきた policy.json ですが、policy の中に

...
        "docker": {
            "docker.io/library/hello-world": [
                {
                    "type": "reject"
                }
            ],
...

という部分があります。

これは hello-world の image pull は拒否するということで、実際に pull しようとするとちゃんと reject されます。

$ sudo crictl pull hello-world
FATA[0006] pulling image failed: rpc error: code = Unknown desc = Source image rejected: Running image docker://hello-world:latest is rejected by policy.

細かく policy を設定することも出来るようですね。

 

cri-o の storage_driver 変更(devicemapper から overlay2 へ)

image は crio.conf 内に定義されている root のディレクトリ配下に置かれます。

# root is a path to the "root directory". CRIO stores all of its data,
# including container images, in this directory.
root = "/var/lib/containers/storage"

 

/var/lib/containers/storage のディレクトリ構成を見てみます。

$ sudo tree /var/lib/containers/storage
/var/lib/containers/storage
|-- devicemapper
|   |-- devicemapper
|   |   |-- data
|   |   `-- metadata
|   |-- metadata
|   |   |-- 078e9ac4658c6d69ba8a0ab0a95fb4bf8f9a8efa59f0611cf26a33d02b7dc16e
|   |   |-- 2c735e3fd96adf058d54e1fb5acf63b0466ff146c7e395494dc8d35d5a8a7a35
|   |   |-- 2e00a080cabc2f6d4b4a03ae381ae95044940cfb12d8f74f5be976737e6b5329
|   |   |-- base
|   |   |-- d39d92664027be502c35cf1bf464c726d15b8ead0e3084be6e252a161730bc82
|   |   |-- deviceset-metadata
|   |   `-- transaction-metadata
|   `-- mnt
|       |-- 078e9ac4658c6d69ba8a0ab0a95fb4bf8f9a8efa59f0611cf26a33d02b7dc16e
|       |-- 2c735e3fd96adf058d54e1fb5acf63b0466ff146c7e395494dc8d35d5a8a7a35
|       |-- 2e00a080cabc2f6d4b4a03ae381ae95044940cfb12d8f74f5be976737e6b5329
|       `-- d39d92664027be502c35cf1bf464c726d15b8ead0e3084be6e252a161730bc82
|-- devicemapper-containers
|   `-- containers.lock
|-- devicemapper-images
|   |-- bb00c21b4edf411c9ef782c070d3cc6c46791298743d3d47c0de2a31e598b6cd
|   |   |-- =c2hhMjU2OmJiMDBjMjFiNGVkZjQxMWM5ZWY3ODJjMDcwZDNjYzZjNDY3OTEyOTg3NDNkM2Q0N2MwZGUyYTMxZTU5OGI2Y2Q=
|   |   `-- manifest
|   |-- images.json
|   `-- images.lock
|-- devicemapper-layers
|   |-- 078e9ac4658c6d69ba8a0ab0a95fb4bf8f9a8efa59f0611cf26a33d02b7dc16e.tar-split.gz
|   |-- 2c735e3fd96adf058d54e1fb5acf63b0466ff146c7e395494dc8d35d5a8a7a35.tar-split.gz
|   |-- 2e00a080cabc2f6d4b4a03ae381ae95044940cfb12d8f74f5be976737e6b5329.tar-split.gz
|   |-- d39d92664027be502c35cf1bf464c726d15b8ead0e3084be6e252a161730bc82.tar-split.gz
|   |-- layers.json
|   `-- layers.lock
|-- mounts
|-- storage.lock
`-- tmp

先程 pull してきた nginx:alpine(bb00c21b4edf4) の image 定義は ${root}/devicemapper-images 配下に置かれています。

どうやら storage_driver には devicemapper 使われているようですね。

 

それではさっそく overlay2 に変えてみましょう。

crio.conf を編集して storage_driver に overlay2 を設定し、crio を再起動させます。

$ sudo vi /etc/crio/crio.conf
...
# storage_driver select which storage driver is used to manage storage
# of images and containers.
storage_driver = "overlay2"
...

$ sudo systemctl restart crio

 

今度は redis の image を pull してみます。

$ sudo crictl pull redis:alpine
Image is update to date for docker.io/library/redis@sha256:28a852279e97b1d8683759a3f70b17b057d94b683447d3fe32be947d1cd04a1c

今度は ${root}/overlay2 が作成されていると思います。

 

 

cri-o で pod を起動させ、container を動かす

cri-o ではまず pod を起動させ、その中で container を動かすという手順になるようです。

pod を起動させる為に pod の設定が書かれた json or yaml を渡してあげる必要があります。

 

まずは sandbox の pod を起動させてみましょう。

cri-o の github repo 内の test/testdata 内に sample が置かれているので、そこから pod を起動させます。

pod の起動には crictl runp を使用します。crictl pods で表示させることができます。

$ cd $GOPATH/src/github.com/kubernetes-incubator/cri-o
$ sudo crictl runp test/testdata/sandbox_config.json
cd9069056ab8e48713bcee6f7e096af178b5a5c5c274fcea01d59d6bb0848d7d

$ sudo crictl pods
PODSANDBOX ID       CREATED             STATE               NAME                NAMESPACE           ATTEMPT
cd9069056ab8e       2 minutes ago       SANDBOX_READY       podsandbox1         redhat.test.crio    1

podsandbox1 の pod が起動しました。

inspectp をすることでより詳細な情報を見ることができます。

$ sudo crictl inspectp cd9069056ab8e
{
  "status": {
    "id": "cd9069056ab8e48713bcee6f7e096af178b5a5c5c274fcea01d59d6bb0848d7d",
    "metadata": {
      "attempt": 1,
      "name": "podsandbox1",
      "namespace": "redhat.test.crio",
      "uid": "redhat-test-crio"
    },
    "state": "SANDBOX_READY",
    "createdAt": "2018-02-01T11:01:01.967620802+09:00",
    "network": {
      "ip": "10.112.76.6"
    },
    "linux": null,
    "labels": {
      "group": "test",
      "io.kubernetes.container.name": "POD"
    },
    "annotations": {
      "owner": "hmeng",
      "security.alpha.kubernetes.io/seccomp/pod": "unconfined"
    }
  }
}

この pod には 10.112.76.6 の ip が振られました。

 

次に podsandbox1 の pod 内で redis の container を起動させてみます。

container を起動する時にも定義ファイルを渡してあげる必要があります。

今回は用意されている testdata 内の container_redis.json を使います。

$ sudo crictl create cd9069056ab8e test/testdata/container_redis.json test/testdata/sandbox_config.json
b661c78f388c6af22df757a4f1ca7da5816233c579a4a1dc5cd2faecdc6d3794

$ sudo crictl ps -a
CONTAINER ID        IMAGE               CREATED             STATE               NAME                ATTEMPT
b661c78f388c6       redis:alpine        28 seconds ago      CONTAINER_CREATED   podsandbox1-redis   0

crictl create が無事に終わったら crictl start させます。

$ sudo crictl start b661c78f388c6
b661c78f388c6

$ sudo crictl ps
CONTAINER ID        IMAGE               CREATED             STATE               NAME                ATTEMPT
b661c78f388c6       redis:alpine        2 minutes ago       CONTAINER_RUNNING   podsandbox1-redis   0

STATE が CONTAINER_CREATED から CONTAINER_RUNNING になりました。

 

それではこの redis に接続してみましょう。

先程 inspectp した際に出てきた podsandbox1 の ip(10.112.76.6) に対して接続してみます。

$ telnet 10.112.76.6 6379
Trying 10.112.76.6...
Connected to 10.112.76.6.
Escape character is '^]'.
info
$2635
# Server
redis_version:4.0.7
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:f295d9beb14f1ade
redis_mode:standalone
...
...

無事に動作の確認ができました!

 

動作確認が終わったので、pod と container は消しておきます。

container を消す際には rm を、pod を消す際には rmp を使用します。

$ sudo crictl rm b661c78f388c6
b661c78f388c6

$ sudo crictl ps -a
CONTAINER ID        IMAGE               CREATED             STATE               NAME                ATTEMPT

$ sudo crictl stopp cd9069056ab8e
Stopped sandbox cd9069056ab8e

$ sudo crictl rmp cd9069056ab8e
Removed sandbox cd9069056ab8e

$ sudo crictl pods
PODSANDBOX ID       CREATED             STATE               NAME                NAMESPACE           ATTEMPT

 

kubelet の設定を変更して runtime に cri-o を指定する

 

kubelet の設定

それではいよいよ kubelet の設定を docker から cri-o に変更してみます。

kubelet に下記の option を設定して起動させます。

--container-runtime=remote
--container-runtime-endpoint=/var/run/crio/crio.sock
--runtime-request-timeout=10m (optional)

–runtime-request-timeout は必要に応じて設定してください。

そして先程と同じように get node で表示させてみます。

$ kubectl get node cri-test-node -o jsonpath='{.status.nodeInfo.containerRuntimeVersion}{"\n"}'
cri-o://1.9.0-dev

無事に変更されました!

 

 

実際に pod を kubernetes 経由で起動させる

次に実際に pod を kubectl で作成してみます。

cri-o で動いている node に label を付け、nodeSelector で指定して作成します。

$ kubectl label node cri-test-node runtime=crio

manifest はこんな感じです。

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  nodeSelector:
    runtime: crio

$ kubectl create -f nginx-crio.yaml

$ kubectl get pods -o wide
NAME      READY     STATUS    RESTARTS   AGE       IP             NODE
nginx     1/1       Running   0          35s       10.112.76.11   cri-test-node

$ kubectl get pod nginx -o jsonpath='{.status.containerStatuses[].containerID}{"\n"}'
cri-o://f48039701e4c6d6239726d5e84cb23f9743e5194ffd47d16a5a3e4001acc0770

kubernetes 上から問題なく pod が作成されました。

cri-o 上では f48039701e4c6 で動いていることがわかりましたので、node 側で確認してみます。

$ sudo crictl ps
CONTAINER ID        IMAGE                                                                                                                                    CREATED             STATE               NAME                 ATTEMPT
f48039701e4c6       docker.io/library/nginx@sha256:926b086e1234b6ae9a11589c4cece66b267890d24d1da388c96dd8795b2ffcfb                        6 minutes ago       CONTAINER_RUNNING   nginx                0

無事に起動していますね。

 

まとめ

このように docker から cri-o への runtime 変更することができるのですが、現状の docker で特に苦労していなければ無理に置き換える必要は無いと思っています。

それでも敢えて docker を置き換える理由として考えられるのは個人的には以下のようなものがあるのではと思っています。

  • docker よりも軽量でスリムな crio を使用することで node 側の負担を少しでも軽減させる (手元の環境では dockerd よりも crio のほうが cpu/memory 使用量が小さかった)
  • dockerd よりも安定しそう  これは根拠が無いので憶測に過ぎないのですが、いろいろな機能が実装されている太った dockerd よりもスリムな crio の方が安定性があるのではないかと思っています

今はまだ導入するのに若干手順が多いですが、その内 rpm や deb の package が作られれば導入は楽になるでしょう。

crictl の option は docker の option に非常によく似ている為、docker に慣れていれば問題なく使うことができそうです。

 

それではよい Kubernetes life を!

]]>
KubeCon + CloudNativeCon NorthAmerica 2017 @Austin recap https://cyberagent.ai/blog/pr/kubernetes/3925/ Wed, 10 Jan 2018 03:30:53 +0000 https://cyberagent.ai/techblog/?p=3925 こんにちは。

アドテクスタジオ CIA の 青山真也 です。

本日は KubeCon + CloudNativeCon 2017 の話をしたいと思います。
資料はちょこちょこ公開していましたが、ブログを書くのが遅れてしまいました。
にしても、Kubernetes 楽しいですね。

 

KubeCon 2017 の概要

KubeCon + CNCon は半年に1回開催されており、今回は大幅に人数が増え 4000 人満員御礼でした。
Kubernetes が事実上のデファクトスタンダードとなったことにより、注目度も非常に高くなったのだと思います。

CNCF が提供する CloudNative Landscape も 1.0 となり、Fluentd や CoreDNS、その他多くのプロジェクトが 1.0 リリースを発表しました。
これは事実上、CNCF からの CloudNative はもう準備完了というメッセージのようですね。
他にも ServiceMesh を実現する Istio や Conduit、Serverless を実現する Fission、kubeless、OpenFaaS などの注目度が高かった印象です。

今回の会場はテキサス州オースティンだったため、時差的には +0900 から -0600 の差(15 時間の時差)があるため 12/4 は 39 時間ありました。
2018 年は下記の日程で開催予定です。
皆様コペンハーゲンでお会いしましょう。

  • KubeCon + CloudNativeCon Europe 2018
    • コペンハーゲン デンマーク
    • 2018/5/2-4
  • KubeCon + CloudNativeCon China 2018
    • 上海 中国
    • 2018/11/14-15
  • KubeCon + CloudNativeCon North America 2018
    • シアトル アメリカワシントン州
    • 2018/12/11-13

 

Keynote

Keynote では様々な企業での Kubernetes や CNCF がホストしている Cloud Native を促進するプロジェクトの事例が多々紹介されていました。
また、CNCF が保有する各プロジェクトのアップデートやコミュニティの活発さも紹介されており、非常に盛り上がりを見せていました。

社内向けに作成した Keynote 1 日目 + αの資料を挙げてありますので、詳しくはこちらを御覧ください。

 

 

CI/CD 周りのセッション

今回の KubeCon では、「CI/CD 関連」「ServiceMesh(Istio / Conduit)」「Network や OpenStack 周りの SIG」「Serverless(Fission / Kubeless / OpenFaaS)」辺りを中心に聞いてきました。

社内的には CI/CD 周りの改善が望まれていたため、こちらも上記の社内向けに作成した資料に挙げてあります。以下簡単に概要だけ記載しておきます。

 

Using Containers for Continuous Integration and Continuous Delivery

Jenkins と Kubernetes の Integration の話。Groovy で PodTemplate のような記述を行って、build task をコンテナとして起動することができるようです。

 

Deploying to Kubernetes Thousands of Times Per/Day

Kubernetes に対して高速な開発を行なう際に注意すべき4つのポイントの話。具体的かつ体系だって説明がされており、Kubernetes を開発に取り入れる場合には是非見てほしい内容です。

 

Continuous Delivery with Kubernetes at Box

Kubernetes クラスタに Agent を仕込んでおき、Agent が Github 上の Manifest を監視して自身の Kubernetes クラスタを更新する方式で CD を行なう話。新規クラスタに Agent を入れるだけで復旧できるなど、Github をベースとした CD 環境を構築できるため、後術する GitOps などとも近い印象。

 

Microservices, Service Mesh, and CI/CD Pipelines: Making It All Work Together

Istio と BRIGATE (+ kashti)を使った、高度な CI/CD パイプラインを構成する方法。Istio を使うと Canary ・Chaos テスト・Fault Injection・レスポンスタイムなどの集計などができ、BRIGADE を組み合わせると Istio と連携しつつコードによるPipeline 定義をはじめとして、様々なことができるようです。使いこなせたらかなり良い Pipeline 環境にできそうです。

 

Building Better Containers: A Survey of Container Build Tools

Dockerfile では様々な問題があるため、多々ある別のイメージビルドツールの調査結果の話。packer のような方式のイメージビルドツールもあれば、ソースコードから直接イメージを作成するようなツールなど多岐に渡って紹介されていました。

 

GitOps – Operations by Pull Request

GitHub (Pull Request)を利用した Kubernetes 環境への CI/CD パイプラインの作り方の話。非常に分かりやすく、CI/CD パイプラインを考えるなら、このセッションか 2 日目の Kelsey Hightower さんの Keystone 辺りは非常に参考になります。

 

Developer Tooling for Kubernetes Configuration

Kubernetes の Manifest ファイルの Validation と Unit Test を kubeval と kubetest を使って行なう話。CI 時にある程度テストをしておくのは有用かもしれません。

 

Webhooks for Automated Updates

Rancher の Webhook Framework を使って DockerHub の更新時に Kubernetes クラスタの更新を自動化する話。

 

また、先日「@市ヶ谷Geek★Night #16 Kubernetes Christmas!」 でお話させていただいた CI/CD の話では、これらのセッションと CyberAgent での話を混ぜ合わせて CI/CD の話をさせていただきました。

 

 

日本交流会@Austin

今回 KubeCon に参加するにあたり現地で日本交流会を主催させていただきました。著名な方々を含む 33 名もの方々にお集まりいただき、貴重な意見交換ができて良かったです。@tnir さんによると KubeCon 2016 の日本人参加者は 3 名とのことなので、最低でも 11 倍になったようです。非常に盛り上がりを見せていますね。

KubeCon + CloudNativeCon Europe 2018 に行かれる方がいらっしゃいましたら、ぜひコペンハーゲンでお会いしましょう。

 

 

その他

Certified Kubernetes Administrator に合格していると RX-M 様のスポンサーブースで CKA の Tシャツをいただく事ができました。

 

また、KubeCon + CloudNativeCon の多くのセッションは動画が公開されています。興味のあるセッションがあれば Youtube の字幕機能を使いつつ見てみるといいかもしれません。

 

そして KubeCon + CNCon EU 2018 へ

弊社ではオンプレ環境に GKE ライクな Kubernetes 基盤を構築しています。Kubernetes にパッチを宛てて Hardware LB との連携を行ったり Ingress Controller の実装などを行い nginx-ingress / nghttpx-ingress ベースの GKE ライクな Ingress Controller を作成しました。今後はこういった事例で登壇も目指していきたいと思います。

日本からも Kubernetes や Cloud Native を盛り上げていきましょう!

 

 

]]>
[Kubernetes] node-problem-detector で node の監視をしてみる https://cyberagent.ai/blog/tech/3727/ Mon, 04 Dec 2017 15:00:42 +0000 https://cyberagent.ai/techblog/?p=3727 みなさんこんにちは。

アドテク本部の makocchi です。

 

この記事は Kubernetes Advent Calendar 2 の 5日目の記事になります。

 

Kubernetes には addon の機能がいくつかありますが、その中でも今日は node-problem-detector について紹介したいと思います。

 

node-problem-detector とは

Kubernetes の node 側では kubelet を動かすことになると思いますが node の状態をある程度 monitor してくれます。

$ kubectl get node node-name -o json | jq .status.conditions
[
  {
    "lastHeartbeatTime": "2017-11-24T13:14:05Z",
    "lastTransitionTime": "2017-11-24T13:08:32Z",
    "message": "kubelet has sufficient disk space available",
    "reason": "KubeletHasSufficientDisk",
    "status": "False",
    "type": "OutOfDisk"
  },
  {
    "lastHeartbeatTime": "2017-11-24T13:14:05Z",
    "lastTransitionTime": "2017-11-24T13:08:32Z",
    "message": "kubelet has sufficient memory available",
    "reason": "KubeletHasSufficientMemory",
    "status": "False",
    "type": "MemoryPressure"
  },
  {
    "lastHeartbeatTime": "2017-11-24T13:14:05Z",
    "lastTransitionTime": "2017-11-24T13:08:32Z",
    "message": "kubelet has no disk pressure",
    "reason": "KubeletHasNoDiskPressure",
    "status": "False",
    "type": "DiskPressure"
  },
  {
    "lastHeartbeatTime": "2017-11-24T13:14:05Z",
    "lastTransitionTime": "2017-11-24T13:08:32Z",
    "message": "kubelet is posting ready status",
    "reason": "KubeletReady",
    "status": "True",
    "type": "Ready"
  }
]

デフォルトだと disk や memory の状態を monitor してくれます。

 

しかし時と場合によっては、disk や memory の状態だけではなくて例えば runtime に Docker を使っていた場合に dockerd が固まっていたりしたら kubelet 的にはコンテナの配置を防いでほしいですよね。

そこで役に立つのが node-problem-detector というわけです。

node-problem-detector は特定の log を watch してくれて、特定の文字列が出てきた場合に node の condition を更新してくれます。

 

node-problem-detector の導入方法

先程紹介した addon の repository に manifest があるのでそれを使うのが一番楽です。

$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/node-problem-detector/npd.yaml

この manifest は設定内容が単純な文字列しか登録されていませんので、実際の運用に入れる場合にはもう少し config をいじる必要があるでしょう。

config は json 形式で記述します。特に何も考えずに起動させた場合、予め image に入っている config が使われます。

予め入っている config はこちらから確認することができます。

config で指定することができる plugin の type は journald / filelog / kmsg を指定することができます。

journald の log を watch させる場合には journald の log を persistent にしておくといいでしょう。

(/etc/systemd/journald.conf に Storage=persistent を書いて systemctl restart systemd-journald して下さい)

kmsg の場合は kernel が出す log を watch することができます。いわゆる dmesg ですね。

 

config をカスタマイズしたい場合は config を configmap で渡してあげるといいでしょう。

configmap で定義して pod 側で mount して、起動時の option で configmap の config を見るように変更してください。

起動時の option は

command:
        - "/bin/sh"
        - "-c"
        # Pass both config to support both journald and syslog.
        - "/node-problem-detector --logtostderr --system-log-monitors=/config/kernel-monitor.json,/config/docker-monitor.json,/config/kmsg-monitor.json"

このように定義されていますので –system-log-monitors の部分を適宜変えて下さい。例えば configmap を定義して /configmap に mount したならば

command:
        - "/bin/sh"
        - "-c"
        # Pass both config to support both journald and syslog.
        - "/node-problem-detector --logtostderr --system-log-monitors=/configmap/original.json"

こんな感じになると思います。

もちろん元々配置されている config に追加してもいいし、configmap を /config に mount して入れ替えてしまってもいいと思います。

 

node-problem-detector が起動すると、config で定義された condition が追加されていることが確認できます。

$ kubectl get node node-name -o json | jq .status.conditions
[
  {
    "lastHeartbeatTime": "2017-11-24T13:48:07Z",
    "lastTransitionTime": "2017-11-24T13:25:54Z",
    "message": "kernel has no deadlock",
    "reason": "KernelHasNoDeadlock",
    "status": "False",
    "type": "KernelDeadlock"
  },
....
]

KernelDeadlock という condition が追加されました。

 

実際に検知させてみる

それでは実際に検知するとどうなるのでしょうか。

今回は下記の config を読ませて実験してみます。

{
      "plugin": "kmsg",
      "pluginConfig": {
        "timestamp": "^.{15}",
        "message": "\\[.*\\] (.*)",
        "timestampFormat": "Jan _2 15:04:05"
      },
      "lookback": "5m",
      "bufferSize": 10,
      "source": "kmsg-monitor",
      "conditions": [
        {
          "type": "KernelDeadlock",
          "reason": "KernelHasNoDeadlock",
          "message": "kernel has no deadlock"
        }
      ],
      "rules": [
        {
          "type": "temporary",
          "reason": "KernelOops",
          "pattern": "BUG: unable to handle kernel NULL pointer dereference at .*"
        },
        {
          "type": "permanent",
          "condition": "KernelDeadlock",
          "reason": "TaskHung",
          "pattern": "task \\S+:\\w+ blocked for more than \\w+ seconds\\."
        }
      ]
    }

本当ならば kernel にいろいろ log を出させてみたいところですが、今回はちょっと擬似的に log を出力させることにします。

さっそく node 側で /dev/kmsg に対して書き込みをしてみましょう。

$ echo "BUG: unable to handle kernel NULL pointer dereference at hogehoge" | sudo tee -a /dev/kmsg
BUG: unable to handle kernel NULL pointer dereference at hogehoge

実際に dmesg を確認
$ dmesg | tail -1
[ 3266.441916] BUG: unable to handle kernel NULL pointer dereference at hogehoge

node 側を describe してみます。

$ kubectl describe node node-name
...
...
Events:
  Type     Reason      Age   From                                 Message
  ----     ------      ----  ----                                 -------
  Warning  KernelOops  1m    kmsg-monitor, node-name              BUG: unable to handle kernel NULL pointer dereference at hogehoge

Event のところに出てきましたね。

しかし node の condition の KernelDeadlock のところはまだ False になっていると思います。

それは先程の config で KernelOops の rule は type が temporary と定義しているからで、一時的なものとして記録されるようです。

それでは type が permanent になっている TaskHung を発生させてみましょう。

$ echo "task hoge:hoge blocked for more than 120 seconds." | sudo tee -a /dev/kmsg
task hoge:hoge blocked for more than 120 seconds.

$ kubectl describe node node-name
...
...
Events:
  Type     Reason      Age   From                                 Message
  ----     ------      ----  ----                                 -------
  Warning  KernelOops  11m   kmsg-monitor, node-name              BUG: unable to handle kernel NULL pointer dereference at hogehoge
  Warning  TaskHung    1m    kmsg-monitor, node-name              task hoge:hoge blocked for more than 120 seconds.

$ kubectl get node node-name -o json | jq .status.conditions
[
  {
    "lastHeartbeatTime": "2017-11-24T14:22:15Z",
    "lastTransitionTime": "2017-11-24T14:20:13Z",
    "message": "task hoge:hoge blocked for more than 120 seconds.",
    "reason": "TaskHung",
    "status": "True",
    "type": "KernelDeadlock"
  },
....
]

KernelDeadlock の status が True になりました。

が・・ KubeletReady の status は True のままでした。こちらの status までは変えてくれないようです。

 

まとめ

今回は node-problem-detector の挙動を検証してみました。

実際に kernel が出す error を検知して node の condition を変更するのはとても便利なのですが、まだ機能的に足りていない点もありそうです。

  • log 等の出力結果を watch することでしか検知することが出来ない
  • 一度 condition(上記の例だと KernelDeadlock) が True になると元に戻せ無さそう
    • その node の node-problem-detector の pod を kill して再作成させると False に戻る

log 等の出力結果しか watch できない点についてはつい最近 custom plugin が実装されたようです。

この custom plugin を使えば shell の実行結果を watch することができるようです。

(こちらの例では ntp の状態を監視しています)

この custom plugin によって一気に config の幅が広がりますね。

 

condition が戻らない件については戻ってしまっては困るケースもあると思います。

ですので問題が発生した node は捨てて再作成するというポリシーで運用することができればそれほど問題にならないと思いました。

自動で drain までやってくれるとすごくいいんですけどもね。

 

今回は node-problem-detector を紹介させて頂きました。

node-problem-detector を上手く動かして快適な Kubernetes Life を送って下さい!

]]>
[Kubernetes] オンプレでも GKE Like な Ingress を使うために 自作 Ingress Controller を実装してみた https://cyberagent.ai/blog/pr/kubernetes/3758/ Thu, 30 Nov 2017 23:00:54 +0000 https://cyberagent.ai/techblog/?p=3758 こんにちは。
CIA の青山 真也です。

この記事は Kubernetes Advent Calendar 1日目の記事になります。

ちなみに CyberAgent Advent Calndar 1日目の記事も「GKE 互換のオンプレコンテナ基盤 AKE (Adtech Container Engine) 誕生秘話とアーキテクチャ完全公開!」というテーマで書いており、まるで夏休み最終日の気分です。
弊社のオンプレコンテナ基盤 AKE の話を詳細にまとめてありますので、こちらも是非お読み下さい。

今回は、SSL 終端や L7 パスベースルーティング及び L7 ロードバランシングを行ってくれる Ingress リソースの話をします。
GKE の Ingress が最高すぎるんですが、GKE の Ingress に負けない使い勝手になりました!

 

はじめに

みなさんは GKE 等で Ingress リソースを作成したことがある方が多いかと思います。

詳しくは後ほど話しますが、GKE で Ingress リソースを作ると非常に簡単に Ingress リソースを作ることができ、しかも分かりやすいと言った特徴があります。
AWS などでも alb-ingress-controller 等で ALB 連携できる Ingress Controller などが用意されています。

しかし、OpenStack 環境やベアメタル環境などでは Ingress を使う場合には手間が掛かる(しかも初心者には分かりづらい)nginx-ingress-controller や nghttpx-ingress-controller を利用する必要があります。

今回は、OpenStack 環境やベアメタル環境などの Kubernetes でも GKE Like に Ingress を利用できる方法をお伝えできればと思います。

 

Ingress の基礎知識

Ingress は L7 LoadBalancing を行なうリソースです。

たとえばこのような Deployment と、

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: sample-ingress-apps
spec:
  replicas: 5
  selector:
    matchLabels:
      ingress-app: sample
  template:
    metadata:
      labels:
        ingress-app: sample
    spec:
      containers:
        - name: nginx-container
          image: zembutsu/docker-sample-nginx:1.0
          ports:
            - containerPort: 80

このような Service を事前に作っておきます。

apiVersion: v1
kind: Service
metadata:
  name: svc1
spec:
  type: NodePort
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8888
      targetPort: 80
  selector:
    ingress-app: sample

 

Ingress では NodePort の Service を登録しておく必要があります。

その後、下記のような Ingress リソースを登録することで L7 LoadBalancer が作成され、L7 LoadBalancer から svc1 に対して転送されるような形になります。
余談ですが、spec.backend はデフォルトの転送先になります。また、SSL 終端を行なう場合には spec.tls を設定しておいて下さい。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: sample-ingress
  annotations:
    ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: sample1.example.com
    http:
      paths:
      - path: /path1
        backend:
          serviceName: svc1
          servicePort: 8888
  backend:
    serviceName: svc1
    servicePort: 8888
  tls:
  - hosts:
    - sample1.example.com
    secretName: tls-sample

 

図にするとこのような感じです。(/path2 宛を svc2 に転送する設定も入っている場合)

仕組みとしては、L7 LoadBalancer から Service に向けて転送するような形になっています。
GKE の場合と Nginx Ingress Controller の場合で結構仕組みはことなりますが、今の時点では結果的には L7 LoadBalancer から Service に向けて転送するということだけ抑えておいて下さい。

 

Ingress と Ingress Controller の違い

Ingress と Ingress Controller という似たような名前が登場してきていますが、これら 2 つは全くの別物です。

  • Ingress
    • Kubernetes のリソースの1種
    • ただの定義にすぎないため登録しただけでは何も起こらない(イメージ的には Custom Resource Definision に近い)
  • Ingress Controller
    • Ingress リソースの定義を読んで L7 ロードバランサを用意する

通常はこの Ingress リソースを登録しても Kubernetes に登録自体はされますが、何も起こりません。

GKE ではマネージドマスターで見えなくなっていますが、デフォルトで Ingress Controller が配備されているため GCLB の L7 LoadBalancer が作成されているだけです。

 

GKE の Ingress

GKE の Ingress の場合、Google Cloud LoadBalancer 上に「特定のパスから全 Kubernetes Node の NodePort へと転送する設定」が登録されます。
また、SSL 終端なども GCLB 上で行われます。

Ingress の場合には DSR 構成ではないため、SNAT と同じようにクライアントの IP Address が欠損してしまいますが、HTTP Header の X-Forwarded-For にクライアントの IP Address があるため、必要に応じて利用して下さい。

 

厳密には、GCLB > NodePort Service > Pod という経路をたどるため、GCLB が Kubernetes Node に割り振った後、Kubernetes Node から Kubernetes Node へと転送される可能性がある点に注意して下さい。
より低レイテンシを求める場合には、NodePort Service を作成する際に spec.externalTrafficPolicy を Local に設定して下さい。
この設定を行った場合には、NodePort Service が受けたトラフィックは必ず自分の Pod へと転送するようになります。
但し注意点が 2 つあります。

1つ目は、Kubernetes Node 上に転送できる Pod が無い場合は 別の Kubernetes Node に転送するといった柔軟な対応はしてくれません。spec.externalTrafficPolicy を Local に設定した場合は、必ず 各ノードに Pod が居るようにするか、NodePort に GCLB の転送設定を書き換えて下さい。

2つ目は、Ingress には関係ありませんが、spec.externalTrafficPolicy を Local にしていても、内部のコンテナから NodePort を叩くと、別の Kubernetes Node にもバランシングされる点に注意して下さい。仕様なのかはわかりませんが、iptables のエントリ的に ClusterIP 相当のチェインに吸い込まれるようです。

さすがは GKE といったところでしょうか。非常に分かりやすく理に適った構成だと思います。

 

Nginx Ingress Controller

Nginx Ingress Controller では、L7 LoadBalancer の代わりに Nginx Pod を作成し、Nginx で SSL 終端やパスベースのルーティングを行います。

また、Nginx Pod を作成すると書きましたが、Nginx Ingress Controller = Nginx Pod となります。そのため、負荷分散を行なう場合には Nginx Ingress Controller (Nginx Pod)を Deployment などでスケールさせることで実現します。
展開方法については詳しくはここでは書きませんが、各リポジトリの examples を見ると良いかと思います。

さらに、GKE の Ingress では/path1 に来たリクエストを svc1 の NodePort に転送していましたが、Nginx Ingress Controller では、/path1 に来たリクエストを直接 Pod に転送してくれるといった違いもあります。

この状態だと Nginx Pod に来たリクエストを分散対象の Pod にL7 LoadBalancing や SSL 終端・パスベースルーティングなどを行ってくれることはわかると思います。

しかし、このままではグローバルなエンドポイントを持たないため、nginx Ingress Controller の Pod に向けた “type LoadBalancer” Service を作成して上げる必要があります。
“type LoadBalancer” が利用できない場合などは、nginx Ingress Controller の Pod に向けた “type NodePort” などを作成することでも代用は可能です。
また、クラスタ内部でしか使わない場合には “type ClusterIP” でも問題ありません。

 

整理すると、L4 LoadBalancer > Nginx Ingress Controller Pod > 各 Pod という経路を辿ります。

 

Nginx Ingress Controller が GKE には勝てない部分

Nginx Ingress の使いづらいポイントはいくつかあります。

前述の通り、エンドポイントは Nginx Ingress Controller Pod への Service を作ることで実現します。
そのため、kubectl get ingress などした場合に、IP Address などは書かれておらず、自分自身で管理しなければなりません。
言い換えるならば、Ingress リソースを作成した後は、その Ingress リソース用に Nginx Ingress Controller Deployment を作成し、Service で紐付けてあげる作業が必要です。
さらに、Ingress 上には Service の IP が登録されるわけではないので、確認したい場合には対応する Service を探さなければならないと言った手間が発生します。

 

例えば、GKE 上ではこのような表示になるのですが、

# kubectl get ing -o wide
NAME              HOSTS                 ADDRESS           PORTS     AGE
sample-ingress    sample1.example.com   xxx.xxx.xxx.xxx   80, 443   3h
sample-ingress2   sample2.example.com   xxx.xxx.xxx.xxx   80, 443   3h

 

Nginx Ingress の場合にはこのような表示となってしまい、Address を確認することができません。
これは Nginx Ingress を利用している場合には、Ingress リソースの Status が更新される処理が存在しないためです。

# kubectl get ing -o wide
NAME              HOSTS                 ADDRESS   PORTS     AGE
sample-ingress    sample1.example.com             80, 443   3h
sample-ingress2   sample2.example.com             80, 443   3h

 

 

また、GKE の場合には複数の Ingress リソースを作成した場合に、それぞれの Ingress リソース毎に GCLB が作成されます。
これらは別のエンドポイントとなるため分離することが可能です。

 

 

一方で nginx ingress Controller の場合には、Ingress リソースに何も指定しないで複数作成した場合、Ingress Controller はどちらの Ingress リソースの定義も吸収します。したがって、上記の様なことを行おうと思っていたにも関わらず、両方共 エンドポイントを分けることは可能だとしても、中身は全く同じになってしまいます。

 

Nginx Ingress Controller を上記のように分離したい場合には、Ingress Controller と Ingress リソースの紐付けを行ってあげる必要があります。

Ingress Controller 側には –ingress-class HOGE の起動オプションを渡して起動します。また、Ingress リソース側に関しては annotation で kubernetes.io/ingress.class: HOGE を指定して作成します。
この作業をすることにより、Ingress Controller が管理する Ingress リソースを判別することができるようになるため、分離を行なうことが可能です。

 

自作 Ingress Controller

前述のおさらいですが、Nginx Ingress Controller を利用する場合、GKE に比べて大きく 3 つの使い勝手が悪い部分があると考えられます。

1.Ingress リソースの状態を確認しても Status は何も分からない

2.Ingress リソースを作成した後に、Nginx Ingress Controller Pod を立てて Service も作成する必要がある

3.Ingress リソースの分離性を保つために Ingress リソースと Nginx Ingress Controller に識別子を設定する必要がある

 

今回はこの問題を解消するべく、Ingress Controller を自作しました。
GKE の方が良いとは言わせません。オンプレも頑張ってるんだぞー。

Deployment Controller など様々なコントローラ系のソースを参考に作成しました。
Controller 系統を自作する場合には、イベントハンドラの機構などは用意されてはいますが、実装量は結構あるため type LoadBalancer のようにお気軽に作れるものではありませんでした。

イベントハンドラとしては、特定の Kubernetes API Resource に対して「追加」「変更」「削除」の処理が行われたタイミングで呼ばれます。
Deployment Controller であれば、追加時に ReplicaSet の追加を行なう・変更時に ReplicaSet から ReplicaSet へのローリングアップデートを実施、削除時は自身の保持する ReplcaSet を削除するといった処理を行なう必要があります。

cache.ResourceEventHandlerFuncs{
                        AddFunc:    c.onAdd,
                        UpdateFunc: c.onUpdate,
                        DeleteFunc: c.onDelete,
                })

 

また、各関数には追加削除時はそのリソースの情報、変更時は変更前後のリソースの情報が渡ってくる形になります。
今回の GKE Like な Ingress Controller では この obj interfece{} は obj.(*ext.Ingress) として Ingress リソースとして扱うことができる構造体として渡ってきています。

func (c *GKELikeIngressController) onAdd(obj interface{}) {
func (c *GKELikeIngressController) onUpdate(oldObj, newObj interface{}) {
func (c *GKELikeIngressController) onDelete(obj interface{}) {

 

GKE Like な自作 Ingress Controller の仕組みはわりとシンプルな構成になっており、Nginx Ingress Controller を上記のように手動で使う部分を全て自動化する Controller として実装しています。

onAdd が呼び出された際には、下記の 4 つの工程を行います。

  • Ingress リソースの annotation に “kubernetes.io/ingress.class” = NAMESPACE_INGRESSNAME を埋め込む
  • Ingress リソースに対応する Ingress Controller の Deployment を作成(–ingress-class = NAMESPACE_INGRESSNAME)
  • 上記の Deployment に対する “type Loadbalancer” Service を作成
  • Ingress リソースの Status に LB の IP と ClusterIP を反映

リソースの作成には kubernetes/go-client を利用しています。
例えば、Ingress の場合にはここにCreate()、Delete()、Update()、UpdateState() といったインターフェースが定義されています。

また、全てのリソースに共通することですが、onAdd 関数内で複数回リソースを更新するとエラーが発生するため、1回だけにする必要があります。
さらに、各リソースの Status に関しては Update() 関数ではなく UpdateStatus() 関数を利用しないと更新があっても無視される点に注意して下さい。
今回の場合だと、Ingress リソースの Status.LoadBalancer.Ingress 構造体を更新することで、LoadBalancer の IP と Cluster IP を登録しています。

onUpdate が呼び出された場合には特別な作業は行いません。作り込み次第ですが、 IP 変更時の変更処理程度だと思います。
Ingress リソースの更新に関しては、Nginx Ingress Controller の Deployment が変更を反映してくれるため、この部分に複雑な処理が必要ないのは多いなメリットだと思います。

onDelete 時には Deployment と Service を削除します。

 

以上の実装をすることで、GKE Like な Ingress Controller を作成することが出来ました。
GKE と違う部分としては、Internal から Ingress を叩く場合には ClusterIP を利用したほうが良いため、Ingress の IP にも一応入れています。

# kubectl get ing -o wide
NAME              HOSTS                 ADDRESS                      PORTS     AGE
sample-ingress    sample1.example.com   192.104.167.215,10.10.10.45   80, 443   3h
sample-ingress2   sample2.example.com   192.111.123.71,10.10.10.47    80, 443   3h

 

裏側的には Deployment と Service が配置されています。当然 annotation と ingress-class は指定された状態でデプロイされています。

# kubectl -n kube-system get deploy,service sample-ingress
NAME                    DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/sample-ingress   1         1         1            1           3h

NAME                 TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
svc/sample-ingress   LoadBalancer   192.104.167.215   10.10.10.45   80:11652/TCP,443:44404/TCP   3h

# kubectl -n kube-system get deploy,service sample-ingress2
NAME                     DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/sample-ingress2   1         1         1            1           3h

NAME                  TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE
svc/sample-ingress2   LoadBalancer   192.111.123.71   10.10.10.47   80:32535/TCP,443:33424/TCP   3h

 

物理 Ingress Controller

Nginx Ingress Controller を利用する形で GKE ライクな Ingress Controller を自作しましたが、結局のところパフォーマンスを最大化するには L7 を捌ける Hardware LoadBalancer が欲しいなと思います。
現状の弊社の LoadBalancer では L7 を全て捌き切るのば難しいため、実装を見送りましたが、Hardware LoadBalancer を使った Ingress も実現することは容易なため、機会や利用感を見つつ検討したいと思っています。

 

Nginx / Nghttpx Ingress Controller の違い

Nginx を利用しているか Nghttpx を利用しているかの違いがありますが、Nghttpx Ingress Controller は Nginx Ingress Controller を参考に作られているため、基本的には同じだと思います。
Nghttpx の方は ZLab 様が公開してくれており、HTTP/2 に対応しているため、gRPC にも利用できるといった特徴があります。
また、今回実装した GKE Like Ingress Controller はどちらも選択できるようにしています。

 

以上、Ingress Controller を作ってみたお話でした。
KubeCon から帰ってきたら、じきに OSS で公開しようかな―と思っています。

]]>
[Kubernetes] Sonobuoy を使って Kubernetes Cluster の E2E テストを行なう https://cyberagent.ai/blog/tech/10208/ Thu, 30 Nov 2017 00:00:01 +0000 https://cyberagent.ai/techblog/?p=3739 こんにちは。
Central Infrastructure Agency の青山 真也です。

もうじき KubeCon + CloudNativeCon 2017 ですね。今から非常に楽しみです。

 

本日は GKE like な弊社のオンプレコンテナ基盤 AKE でも利用している E2E テストツールの Sonobuoy と Certified Kubernetes Platform & Distribution についてご紹介したいと思います。

 

AKE では、”type LoadBalancer” のために Kubernetes に対してパッチをあてていたり、1 から Kubernetes Cluster を構築しているため E2E テストが欠かせません。

そのため、AKE でもこの Conformance Program でも使われている Sonobuoy を使った E2E テストを実施しています。

現状は商用利用かつ CNCF のメンバーでもないため Certified Kubernetes に認定はされていないのですが、E2E テスト自体はパスしているため事実上互換性のある Kubernetes Platform として提供出来ています。

 

CNCF と Certified Kubernetes Platform & Distribution

コンテナオーケストレーションエンジンのデファクトスタンダードともなってきた Kubernetes ですが、様々なクラウドプロバイダーが提供する Kubernetes Platform、OSS ツールや各ベンダーが拡張して出している Kubernetes Distribution など色々な Kubernetes 環境が存在します。

そのため、ユーザはこれらの Kubernetes 環境を利用する際に Kubernetes としての機能を正しく保有しているかどうかを確認することが必要でした。

Kubernetes を管理・運営している Cloud Native Computing Foundation (CNCF) はこの問題に対して、これらの Kubernetes Platform & Distribution が正しく Kubernetes として動作していることを保証するために Conformance Program を提供しています。

Conformance Program は Kubernetes 1.7 以降に対応しています。

 

Conformance Program では、下記の 4 つのフェーズを行なう必要があります。

1.Certified Kubernetes についての説明を読む

2.Sonobuoy による E2E テストを行なう

3.CNCF に連絡・Github にデータを PR であげる

4.CNCF からの回答を待つ

 

 

1つ目の項目に関しては Certified Kubernetes に認定された場合にロゴマークの使用が許可される等の規約的なものやこのプログラムの目的などが書かれています。

4つ目の項目に関しては、3 で申請した後のレビュー待ちの状態です。

そのため、認定を受ける際に行なう作業は主に2番目と3番目の項目になります。

 

E2E テストツール Sonobuoy

Kubernetes クラスタが機能として機能しているかどうかを確認するために、Conformance Program では E2E テストツールの Sonobuoy を利用します。

ちなみに日本語ではソノブイと発音します。

Sonobuoy は航空機から海へ投下して利用するソナー内蔵の対潜水艦用捜索機のことだそうです。

Sonobuoy は Sonar(ソナー)+ Buoy(ブイ、浮き)の造語です。

Kubernetes が正しく動作するかどうかをチェックするというような意味合いが込められてるんじゃないでしょうか。

 

Sonobuoy を実行するには下記のコマンドを実行します。

実行後は Sonobuoy 用の Pod や Pod が利用する ConfigMap の他にも ClusterRole や ClusterRoleBinding や ServiceAccount などが展開されます。

また、全てのリソースは sonobuoy Namespace に配置されます。

なお、kubectl を実行するユーザは、ClusterRole や ClusterRoleBinding が作成可能なユーザで行って下さい。

$ curl -L https://raw.githubusercontent.com/cncf/k8s-conformance/master/sonobuoy-conformance.yaml | kubectl apply -f -

 

テスト自体は、

  • 各 Kubernetes API リソースのチェック
  • ネットワークの疎通性チェック
  • Internal Cluster Network のセグメントチェック
  • kubedns のドメイン名チェック
  • その他多岐にわたるテスト

など、結構細かく実施されます。

また、テストされている E2E テスト自体は Golang で書かれており、https://github.com/kubernetes/kubernetes/tree/master/test/e2e に実体があります。

Sonobuoy のチェック自体は大体 1 時間ちょっとかかるため、気長に待っている必要があります。

テストが終わった判定として下記のようにログを確認し、no-exit was specified, sonobuoy is now blocking が表示するまで待つ方法が公式に書かれていますが、終了判定の自動化が出来ません。

$ kubectl logs -f -n sonobuoy sonobuoy
time="2017-11-19T15:21:18Z" level=info msg="Scanning plugins in ./plugins.d (pwd: /)"
time="2017-11-19T15:21:18Z" level=warning msg="Unsupported plugin file detected ..119811_20_11_00_21_04.915096861"
time="2017-11-19T15:21:18Z" level=warning msg="Unsupported plugin file detected ..data"
time="2017-11-19T15:21:18Z" level=info msg="Scanning plugins in /etc/sonobuoy/plugins.d (pwd: /)"
time="2017-11-19T15:21:18Z" level=info msg="Directory (/etc/sonobuoy/plugins.d) does not exist"
time="2017-11-19T15:21:18Z" level=info msg="Scanning plugins in ~/sonobuoy/plugins.d (pwd: /)"
time="2017-11-19T15:21:18Z" level=info msg="Directory (~/sonobuoy/plugins.d) does not exist"
time="2017-11-19T15:21:18Z" level=info msg="Loading plugin driver Job"
time="2017-11-19T15:21:18Z" level=info msg="Namespace default Matched=true"
time="2017-11-19T15:21:18Z" level=info msg="Namespace kube-public Matched=true"
time="2017-11-19T15:21:18Z" level=info msg="Namespace kube-system Matched=true"
time="2017-11-19T15:21:18Z" level=info msg="Namespace sonobuoy Matched=true"
time="2017-11-19T15:21:18Z" level=info msg="Starting server Expected Results: [{ e2e}]"
time="2017-11-19T15:21:18Z" level=info msg="Running (e2e) plugin"
time="2017-11-19T15:21:18Z" level=info msg="Listening for incoming results on 0.0.0.0:8080\n"
time="2017-11-19T16:13:35Z" level=info msg="got e2e result\n"
time="2017-11-19T16:13:35Z" level=info msg="Creating directory /tmp/sonobuoy/de1791ab-33f2-4aa2-9767-e6fd32215bf0/plugins/e2e"
time="2017-11-19T16:13:35Z" level=info msg="extracted results tarball into /tmp/sonobuoy/de1791ab-33f2-4aa2-9767-e6fd32215bf0/plugins/e2e/results"
time="2017-11-19T16:13:35Z" level=info msg="Running non-ns query"
time="2017-11-19T16:13:35Z" level=info msg="Running ns query (default)"
time="2017-11-19T16:13:35Z" level=info msg="Running ns query (kube-public)"
time="2017-11-19T16:13:35Z" level=info msg="Running ns query (kube-system)"
time="2017-11-19T16:13:35Z" level=info msg="Running ns query (sonobuoy)"
time="2017-11-19T16:13:35Z" level=info msg="Results available at /tmp/sonobuoy/201711191521_sonobuoy_de1791ab-33f2-4aa2-9767-e6fd32215bf0.tar.gz"
time="2017-11-19T16:13:35Z" level=info msg="no-exit was specified, sonobuoy is now blocking"

 

もう一つの方法としてはテストが終了すると結果をアーカイブして保存することを利用してチェックを行なうと良いかと思います。

上の例だと、/tmp/sonobuoy/201711191521_sonobuoy_de1791ab-33f2-4aa2-9767-e6fd32215bf0.tar.gz がアーカイブされたファイルになります。

AKE でもバージョンアップや変更などがあった場合にオリジナルのシナリオテストの他に Sonobuoy による E2E テストを行っているため、自動化するために下記のようなことを行っています。

until ${KUBECTL} -n sonobuoy exec -it sonobuoy -- sh -c "ls /tmp/sonobuoy/*.tar.gz"; do
  sleep 60
done

 

アーカイブの中に結果のログなどが保存されているため、解凍後に確認します。

$ kubectl -n sonobuoy exec -it sonobuoy -- sh -c "tar -xvf /tmp/sonobuoy/*.tar.gz"
$ kubectl -n sonobuoy exec -it sonobuoy -- sh -c "cat plugins/e2e/results/e2e.log"
...(省略)...
Ran 125 of 697 Specs in 3119.168 seconds
SUCCESS! -- 125 Passed | 0 Failed | 0 Pending | 572 Skipped PASS

 

テスト結果が SUCCESS になっていれば問題ありませんが、FAIL している場合にはログを確認する必要があります。

少しログのメッセージが直結的ではない場合があるため、エラーが出たテストコードを直接 https://github.com/kubernetes/kubernetes/tree/master/test/e2e から確認したほうが楽だと思います。

テストコード自体は kubectl と同じく、In-cluster config を使った kubernetes/go-client を利用して行っていることがほとんどです。

 

認定依頼と Github への PR

晴れて E2E テストが完了したら、PR と CNCF への申請を行い Certified Kubernetes として認定してもらいます。

なお AKE は商用利用(プライベートクラウド)かつ CNCF のメンバーではないため、現状は認定依頼自体はしていません。

 

PR を行なう際には、https://github.com/cncf/k8s-conformance に対して各バージョン毎にディレクトリを切って申請する形になります。

例えば GKE の場合には v1.7/gke/ を作成し、conformance テストの結果や PRODUCT.yaml を配置して PR を出します。

PRODUCT.yaml には ベンダー名やバージョンといった情報を記載して提出します。

vendor: Yoyodyne
name: Turbo Encabulator
version: v1.7.4
website_url: https://yoyo.dyne/turbo-encabulator
documentation_url: https://yoyo.dyne/turbo-encabulator/docs
product_logo_url: https://yoyo.dyne/assets/turbo-encabulator.svg

PR 提出後は、ここ にあるフォームに記載した上で CNCF にメールを提出し、やりとりすることで認定してもらうことになるようです。

 

Certified Kubernetes Distribution & Platform の例

現状認定されている Certified Kubernetes の一覧は ここ から確認することが可能です。

GKE は勿論のこと Pivotal の PKS や Azure の AKS、他にも Rancher、kubeadm、Tectonic なども登録されています。

ちなみに、この Certified Kubernetes の認定を受けることで初めて、プロダクト名に “Kubernetes” を含むことが許されるようになります。 Google の GKE も Certified Kubernetes 認定されたタイミングで Google Container Engine から Google Kubernetes Engine に改名したのもそのためです。

AKE も OSS として公開すれば Certified Kubernetes として認定してもらいやすいかなとざっくり考えてもいます。

]]>
[イベントレポート] サイバーエージェントとさくらインターネットのインフラ談義 https://cyberagent.ai/blog/pr/kubernetes/3673/ Tue, 21 Nov 2017 03:00:16 +0000 https://cyberagent.ai/techblog/?p=3673 みなさんこんにちは。

アドテク本部の makocchi です。

 

先日さくらインターネット様と合同で勉強会を開催させて頂きました。

今回の勉強会のテーマを決めるにあたり、双方で検討した結果、最近のトレンドでもあるコンテナについてが良いだろうということで

コンテナまわりの話題についての勉強会となりました。

メインの発表の後に懇親会兼 LT 大会も開催され、夜遅い時間になったにもかかわらず最後までお客様が多くて大変盛況でした!

 

それではさっそく発表の振り返りを(makocchi が勝手に)していきましょう。

 

「Tectonic関連の発表(仮)」

最初に登壇して頂いたのはさくらインターネットの前佛様です。

CoreOS が開発している Tectonic について発表して頂きました。

実際に構築する際のハマリポイントや内部の構成について網羅されていたので、とても興味深い内容でした。

あまり Tectonic についての情報って世の中に無い(特に日本語)ので貴重な話だと思いました。

Tectonic 自体は Enterprise なものなので、基本的には有料なのですが 10 node までは無料でできるので、是非自分で構築してみたいと思います。

今まで名前は知っていたんですけども、あんまり詳しくなかったので参考になりました!

 

 

「On-premise コンテナ基盤とHardware LB を使った “type LoadBalancer”」

続いて弊社の青山より Kubernetes の type LoadBalancer をカスタマイズした話をさせて頂きました。

アドテク本部では OpenStack の上に GKE のように Kubernetes の cluster を展開できるサービスを提供しています。

この仕組みのことを我々は AKE (Adtech Container Engine) と呼んでいます。

この AKE の Kubernetes は OpenStack と連携しており、persistence volume を cinder から切り出したり認証を keystone に担当させたりしています。

しかしながら弊社の OpenStack の環境では Kubernetes 上で type LoadBalancer を作った場合に、そのままでは cluster 外から繋ぐことが出来なくて少々工夫が必要になります。

その少々工夫した部分について紹介させて頂きました。

 

「1年ほどDC/OSを運用してみた話」

引き続き弊社の和田より DC/OS の運用について話をさせて頂きました。

先程の Kubernetes と同じようにコンテナをオーケストレーションしてくれる DC/OS ですが Mesosphere が開発しています。

アドテク本部では分析基盤の構築に DC/OS が使われており、その運用まわりで苦労した点を中心にお話させて頂きました。

Kubernetes に比べれば圧倒的に情報量が少ない DC/OS ですが、今のところ DC/OS 自体には大きなトラブルはなく運用できています。

構築も難しくなく、これからコンテナ基盤を構築される方は DC/OS も検討してみては如何でしょうか。

余談ですが、こちらの blog にもある通り、DC/OS もついに Kubernetes を support し始めるそうです。

Kubernetes の勢いは止まら無さそうですね。

 

「LT大会」

最後にさくらインターネット様と弊社で 2 名ずつ LT を発表させて頂きました。

  • 「10分でわかる Cilium」
  • 「Dockerを始めて1週間のセキュリティエンジニア」
  • 「KubernetesでCLIを快適を使いたい」
  • 「高火力インスタンスのGPUについて」

中でも面白かったのは Cilium についての LT ですね。Cilium は Kubernetes の CNI の plugin として使うことができると初めて知りました。

Linux の kernel の BPF を使って実現していますが、新しめの kernel じゃないと対応してないです。

LT 内では実際のデモもして頂き、もりだくさんの内容で 正直 LT レベルでは無かったので今後機会があればじっくりお話を聞いてみたいところです。

また、さくらのクラウドの高火力インスタンスで使われている GPU の話もとても面白かったです。

 

今回の勉強会はさくらインターネット様と一緒に企画を考えながら運営させて頂きました。

申し込み人数が少なかったらどうしようと心配していましたが、たくさんの方に来て頂いてよかったです。

また機会がありましたら一緒に勉強会を開催していきたいと思います。ご期待ください。

 

最後に公開されている登壇資料を貼っておきます。みなさん是非御覧ください。

 

 

 

 

 

]]>