/

humstackについて

https://github.com/ophum/humstack

主な仕組み

今回作成したhumstackはkubernetesのようにリソース情報を保存するapiserverとそのapiで取得する情報を元にリソースを展開するagent(kubernetesで言うところのcontrollerやoperator)によって構成されています。
humstackでは主に以下のagentが動作しています。

  • VirtualMachineAgent
    • 仮想マシンを作成・削除します。
  • BlockStorageAgent
    • 仮想ディスクを作成・削除します。
  • NodeNetworkAgent
    • 仮想ネットワーク(Linux Bridgeやvlanインターフェース)を作成・削除します。

humstack dashboardについて

https://github.com/ophum/humstack-dashboard
humstackでは操作する方法としてWebUIであるhumstack dashboardを容易し問題の作成に利用しました。

UIから問題で使用するVMやディスクネットワークを定義できます。

dashboardでは、問題の作成だけでなくチーム毎の展開を行うことが出来ます。
上記の定義を元に各チーム毎に展開できます。ICTSCではノードの障害が起きたときに公平性が損なわれる可能性を減らすため、1つの問題は1つのノード上で動作させます。そのため、dashboardでは展開先のノードを指定できるようにしています。

humstackのCeph利用について

qemuの起動オプションで以下の指定をすることで、rbdのディスクを接続することが出来ます。

-drive file=rbd:image-name

ceph/go-cephを利用してcephイメージの作成・削除を実装しました。
問題VMの仮想ディスクのベースイメージ(問題を展開する際に元にするイメージ)化を行う際にスナップショットを作成し、スナップショットを利用して問題の仮想ディスクを作成することでディスク使用量を減らすことが出来ました。
@onokatioがhumstackにスナップショットの作成などの機能を実装してくれました。

ベースイメージが5GB, 5VMの場合
※ αは起動後に増える使用量

1チーム使用量20チーム使用量
スナップショット未使用25GB + α500GB + α
スナップショット使用25GB + α25GB + α

Cephクラスタについて

分散ストレージの一種で、複数のマシンでストレージデバイスを管理し、多重に保管することでデータの完全性を高めることができます。
今回は問題VMのブロックストレージを保管する目的で使用しました。

Cephの特徴

  • 各HDD/SSDにOSDと呼ばれるサービスが動作し、モニターと呼ばれるサービスがどのOSDにどのデータが置いてあるかを管理する
  • モニターは冗長構成のためある程度生き残っていれば読み書きに問題がない
  • データはデフォルトで3OSDに複製されるので、ある程度生き残っていれば読み書きに問題がない
  • NFSやS3互換API、RBDなどのインターフェイスでデータを操作できる

使われ方

humstackの章で先述したように、「RBD」と呼ばれる、仮想的なブロックストレージを複数作りCephに保存できる仕組みを採用しました。

またスナップショット機能と呼ばれる、親となるブロックストレージからCopy on Write方式で子となるブロックストレージを生成できる機能を利用しました。データ変更・削除のみが子に保存され、それ以外のデータ読み込みは親へとパススルーされます。

これにより、問題のブロックストレージを各チーム分コピーしても、物理ディスク使用量は数倍にはならず、実際には各チームごとの変化分のみデータが増加する程度に抑えられました。
今までICTSCでは電源やメモリ、ストレージが足りない問題が多々ありましたが、今回はCephスナップショットの採用で大変余裕を持ったストレージ設計が行えました。

構成

以下の構成を行いました。

  • SSD 1TB x 6個(それぞれ別サーバーに配置)
    • 主にVMのブロックストレージ系をこのSSDからなるプールに保管しました
  • HDD 2TB x 3個(それぞれ別サーバーに配置)
    • バックアップや雑多な目的に利用しました
  • モニター x 8サービス(それぞれ別サーバーに配置)
    • 意思決定には奇数台である必要があるため、実際には常に1台はスタンバイ状態でした。
  • マネージャー x 8サービス(それぞれ別サーバーに配置)
    • OSDやモニターの死活監視、スケール、更新や、ダッシュボードを提供していました。

性能

ホットステージ期間での負荷テストでは、CPUとストレージ帯域を100%まで使い切る様子が確認できました。
(正確なメトリクスが残っていないので概算ですが)SSDのみのプールの場合、最大IOPS・最大速度がSSD一つとより少し低いぐらいの性能が出ています。
各OSDへのレプリカがライトバック方式で行われていること、ネットワークが10Gでありボトルネックにならなかったことなどが起因して、出せる最大スペックでのストレージ性能が出ていたと考えられます。
結果的に、問題VMの全展開も数分程度に収まるようになりました。

また、ブロックストレージ以外にも雑多なデータ保管にCephを使いました。その際にはCephをLinuxにファイルシステムとしてマウントして利用しましたが、ローカルのストレージと同じ様に振る舞い、ファイルの故障なども起こりませんでした。

評価

構築中には様々な障害がありましたが、本戦期間中に大規模な問題なども起こらず、結果としてCephをストレージとして採用して成功たったと思います。
正直ここまでの速度が出るとは思わなかったので、予想以上の働きをしてくれました。

こんにちは。@takemioIOです。
この記事は ICTSC2020 k8s運用解説、前編:構築と構成の後編 にあたります。
ここでは以前の記事ではk8sを構築したことについてと全体像について述べました。ここではその構築したk8sをどのように利用したのか、それらを通じた知見を述べます。

CDについて

こんにちは。CI/CDに関する部分を担当した sakuraiです
今年はArgoCDHelmfileを利用してクラスタごとにスコアサーバー(コンテストサイト)などをデプロイする運用をしました。

背景

クラスタチームではKubernetesクラスタを3つ運用し、次のような位置づけとしています。

  • wspクラスタ(監視系、ダッシュボードなど)
  • devクラスタ(開発用テスト環境)
  • prdクラスタ(コンテスト参加者へ提供する本番環境)

各クラスタへのアプリケーションのデプロイを行うためには、そのクラスタに対してkubectl apply -f hogehoge.yamlといったコマンドを打つ必要があります。しかし、これを手作業で行うことは

  • 単純に手間
  • 人為的なミスが起こりうる
  • アプリケーション側の人間がクラスタを触る必要がある

ということがあります。そこで、クラスタへのデプロイを自動化したりクラスタチーム以外がk8s上にアプリケーションをデプロイするときの動線を整備したりすることによって、k8sとその上のアプリケーションの運用を継続的に行えることを目指しました。

Helmfile

まず初めに、これまでスクリプトでごり押されていたマニフェスト群をテンプレート化を行いました。テンプレート化にはKustomizeとHelmが候補となりましたが、環境変数の変更のしやすさの観点からHelmを使用することにしました。また、要件としてアプリケーションを各クラスタへデプロイする際、環境変数を変えることでConfigMapやimageを変更できる必要がありました。このテンプレート化では例えば、

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: {{ .Values.__VAR__NAMESPACE }}
...

values.yaml

__VAR__NAMESPACE: "scoreserver"

というようにマニフェストの一部を変数として、その変数に対応する内容を記したファイルを用意することで目的のマニフェストを出力することができます。

# helm template --values values.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: scoreserver
...

さらに、この変数の内容(values.yaml)を各クラスタと紐づけるためにHelmfileを利用しました。HelmfileではHelmのテンプレートに加えて環境を定義することができます。ここでdevelop/productionというように環境を定義し、環境変数も紐づけていきます。

helmfile.yaml

environments:
  workspace:
    values:
    - environment/workspace/values.yaml
  develop:
    values:
    - environment/develop/values.yaml
  production:
    values:
    - environment/production/values.yaml
...

単純な置き換えとしては次のようになります。

helm template --values environment/production/values.yaml

↓

helmfile --environment production template .

Argo CD

クラスタへのアプリケーションデプロイの自動化ツールとして、Argo CDを利用しました。Argo CDはGitHub上のマニフェストを参照して、アプリケーションのデプロイを行うことができます。また、GitHub上でそのマニフェストが変更された場合に自動でデプロイを行うことができます。したがって、Argo CD上でデプロイ設定を行った後はGitHub上でマニフェストを管理することでデプロイを行うことができるため、アプリケーション管理者がクラスタへ触る必要がなくなります。

(Argo CD自体の導入はとても簡単なので内容としては割愛します。GUIで設定できるし適当に使いたいときもおすすめできそう。)

Argo CDはhelmなどのテンプレートエンジンに対応していますが、Helmfileには対応してないためプラグインとして追加します。

deploy.yaml

spec:
  template:
    spec:
      # 1. Define an emptyDir volume which will hold the custom binaries
      volumes:
      - name: custom-tools
        emptyDir: {}
      # 2. Use an init container to download/copy custom binaries into the emptyDir
      initContainers:
      - name: download-tools
        image: alpine:3.8
        command: [sh, -c]
        args:
        - wget -qO /custom-tools/helmfile https://github.com/roboll/helmfile/releases/download/v0.128.0/helmfile_linux_amd64 && chmod +x /custom-tools/*
        volumeMounts:
        - mountPath: /custom-tools
          name: custom-tools
      containers:
      - name: argocd-repo-server
        volumeMounts:
        - mountPath: /usr/local/bin/helmfile
          name: custom-tools
          subPath: helmfile
kubectl -n argocd patch deploy/argocd-repo-server -p "$(cat deploy.yaml)"

アプリケーション情報の登録はCUIまたはGUIで行うことができるが、こちらもテキストで管理が可能です。

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: scoreserver-production
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: production
  source:
    repoURL: /* CENSORED */
    path: scoreserver
    targetRevision: master
    plugin:
      name: helmfile
      env:
        - name: ENV
          value: production
  destination:
    name: kubernetes-prd #ここでクラスタを指定する。Argo CDが動いているクラスタ以外は別途事前登録する必要がある。
    namespace: scoreserver-production 
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Argo CDにはadminユーザがデフォルトで設定されていますが、ユーザの追加を行ったり権限設定を行うことが可能です。(設定方法)
ICTSCではprdクラスタへの変更を制限して、本番環境への破壊や意図しない変更を防ぐようにしています。

Argo CD notifications

デプロイ結果をSlackに通知するためにArgo CD notificationsを使いました。
(比較的最近v1.0~になって変更が辛かったデスネ)

ドキュメントがアレなんですが、最低限以下だけすれば動きそうです。(別途Slack側でOauth tokenの発行は必要)

# kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj-labs/argocd-notifications/v1.0.2/manifests/install.yaml
# kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj-labs/argocd-notifications/v1.0.2/catalog/install.yaml
# kubectl edit cm -n argocd argocd-notifications-cm

data:
  defaultTriggers: |
    - on-deployed
    - on-health-degraded
    - on-sync-failed
    - on-sync-running
    - on-sync-status-unknown
    - on-sync-succeeded
# kubectl edit secret -n argocd argocd-notifications-secret

stringData:
  slack-token: CENSORED

以下のような通知が届くようになります。

監視基盤

こんにちは、監視基盤を担当した梅田@x86takaです。

今回は監視基盤として行ったことを説明します。

まず初めに、今年の監視基盤の構成を説明します。

構成

まず、最初に監視対象です。

監視対象はNTT中央研修センタをメインに行いました。

  • NTT中央研修センタ
    • サーバ
      • S7 * 2
      • m4 * 2
      • RH1288 * 4
    • ネットワーク機器
      • MX5
      • SRX1500
      • SN2410
  • さくらクラウド
    • k8sクラスタ
      • prd クラスタ
      • wsp クラスタ

NTT中央研修センタとさくらクラウドの構成は、前回の記事に詳細にかかれています。

また、これらの監視に利用したコンポーネントは以下の通りです。

  • 分析
    • Elasticsearch
  • データ収集
    • Logstash
    • Prometheus
    • Elastiflow
    • Zabbix
  • 可視化
    • Kibana
    • Grafana
  • その他
    • AlertManager

具体的にこれらのコンポーネントを、どのように利用したのかという話をしたいと思います。

Hardware監視

ここでは、サーバのIPMIから得られるデータやネットワーク機器(SNMP)のデータの監視についてです。

今回は、Zabbixを利用しNTT中央研修センタにある合計11台のホストを監視しました。

使用したテンプレートも含め、以下のスクリーンショットを載せておきます。

IPMIからは、サーバのハードウェアの状態の監視を行いました。

IPMIのデータ取得は、Zabbixのデフォルトの設定では行えません。
ENVにZBX_IPMIPOLLERS=1 のように、0以上の値を設定することにより取得できるようになります。

            - name: ZBX_IPMIPOLLERS
              value: "1"

今回は準備期間中に、HDDの故障が発生したりしていましたので重要な監視になりました。

(Problemのメッセージ)

hardDisk [1] status major

また、ネットワーク機器はSNMPによる監視, メトリクスの取得を行いました。

後述する、GrafanaでZabbixで取得した、対外トラフィックの可視化を行いました。

サービス監視

主にPrometheusを利用し、Grafanaで可視化を行いました。

Prometheus

Prometheusでは Node-expoter, Ceph-expoter, BlackBox-expoterなどを利用しデータの収集を行いました。

Prometheusのデータの永続化についてです。
監視項目が多い場合かなりのストレージを使うため、注意が必要でした。

ICTSC2020の場合、10日間で20GBのストレージを消費しました。

また、PrometheusのDBのサイズが肥大化し起動に時間がかかるようになっていました。
長期の運用を考える場合に考え直さなければならない部分だと思っています。

Deploy

ICTSC2020ではArgoCDによるデプロイを行っています。
しかし、ConfigMapに書かれている設定の変更時にPodの再起動が行われません。

そのため、DeployのアノテーションにConfigMapのhash値をいれておき、ConfigMapに変更があったときのみPodのUpdateが行われるようにしました。

このような形です。

      annotations:
        checksum/config_volume: {{ $.Files.Get "templates/prometheus-configmap.yaml"| sha256sum }}
        checksum/blackbox_volume: {{ $.Files.Get "templates/blackbox_exporter-configmap.yaml"| sha256sum }}
        checksum/ceph_target_volume: {{ $.Files.Get "templates/ceph-exporter-target.yaml"| sha256sum }}
        checksum/node_volume: {{ $.Files.Get "templates/node_exporter-configmap.yaml"| sha256sum }}

Grafana

Grafanaでは、DatasourceにPrometheus, Zabbixを利用し可視化していました。

全体で3つGrafanaが存在したため、Dashboardのjsonをk8sのConfigMapで管理しています。

改善点

ICTSCではConfigMapの定義yamlに、すべてのdashboardのjsonが以下のように一つのファイルに書かれています。

---
apiVersion: v1
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: grafana-import-dashboards
  namespace: monitoring
data:
  grafana-net-2-dashboard.json: |
    {
      "__inputs": [{
        "name": "DS_PROMETHEUS",
        "label": "Prometheus",
        "description": "",
        "type": "datasource",
        "pluginId": "prometheus",
        "pluginName": "Prometheus"
      }],
      "__requires": [{
        "type": "panel",
        "id": "singlestat",
        "name": "Singlestat",
        "version": ""
      }, {
        "type": "panel",
        "id": "text",
        "name": "Text",
        "version": ""
      }, {
      .........

しかし、この状態だとdashboardの変更した際にConfigMapの定義ファイルとjsonが同じところにあるため書き換えが大変でした。

そのため、jsonとConfigMapの定義を分離することにしました。

apiVersion: v1
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: grafana-import-dashboards
  namespace: monitoring
data:
  grafana-net-2-dashboard.json: |-
{{ .Files.Get "dashboard/grafana-net-2-dashboard.json" | indent 4}}

templateに置かれているConfigMap定義から、別ディレクトリに置かれているjsonを読み込む形に変更しました。

その際にindent 4 をつけることによって、yamlに読み込まれた際のインデントを気にする必要がなくなります。

  k8s-dashboard.json: |-
{{ .Files.Get "dashboard/k8s-dashboard.json" | indent 4}}

また、サーバの監視にGrafana.comに公開されているdashboardを利用しました。

https://grafana.com/grafana/dashboards/11074

上にある公開されているDashboardの一部において、jsonの定義でConfigMapのサイズ制限を超えてしまう問題が発生しました。
GrafanaのAPIを利用して、Grafana.comから取得したデータのUploadを行い回避を行いました。

grafana_host="http://grafana:3000";
grafana_cred="${GF_ADMIN_USER}:${GF_ADMIN_PASSWORD}";
grafana_datasource="prometheus";
ds=(2842 1860 11074);
for d in "${ds[@]}"; do
  echo -n "Processing $d: ";
  j=$(curl -s -k -u "$grafana_cred" $grafana_host/api/gnet/dashboards/"$d" | jq .json);
  echo "{\"dashboard\":$j,\"overwrite\":true, \
        \"inputs\":[{\"name\":\"DS_PROMETHEUS\",\"type\":\"datasource\", \
        \"pluginId\":\"prometheus\",\"value\":\"$grafana_datasource\"}]}" \
  | curl -s -k -u "$grafana_cred" -XPOST -H "Accept: application/json" \
    -H "Content-Type: application/json" \
    $grafana_host/api/dashboards/import -d "@-" ;
  echo "" ;
done

今回は行いませんでしたが、Grafana.comのようなPrivateなDashbordの公開場所を設けるなど、
k8sのConfigMapに書き込まない方がよいと感じました。

また、以前公開しているPomeriumの記事に関連してGrafanaの認証をGitHubで行えるようにしています。

以下のような設定をgrafana.iniに書くことによってGitHub OAuth2を有効にしました

    [auth.github]
    enabled = true
    allow_sign_up = true
    client_id = {{ .Values.github_clientid }}
    client_secret = {{ .Values.github_client_secret }}
    scopes = user:email,read:org
    auth_url = https://github.com/login/oauth/authorize
    token_url = https://github.com/login/oauth/access_token
    api_url = https://api.github.com/user
    team_ids = xxxxxxx
    allowed_organizations = ictsc

GitHub Organizationで利用している場合、team_idsという設定項目でGitHub Organizationの特定のTeamのみ利用できるといった指定をすることができます。

team_idsというものは、チーム名ではなくGithubAPIから取得できる数字のIDを書かなければなりません。

GitHubのwebからは取得できないため、curlで取得する必要があります。

GrafanaのSession管理

また、Grafanaはセッション管理などにSQLiteを利用しています。

Grafanaをレプリカを行って運用する場合にはDBのLockがかかってしまうことがあります。

具体的に、DBのLockがかかるとGrafanaから突然ログアウトされるような現象が発生します。

そのため、MySQLサーバなどを用意しgrafana.iniで以下のようにデータベース接続の設定を行うことで解消します。
databaseに接続情報を設定し、[session] をmysql変更します。

[database]
    # You can configure the database connection by specifying type, host, name, user and password
    # as seperate properties or as on string using the url propertie.
    # Either "mysql", "postgres" or "sqlite3", it's your choice
    type = mysql
    host = helm-grafana-mysql:3306
    name = grafana
    user = ictsc
    # If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
    password = hogehoge
[session]
     # Either "memory", "file", "redis", "mysql", "postgres", default is "file"
     provider = mysql 

ネットワーク監視

次は、ネットワーク監視についてです。

主に、DataSourceはSNMP, sflowからデータを取得を行いました。

監視対象は、NTT中央研修センターにあるネットワーク機器です。
繰り返しになりますが、列挙しておきます。

  • ネットワーク機器
    • MX5
    • SRX1500
    • SN2410

これら3台の機器を、ZabbixからSNMPでデータ取得を行いました。

これらの機器のICTSC2020での使用用途を簡単に説明します。

  • MX5
    HomeNOC様とのBGPフルルートを受け取っているルータです。
    2拠点と接続し冗長化を行っています。
  • SRX1500
    MX5の配下に接続されている、Firewallです。
  • SN2410
    サーバ間の通信など、データ通信のコアスイッチとして利用しています。

これらの機器から取得したデータの可視化を行いました。

まず、HomeNOC様との対外トラフィックの可視化についてです。

以下は、取得したデータをZabbixのScreenで表示させたのものです。

今回はGrafanaで様々な監視のdashboardを扱っていますので、GrafanaでZabbixのデータを表示を行います。

GrafanaからZabbixのデータを表示するために、以下のPluginのインストールを行います。
https://grafana.com/grafana/plugins/alexanderzobnin-zabbix-app/

この際、zipファイルからインストール作業をする必要はなく、k8sのマニフェストからENVでインストールするプラグインを指定できます。

          - name: GF_INSTALL_PLUGINS
            value: "alexanderzobnin-zabbix-app"

今回、ZabbixをDatasourceとしたDashboardとして対外トラフィックの可視化を行いました。

以下は、本戦二日間の実際のDashboardになります。

Elastiflow

Elastiflowを利用したflow情報の可視化です。

構築はElastiflowのdocker-composeファイルを参考にk8sのtemplateを書いて構築をしました。
https://github.com/robcowart/elastiflow/blob/master/docker-compose.yml

参考までに、作成したelastiflow-logstash用のファイルを載せておきます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: elastiflow
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: elastiflow
  replicas: 2
  template:
    metadata:
      labels:
        app: elastiflow
    spec:
      containers:
        - name: elastiflow
          image: robcowart/elastiflow-logstash:4.0.1
          env:
            - name: LS_JAVA_OPTS
              value: "-Xms3g -Xmx3g"
            - name: ELASTIFLOW_AGENT_ID
              value: "elastiflow"
            - name: ELASTIFLOW_GEOIP_CACHE_SIZE
              value: "16384"
            - name: ELASTIFLOW_GEOIP_LOOKUP
              value: "true"
            - name: ELASTIFLOW_ASN_LOOKUP
              value: "true"
            - name: ELASTIFLOW_OUI_LOOKUP
              value: "true"
            - name: ELASTIFLOW_POPULATE_LOGS
              value: "true"
            - name: ELASTIFLOW_KEEP_ORIG_DATA
              value: "true"
            - name: ELASTIFLOW_DEFAULT_APPID_SRCTYPE
              value: "__UNKNOWN"
            - name: ELASTIFLOW_RESOLVE_IP2HOST
              value: "true"
            - name: ELASTIFLOW_NAMESERVER
              value: "127.0.0.1"
            - name: ELASTIFLOW_DNS_HIT_CACHE_SIZE
              value: "25000"
            - name: ELASTIFLOW_DNS_HIT_CACHE_TTL
              value: "900"
            - name: ELASTIFLOW_DNS_FAILED_CACHE_SIZE
              value: "75000"
            - name: ELASTIFLOW_DNS_FAILED_CACHE_TTL
              value: "3600"
            - name: ELASTIFLOW_ES_HOST
              value: "elasticsearch:9200"
            - name: ELASTIFLOW_NETFLOW_IPV4_PORT
              value: "2055"
            - name: ELASTIFLOW_NETFLOW_UDP_WORKERS
              value: "4"
            - name: ELASTIFLOW_NETFLOW_UDP_QUEUE_SIZE
              value: "4096"
            - name: ELASTIFLOW_NETFLOW_UDP_RCV_BUFF
              value: "33554432"

            - name: ELASTIFLOW_SFLOW_IPV4_PORT
              value: "6343"
            - name: ELASTIFLOW_SFLOW_UDP_WORKERS
              value: "4"
            - name: ELASTIFLOW_SFLOW_UDP_QUEUE_SIZE
              value: "4096"
            - name: ELASTIFLOW_SFLOW_UDP_RCV_BUFF
              value: "33554432"

            - name: ELASTIFLOW_IPFIX_UDP_IPV4_PORT
              value: "4739"
            - name: ELASTIFLOW_IPFIX_UDP_WORKERS
              value: "2"
            - name: ELASTIFLOW_IPFIX_UDP_QUEUE_SIZE
              value: "4096"
            - name: ELASTIFLOW_IPFIX_UDP_RCV_BUFF
              value: "33554432"
---
apiVersion: v1
kind: Service
metadata:
  name: elastiflow
  namespace: monitoring
  annotations:
    metallb.universe.tf/address-pool: privateIP
spec:
  ports:
    - name: netflow-port
      port: 2055
      protocol: UDP
      targetPort: 2055
    - name: sflow-port
      port: 6343
      protocol: UDP
      targetPort: 6343
    - name: ipfix-port
      port: 4739
      protocol: UDP
      targetPort: 4739
  selector:
    app: elastiflow
  type: LoadBalancer

ElastiflowのDashboardは以下にあるjsonファイルをKibanaからimportを行うことによって、みれるようになります。

https://github.com/robcowart/elastiflow/tree/master/kibana

コンテスト中のflow数はこのような形です。

監視基盤については以上です。

終わりに

今回k8sをどのように利活用したかについて説明しました。これが今回の我々の成果になります!
前編・後編と説明しましたが面白く読んでもらえましたでしょうか?
最後になりますが参加してくださった皆さん、スポンサーとしてリソース提供をしてくださったさくらインターネット様ありがとうございました。

もし今回の記事が皆さんがk8sを運用するにあたっての参考になれば幸いです。

今回のk8sを利用したインフラの取りまとめを担当した@takemioIOです。

みなさん1年間コンテストに参加してくださってありがとうございました。
今回のコンテストも楽しんでいただけたでしょうか。

今回も引き続き機材提供してくださいました企業様、新規で機材提供して頂いた企業様、誠にありがとうございました。お陰様で無事開催ができ、運営学生もインフラ技術を学ぶことができました。特にk8sの担当者としてはさくらインターネット様のクラウドリソースを十二分に使わせていただきましたことを感謝いたします。

今年一年を通じて我々は k8s を利用し、スコアサーバー、監視基盤、問題用VMプロビジョニングツールの運用などを行ってきました。
この記事は我々が行ったk8sに関する利活用を解説する記事となっており、各担当者の寄稿文形式で行います。
まず前編ではネットワークやストレージなどのk8s自体に関する構築と構成について、後編では構築したk8sの利活用についてスポットを当てCDや監視などの運用についてを述べたいと思います。

全体像

このセクションでは今回の全体像について述べます。
我々のk8sはさくらのクラウド上に構築されています。
単一のk8sクラスタの構成と乗っかるコンテンツとしては以下の通りです。

画像に示している様にネットワーク周りではL4LBにMetalLB, L7LBにNginx Ingress Controllerを利用し、ストレージには Rook + Ceph、監視には Prometheus, Grafana, ELK, Elastiflow…etcと言った形で利用していました。

これらは予選と本戦通じて利活用されました。
我々の運用しているk8sクラスタは以下に示す合計3つとなります。

  • prd: 参加者がアクセスするためのスコアサーバーなど絶対に落とせないものを載せるクラスタ
  • dev: 実験のための利用がメインで、カナリアとして利用するためのクラスタ
  • wsp: 監視やツールなどを展開できる実験に巻き込まれるわけにはいかないが prd と共存させたくない時に使うクラスタ

また本戦においては問題が展開されているコアネットワークとVPNを貼る必要があり、以下の様な構成になっていました。この様にすることで本戦においての監視などは全てk8s上に載せることが可能になりました。

以降のセクションではマネージなk8sを運用していれば 全く ハマることがなく関わること少ないような部分だと思いますが、どのように転びながら構築をしたのかという話をします。

ストレージ

このセクションではk8sで利用してるストレージ周りについてを説明します。
今回の問題:Nginxが展開できない でも題材になっていましたが Rook + Ceph を我々は採用をしました。(実はこの問題は我々の構築においてもうっかり消し忘れていたことから起きたものでもありました)

Rookとは OSSのクラウドネイティブなストレージオーケストレーターと言うモノで、ストレージのソフトウェアをスケーリングや監視、スケーリングなどの機能ををk8sのエコシステムに載せることで高い可用性を実現したツールです。

今まで StateFulSet を利用する際に hostPath/data などを書いて適当にマウントする運用をしており、データの管理が大変辛かったと言うのがありました。

例えばStateFulSetを複数利用したいと考えると hostPath を複数書くことになります。その際に新しく追加したいと考えた際には /data/1, /data/2,/data/3…etc とどれがどこで何を使ってるのかわからない悲しいことが起きます。また雑にデータを処分することも叶わず rmコマンドを叩いて苦痛を伴った運用でカバーを行う羽目になっていました。

そこで、Rookを導入することで分散ストレージに加えてk8sでPV/PVCを使える様に我々クラスタにおいても可能になりました!
Cephの採用理由はCephであれば充分に枯れているので信頼性があると言う点やVeleroなどを併用することでDisasterRecoveryやDataMigrationが可能になると言うところや耐障害性を求めることができるのが嬉しい点としてあります。

また単純にRookの現在の安定バージョンのストレージプロバイダーは Cephのみであることからこれを選択することなりました。

Rook + Cephを利用した構築については本家ドキュメントが参考になるので今回は言及しませんのでそちらをご覧ください。

ネットワーク

k8sの運用構築担当の東畑@siberiy4です。 以降の解説記事は私が担当します
ここではk8sがおいてあるVPCの構成や、Metal LBの利用法についてを説明します。
まず、今年度のVM構成としては、

  • KubernetesのAPI用LB 2台
  • Kubernetesのマスター 3台
  • VyOS 1台
  • Kubernetesのノード 3台

となっていました。
API用LBと、Kubernetesのマスター、VyOSは対外疎通できるスイッチに接続しています。
プライベートなスイッチを作成し、KubernetesのノードとVyOSを接続しています。
この状態ではKubernetesのノードが対外疎通できないため、Kubernetesのノードがインターネット通信を行うためにVyOSにはProxy ARPの設定を入れています。

kube-apiserver の冗長

Kubernetesの冗長化のため、Kubernetesのマスターを複数台立てています。
複数のマスターを利用するためには、各マスターに対ししてロードバランスしてあげる必要があります。
そのためにLBとして HAProxyが稼働しているVMを2台用意しています。
2台利用している理由としては、単純にHAPorxyを1台稼働させた場合はSPOFとなってしまいます。
この対策として、2台用意しKeepalivedによってアクティブ・スタンバイ構成にしています。

アプリの外部公開

K8s上のアプリケーションを外部に公開するためのLoadBalancer Serviceを作成するため、MetalLBを利用しています。
昨年度のコンテストではMetalLB をLayer2モードで稼働させていました。
Layer2モードの場合、選出された単一のノードがすべてのトラフィックを受け取ります。
このため、ノードごとに処理負荷の偏りが発生してしまいます。

これを解決するために、MetalLBをBGPモードで利用することにしました。
BGPモードで利用するにはクラスタ外に用意したルーターが必要です。
用意したルーターとk8sの各ノードにあるMetalLBのSpeakerがBGPのピアを張ることで ECMPによるロードバランスができます。
そのため、VyOSを追加しそのVyOSに全てのKubernetesのノードを接続するように変更しました。
以下に 今年のクラウド上のトポロジー図を示します。

image.png

この追加したVyOSを利用して、コンテスト会場と各クラスタでVPN接続を行いました。 VPNソフトとして、Wireguardを利用しています。
これによって、WSPクラスタからすべてのリソースの監視が可能になります。

また、MetalLBでは複数のアドレスプールを用意することができます。
プライベートアドレスのプールを用意することで、コンテスト会場などの運営のみにアプリケーションを公開もできます。
使用するアドレスプールはServiceで指定します。

apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    peers:# BGPのピアの設定
    - peer-address: K8S_BGP_ROUTER_IPADDRESS 
      peer-asn: 65020
      my-asn: 65021
    address-pools:
    - name: globalIP
      protocol: bgp
      addresses:# e.g.- 192.0.2.1-192.0.2.254 or 192.0.2.0/24
      - METALLB_ADDRESS_RANGE 
      - METALLB_ADDRESS_RANGE 
    - name: privateIP
      avoid-buggy-ips: true
      protocol: bgp
      addresses:
        - PRIVATE_ADDRESS_RANGE
apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: scoreserver
  annotations:
    metallb.universe.tf/address-pool: globalIP
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
spec:
  type: LoadBalancer
  ports:
  - name: http
    port: 80
    targetPort: 80
    protocol: TCP
  - name: https
    port: 443
    targetPort: 443
    protocol: TCP
  selector:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx

Ingress

SSLの終端やパスベースのルーティングを行うために、Ingressを利用しています。
昨年と同様に、Ingress ControllerとしてNginx Ingress Controllerを 使用しています。

クラスターのプロビジョニング

引き続き東畑が解説をします。

さくらのクラウドにKubernetesのマネージドサービスがないため、VMを作成しKubeadmによってKubernetes クラスタを構築しています。
手順としては、

  1. Terraform でVM作成
    Terraform for さくらのクラウド(v2)を利用してTerraform で各VMを作成しています。
    クラスタ作成時、さくらのクラウドでオブジェクトストレージのサービスが存在しなかったため、minioを稼働させたVMを作成しtfstateを保存しています。
  2. AnsibleでKubeadmのインストール/VyOSのBGPのコンフィグ投入
    Ansible のDynamic Inventory として、minioに保存されたtfstateを解釈しJSONとして出力してくれるPythonスクリプトを利用しています。
    Ansibleのplaybook は二つあり、一つ目はVyOS用です。
    Terraform for さくらのクラウド では複数インターフェースのIP設定はできないため、VyOSのKubernetesのノード側インターフェースにIPを割り当てをしていません。 そのための措置として、Terraform outputにあらかじめ出力してあるVyOS用のIPをインターフェースに設定します。
    また、VyOSにBGPのConfig、MetalLBから広報されたアドレスとKubernetesのノードを対外疎通させるためのProxy ARPなどのネットワーク設定をします。
    二つ目はサーバーのパッケージインストール用です。
    Keepalived、HAProxy、Kubeadmのインストールや、管理用ユーザーの追加を行います。
  3. 手動でKeepalived、HAProxy,kubeadmによるクラスタ構築。
    以降手作業。 Keepalived、HAProxy、kubeadmは設定ファイルのひな型が用意してあり、それらを各VMに適用します。
    あとは、MetalLB, Cert Manager, Nginx Ingress Controller, Argo CDなどをApplyすればKubernetesクラスタの土台が出来上がります。  
    課題としては、terraformの実行からKubernetesクラスタの構築まで2,3時間かかるのを眺める必要がありますのでpackerなどを利用しvmを事前にイメージ化するなどをしておくことを検討すべきだったなと思いました。

終わりに

今回は k8sの全体像と構築にあたっての詳細なエッセンスについて説明しました。

昨年私が一番最初に立てたk8sのクラスタはLBがそもそもL7のみでその単一のhostpathで上手いこと一つのアドレスを利用し複数のアプリケーションを公開していました。そこから振り返ると今年はようやく人が触れても問題なさそうなものになってきたなと思いました。

次回はk8sの利活用についてスポットを当て紹介します。

 /

問題文

先輩がMySQLサーバを立てて、新しく運用を開始したらしい。

そのサーバを利用するため、rootパスワードを教えて貰ったため、今までどおりのログインコマンドを試してみた。

$ mysql -uroot -p -h 192.168.0.100

だが、なぜかログインエラーが出てしまい先輩のサーバにアクセスできない。先輩はサーバ上で正しく動いているらしいので、トラブルシューティングのためにそのサーバへのアクセス権限をもらった。

クライアントサーバから上記のコマンドを実行し、rootユーザがログインできるように変更を加え、下記の内容を報告してほしい。

  • ログインできるようになるためどのような作業を行ったのか
  • なぜログインが行えなかったのか

問題サーバー

クライアント

サーバ

  • IPアドレス: 192.168.0.100
  • ユーザー: admin
  • パスワード: [email protected]
  • DBユーザー: root
  • DBパスワード: root

問題解説

本問題ではMySQL 8系から導入された認証形式であるcaching_sha2_passwordが古いMySQLクライアントでは対応していないために発生しているエラーでした。

クライアントサーバから実際にログインしてみようとコマンドを実行すると以下のようなエラーが出ていたかと思います。

$ mysql -uroot -p -h 192.168.0.100
Enter password:
ERROR 2059 (HY000): Authentication plugin 'caching_sha2_password' cannot be loaded: /usr/lib/mysql/plugin/caching_sha2_password.so: cannot open shared object file: No such file or directory

このエラーメッセージからなんらかの認証回りでエラーが起きており、回答に気づくシナリオでした。

実際にMySQLサーバの起動しているホストにログインするとMySQL 8系のパッケージがインストールされており、クライアント側のサーバではMySQL 5.7.21のパッケージがインストールされていました。

具体的な回答としては以下のものを想定していました。

  • クライアント側のサーバにインストールされているMySQLクライアントのバージョンをcaching_sha2_passwordに対応したものをインストールする
    • 5.7系であれば5.7.23以上、8系であればどのバージョンでも対応しています
  • rootユーザのログインに用いる認証形式を古い形式に変更する
    • mysql_native_passwordに変更する事で古いMySQLクライアントでもログインすることが可能になります

ちなみに、問題文の中で「先輩がMySQLサーバを立てて運用を開始した」という文面がありましたが、実際にMySQLサーバ側からmysqlコマンドを用いると特にエラーが出力されないままログインに成功します。
MySQLサーバ側にはMuSQL 8系のパッケージがインストールされており、MySQLクライアントも8系のものがインストールされているためcaching_sha2_passwordに対応しておりログインが可能であった、というシナリオでした。

この問題を通して新しいMySQL認証プラグインであるcaching_sha2_passwordについて知っていただければ幸いです。

 /

問題文

あるデベロッパーから「dev環境と間違えて、prod環境のMySQLで直接truncate table sbtest3;を実行してsbtest3テーブルのデータを削除してしまいました、復旧をお願いします!!」と緊急の連絡を受けました。
このクエリの実行後、すぐに間違いに気づいたデベロッパーはサービスをメンテナンス状態にし、アプリケーションサーバを停止したそうです。

問1(q01)

truncate table sbtest3; というクエリが実行された日時をyymmdd HH:MM:SSのフォーマットで報告してください。
また、どのようにこの日時を特定したかを説明してください。

問2(q02)

truncate tableが実行される直前の状態(truncate tableが実行される1つ前のクエリが実行された状態)にデータを復旧し、復旧後checksum table sysbench.sbtest3;の結果を報告してください。
また、データの復旧に必要な手順を説明してください。

情報

状況

  • このMySQLは毎日定時にsysbench databaseのバックアップを取得していて(コンテスト問題の作成上truncate table文が実行された日まで)、偶然truncate文が実行される(数分)前にこの日のバックアップが完了していた
  • バックアップは以下のコマンドで取得されている
  • mysqldump --opt --single-transaction --master-data=2 --default-character-set=utf8mb4 --databases sysbench > /root/backup/backup.dump
  • mysql -u root -p < /root/backup/backup.dumpでバックアップが取得された時点に復旧できる
  • adminユーザからsudo suすることでrootユーザから操作してください

問題サーバー

  • IPアドレス: 192.168.0.1
    ユーザー: admin
    パスワード: [email protected]
  • DBユーザー: root
  • DBパスワード: root

ゴール

  • 問1, truncate table sbtest3; というクエリが実行された日時を特定し、その特定方法を説明していること
  • 問2, MySQLのデータが復旧し、その手順の説明とchecksum table sysbench.sbtest3;の結果が報告されていること
  • 問1に正解すると部分点が与えられます。

問題解説

「本番環境で間違ってtruncate table ...を実行してしまったから、その直前の状態までデータ復旧してほしい!!」
というデータベースの復旧問題でした。

あまり想像して楽しくなる状況ではありませんが、ソフトウェア開発をする上でデータベースのデータの復旧方法を知っておいても損はないと思います。
そこで、MySQLを例にバックアップと更新ログを利用して任意のタイミングまでデータを復旧する問題を出題しました。
DBMSごとの際はありますが、基本的にデータベースの復旧方法(特にロールフォーワードリカバリ)の概念は同じですので、今後使うDBMSでこういった状況に対応できるかを考えてみてください。

具体的な解法を説明する前にMySQLにおけるリカバリ方法、バイナリログ、バックアップについて説明します。

ポイント・イン・タイム・リカバリ (PITR)

データベースのバックアップを定期的に取得するだけでは、その時点のデータを復元することしかできないため、バックアップ以降の更新データを失うことになります。
そこで、データの永続化を保証する多くのデータベースでは、バックアップとは別にデータの更新情報を保持しておき、バックアップとそれ以降のデータ更新を復元することで任意の時点のデータをリカバリできるようにしています。このようなリカバリ方法をポイントインタイムリカバリ(PITR)と言います。

バックアップ以降のデータ更新情報をどのように保持するかはDBMSによって異なりますが、MySQLでは後述するバイナリログを利用することで取得できます。

バイナリログ

バイナリログにはMySQL serverで実行された更新がイベントという形で出力されています。
イベントには直接実行されたクエリが出力されているわけではなく、行ごとの変更分が特定のフォーマットにしたがって出力されています。
この他にもバイナリログそのもののバージョンやサーバの設定などのメタ情報も含まれており、これを利用してリカバリやレプリケーションを行うことができます。
truncate table ...を含むDDLはSTATEMENTが直接書かれていたり、推奨されませんが、DMLを直接STATEMENTに出力したりも出来ます。詳しくはバイナリログのドキュメントを参照してください

バイナリログの中身はmysqlbinlogコマンドを使うことで確認でき、より詳細に確認したい場合は--base64-output=DECODE-ROWS -vvなどのオプションをつけることで、実際に実行されたSQL文とは異なりますが、SQL文として復元されたものを確認できます。ただしクエリはコメントアウトされているので、このオプションをつけた結果をリカバリに使うことは出来ません。

バイナリログから特定の期間の更新を取得するには時間で指定する方法とバイナリログ中のオフセットを利用する方法がありますが、時間単位での指定は秒単位でしかできないため、リカバリにはオフセットを用いて指定する --start-position,--stop-positionを利用することをおすすめします。(秒間1クエリしか実行していない、なんてサービスはほとんどないはず)

バックアップ

MySQLにおけるバックアップ方法はいくつもあり実際は要件に合わせて選択しますが、今回はMySQL標準の論理バックアップクライアントのmysqldumpを利用してバックアップしました。
オプションの説明は省略しますが、問題環境では以下のコマンドでsysbench databaseの完全バックアップが取得されていました。

mysqldump --opt --single-transaction --master-data=2 --default-character-set=utf8mb4 --databases sysbench > /root/backup/backup.dump

--master-data=2が指定されていることから、バックアップを行ったホストに対してレプリケーションを繋ぐ際のコマンド(CHANGE MASTER...)がコメントアウトされて出力されます。この出力を見ることで、バックアップ以降の更新がバイナリログのどこから始まっているかをバイナリログのオフセットとして確認できます。

復旧手順

それでは実際に今回の問題環境でPITRを実施してデータを復旧する方法を説明します。

PITRの項で説明したように、まずはバックアップをリストアし、バックアップ以降でtruncate table sbtest3;の実行直前までの更新をバイナリログから抽出して適用することで復旧します。

mysqldumpによるダンプファイルの22行目に以下の出力があることからバックアップ以降の更新はbinlog.000018の34626719byte目以降ということがわかります。

-- CHANGE MASTER TO MASTER_LOG_FILE='binlog.000018', MASTER_LOG_POS=34626719;

次に、バイナリログからmysqlbinlogコマンドを使ってtruncate table...を実行を表すイベントのオフセットを取得します。
問題環境ではデータの更新が記録されているのはbinlog.000018だけなので、このファイルについてだけ考えれば良いです。

$ mysqlbinlog --no-defaults --base64-output=DECODE-ROWS -vv --start-position=34626719  binlog.000018 | grep -B 10 truncate
/*!80001 SET @@session.original_commit_timestamp=1574235055802260*//*!*/;
/*!80014 SET @@session.original_server_version=80018*//*!*/;
/*!80014 SET @@session.immediate_server_version=80018*//*!*/;
SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/;
# at 54452036
#191120 16:30:55 server id 1  end_log_pos 54452141 CRC32 0x7e67e1d2     Query   thread_id=11    exec_time=0 error_code=0    Xid = 189179
use `sysbench`/*!*/;
SET TIMESTAMP=1574235055/*!*/;
/*!\C utf8mb4 *//*!*/;
SET @@session.character_set_client=255,@@session.collation_connection=255,@@session.collation_server=255/*!*/;
truncate table sbtest3

この出力の# at {数字}を見ることでtruncate文を実行したのが54452036 byte目から始まるイベントとわかります。
また、次の行をみると191120 16:30:55に実行されたことがわかります (問1)

問1としてあえて秒単位でしかない実行時刻を解答してもらったのは、実際にこういった問題が起こった場合には似たようなクエリがアプリケーションの正常な処理として実行されていたり、実は何回も間違った後だったりするため確認作業を必要とすることが多いからです。
辛いですね。。。

ここまででわかったbinlogの状況を図1に整理してみます。

スクリーンショット 2019-12-08 15.10.27.png
図1 binlogの状況

バックアップを適用することで②の直前の状態にまでデータが復旧されるため、②から③までの更新情報を適用する必要があります。
①から②の部分も適用したチームも見られましたが、この部分の更新分はバックアップに含まれているので、重複した更新を適用してしまうことになります。

最後にここまでで取得したオフセットを--start-position--stop-positionで指定して抽出し、適用することでリカバリが完了します(問2)

mysqlbinlog --no-defaults --start-position=34626719 --stop-position=54452036 binlog.000018 | mysql -u root -p
# または
mysqlbinlog --no-defaults --start-position=34626719 --stop-position=54452036 binlog.000018 > recover.sql
mysql -u root -p < recover.sql

採点基準

  • 問1: 30%
  • 問2: 70%

問2の採点にはリカバリ後に実行してもらったchecksum table ...の結果を利用するつもりでしたが、問題環境ではバイナリログからの抽出位置を正しく指定しなくてもchecksumが同じ値になってしまうことがありました。
そのため解答中のリカバリ方法の説明で、バックアップ以降、truncate table文まで(図1の②~③部分)の更新分を抽出、適用していることを確認して正解としました。