k8sが便利な理由 #2

koboltlog.hatenablog.com

#1の続きになります。

Kubernetes機能で便利になったサービス安定化

監視システムを例としてKubernetesが便利な理由を説明します。

  1. 開発と監視システムが互いに結ばれる構造 監視やロギングをするには開発パッケージにエージェントを入れるかコードの修正が必要になります。 そのため、監視と開発システムは結ばれる構造となっていて例えば性能テスト時に思っている性能が出ない場合監視エージェントを疑うこともあります。

  2. 開発で一度も使用したことのない(開発システム用の)監視システムを構築する構造 また、監視システムは開発初期から使ってみて不便な個所や監視ポイントを受容しつつ作られます。 →最初から使えない(後から導入されても既存の慣れているやり方でログを確認するようになる)

  3. オープン時に開発プロジェクトとは異なる範囲のアプリケーションを監視することになる構造 初期の要件にプラスされる要件でアプリケーションが追加されたり抜けたりする可能性もあります。 監視システムに自動検知の機能がないことやエージェントを入れ忘れることもあるかもしれません。 監視プロジェクトが先に終わって開発者がいらっしゃらないことも。

こういった問題を標準システムにある監視とロギングツールを使えば解決されます。

  1. 開発と監視システムが互いに結ばれる結ばれない構造
  2. 開発初期から使えるで一度も使用したことのない(開発システム用の)監視システムを構築する構造
  3. オープン時の開発プロジェクトと同じ範囲は異なる範囲のアプリケーションを監視することになる構造

実際そうであるか確かめてみましょう。

監視システムのインストール

[1] GithubでPrometheus(with Grafana)、Loki-Stackをインストール

▶ [k8s-master] Consoleに接続

[root@k8s-master ~]# yum -y install git

# ローカルレポジトリの作成
git init monitoring
git config --global init.defaultBranch main
cd monitoring

# remote 追加
git remote add -f origin https://github.com/kobolt-log/k8s-install.git

# sparse checkout 設定
git config core.sparseCheckout true
echo "ground/k8s-1.27/prometheus-2.44.0" >> .git/info/sparse-checkout
echo "ground/k8s-1.27/loki-stack-2.6.1" >> .git/info/sparse-checkout
 
# ダウンロード
git pull origin main
  • git config --global init.defaultBranch main

    • --globalは、システム上のすべてのプロジェクトに同じ設定を適用します。
    • init.defaultBranch:新しいリポジトリを作成する(git init)際に、デフォルトのブランチ名を指定します。(以前はmaster → 最近はmain)
  • git remote add -f origin https://github.com/kobolt-log/k8s-install.git

    • リモートリポジトリを現在のローカルGitリポジトリに追加します。
    • originはリモートリポジトリの名前を意味します(originという名前でURLをリモートリポジトリとして登録)。
    • -f(fetch):リモートリポジトリのすべてのブランチ情報を即座に取得します(git fetchと同じ)。
    • つまり、リモートリポジトリを追加すると同時に、リモートにあるブランチやコミットの情報をローカルに取得します。
  • git config core.sparseCheckout true

    • 特定のディレクトリやファイルだけを選択的にcheckoutする設定(必要な部分だけを取得する際に便利)
    • sparse checkoutのパターンを定義する必要があります:
      • パターンは.git/info/sparse-checkoutファイルに記述されます。
      • このファイルに記載されたパスに対応するファイルやディレクトリだけがワーキングディレクトリにcheckoutされます。
    • 解除するにはfalseを設定すればOKです。

[2] Prometheus(with Grafana)

# インストール
kubectl apply --server-side -f ground/k8s-1.27/prometheus-2.44.0/manifests/setup
kubectl wait --for condition=Established --all CustomResourceDefinition --namespace=monitoring
kubectl apply -f ground/k8s-1.27/prometheus-2.44.0/manifests

確認

kubectl get pods -n monitoring

  • kubectl apply --server-side -f ground/k8s-1.27/prometheus-2.44.0/manifests/setup

    • --server-sideはサーバー側での適用を意味し、変更内容のマージや管理をKubernetes APIサーバーが処理します
      (リソースの所有権やマージ処理がサーバー側で行われます)。
    • デフォルトではクライアント側適用となり、クライアント(kubectlを実行する側)でマージした結果をサーバーに送信します。
  • kubectl wait --for condition=Established --all CustomResourceDefinition --namespace=monitoring

    • kubectl wait
      • Kubernetesリソースが特定の条件を満たすまで待機します。
    • --for condition=Established
      • リソースの状態条件(condition)がEstablishedになるのを待ちます。
      • EstablishedはCRD(Custom Resource Definition)リソースの特定の状態条件を意味し、CRDが正常に作成され、使用可能な状態になったことを示します。
    • --all
      • 指定されたnamespaceのすべてのリソースを対象に待機します。
      • ここではすべてのCustomResourceDefinitionリソースを対象とします。
    • CustomResourceDefinition
      • Kubernetesでユーザー定義リソースを定義するリソースタイプ。
      • 新しいAPIリソースを作成する際に使用されます。
    • --namespace=monitoring
      • 対象リソースのnamespaceを指定します。
      • 注意:CRDはクラスタースコープ(cluster-scoped)のリソースであるため、namespaceに属しません。
      • そのため、このオプションはこのコマンドでは影響を与えません。
      • CRDから作成されたCustomResourceは、スコープ設定(scope)に応じてnamespace内に配置可能です(scope: Namespaced)
      • scope: Namespacedに設定されたCustomResourceは、metadata.namespaceフィールドでnamespaceを指定する必要があります。 → 上記のnamespace設定はsetupディレクトリ内のnamespace.ymlファイルでmetadata.nameとして設定されています↓
        setup/namespace.yaml
        apiVersion: v1
        kind: Namespace
        metadata:
        name: monitoring

動作プロセス

  1. CRDの作成確認
    1-1. KubernetesはCRDを作成する際、2つの主要な状態条件を管理します:
    1-1-1. NamesAccepted:CRDの名前が有効であり、衝突がないことを示します。
    1-1-2. Established:CRDがAPIサーバーで正常に有効化され、リソースが使用可能な状態になったことを示します。
  2. kubectl waitで状態確認
    2-1. kubectl waitコマンドはCRDの状態条件を定期的に確認し、条件が満たされると待機を終了します。
    2-2. -timeoutオプションがない場合、コマンドは無期限に待機します。
  3. CRDが"Established"状態に遷移するまで待機
    3-1. すべてのCRDがEstablished条件を満たすと、コマンドが終了します。

[3] Loki-Stack

# インストール
kubectl apply -f ground/k8s-1.27/loki-stack-2.6.1

確認

kubectl get pods -n loki-stack

★helmでのprometheus-operator/loki-stackのインストール方法も追記できれば更新★

[4] Grafanaへの接続

▶ 接続URL:http://192.168.56.30:30001 ▶ ログイン:ID:admin、パスワード:admin ▶ 確認結果

[5] GrafanaでのLoki-Stack接続 ▶ データ接続:Home > Connections > Connect data ▶ 検索に[loki]を入力後、項目をクリック > Create a Loki data source

▶ URLに内容を入力:http://loki-stack.loki-stack:3100 ▶ 下のSave & Test

APPをデプロイしLoki-Stackからログを確認

[1] アプリのデプロイdashboardにアクセス > ネームスペース [default] > [+] ボタン > [入力して作成] > YAMLファイルを貼り付け > アップロード またはyamlファイル作成後 kubectl apply -f yamlファイル名

yamlファイル(モニタリング関連設定)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-1-2-2-1
spec:
  selector:
    matchLabels:
      app: '1.2.2.1'
  replicas: 2
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: '1.2.2.1'
    spec:
      containers:
        - name: app-1-2-2-1
          image: kobolt0/app
          imagePullPolicy: Always
          ports:
            - name: http
              containerPort: 8080
          startupProbe:
            httpGet:
              path: "/ready"
              port: http
            failureThreshold: 20
          livenessProbe:
            httpGet:
              path: "/ready"
              port: http
          readinessProbe:
            httpGet:
              path: "/ready"
              port: http
          resources:
            requests:
              memory: "100Mi"
              cpu: "100m"
            limits:
              memory: "200Mi"
              cpu: "200m"
---
apiVersion: v1
kind: Service
metadata:
  name: app-1-2-2-1
spec:
  selector:
    app: '1.2.2.1'
  ports:
    - port: 8080
      targetPort: 8080
      nodePort: 31221
  type: NodePort
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler # HPA
metadata:
  name: app-1-2-2-1
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: app-1-2-2-1 # オートスケーリングを適用する対象
  minReplicas: 2 # 最小Pod数
  maxReplicas: 4 # 最大Pod数
  metrics:
    - type: Resource
      resource:
        name: cpu # CPU使用量を基準に
        target:
          type: Utilization
          averageUtilization: 40 # CPU使用率が40%を超えるとスケールアウト

[2] 確認 kubectl get pod -n default

▶Grafana Dashboardから確認

[3]. APPのログ確認 ▶Home > Explore > 左上の選択から'Loki'指定 > Label browser

▶Select labelsでapp全体やpodを指定 > デプロイした1.2.2.1のアプリを指定 > Show logs

appで出力されているログが表示される

アプリ側の設定は必要なく簡単に監視システムと連携できることが確認できた。

Kubernetes代表機能

Traffic Routing

現在2つのPodが起動中でコントロールプレーンからServiceのnodePortにトラフィックを送るとこのAPPが受け取ります。Serviceの機能でPodが複数あるとそれぞれのPodにトラフィックを分散させます。

Appにトラフィック送信

while true; do curl http://192.168.56.30:31221/hostname; sleep 2; echo ''; done;
トラフィックアルゴリズム関連: 現在iptablesモード(default)で動作しているため、トラフィックがランダム方式でPodに分散されます。 https://kubernetes.io/docs/reference/networking/virtual-ips/#proxy-mode-iptables

動作確認

Podを削除すると新しいPodが起動されます。 →途中片方のPodを削除すると、起動中のもう1つのPodにのみトラフィックが送信されて、削除されたPodが起動を完了するとトラフィックは分散されます。

Self-Healing

Appが何かの理由によってダウンされるとrestartしてくれます。

AppをMemory Leakさせる

curl http://192.168.56.30:31221/memory-leak

Traffic Routingの続きでrestartされるとAPPがトラフィックを受け取れないため起動中のもう一つのPodにのみトラフィックが送信される。 同じくrestartが完了すると分散される。

※restartの理由を確認するには GrafanaでrestartがあったPodを指定し、メモリが高くなった区間があるか確認 その区間の時間帯でログを確認。(Loki-Stack)

AutoScaling

リソースの使用率が高まるとオートスケーリングしてくれます。

Appに負荷をかける

curl http://192.168.56.30:31221/cpu-load
今回はCPU使用率が40%以上になるとPodがスケールアウトされる設定になっています。

kubectl get pod -n default

負荷によってPodが2→4にスケールアウト →Traffic Routingの分散は2→4に

時間が経過し負荷が収まるとPodがTerminatedされます。

RollingUpdate

Podのアップデート中トラフィックは止まりません。

APPイメージアップデート ▶ Namespace: default > デプロイメント > ... > 編集

spec:
  containers:
    - name: app-1-2-2-1
      image: kobolt0/app-update 

または

kubectl set image -n default deployment/app-1-2-2-1 app-1-2-2-1=kobolt0/app-update

yamlファイルを作成している場合、yamlを修正しapplyすることも可

アップデート中はトラフィックが既存のPodへ分散され、完了後には新しいPodにトラフィックが送信され既存のPodは終了されます。

もし、アップデートをしても何かの理由で新しいイメージが正常に起動できない場合はトラフィックが新しいPodには分散されません。(新しいPodがrestartされる)

戻すにはrolloutさせます。

kubectl rollout undo -n default deployment/app-1-2-2-1

これらの機能からサービスを安定化してくれることを確認できました。

インフラ環境管理のコード化

既存のVM環境とKubernetes環境の比較

既存のVM環境において、負荷が大きくなりリソースの増設が必要だと判断された場合、以下のようなシナリオで作業が進行します。

まず、OS担当者がネットワーク、ストレージ、OSなどの作業を行います。次に、webサーバー管理者がIPを設定する必要がありますが、この簡単な作業でさえ既存の稼働中のアプリケーションに影響を与える可能性があるため、すぐには作業できず、夜間に作業を実施します。 その後、モニタリング担当者が新しく追加されたアプリケーションについてもモニタリングができるように設定作業を行います。

一方、Kubernetes環境では、増設が必要な場合、KubernetesエンジニアにPodを増やしてほしいと依頼し、エンジニアはボタンを1回押すだけで、VM環境で手動で行っていた作業を自動的に実行してくれます。 開発期間や性能テストの段階で、このような自動化動作が問題なく機能する設定を検証するため、安定性も確保されています。 また、ポッド内にすべての設定が含まれているため、スケーリングされたポッドの1つだけが設定ミスを起こすような事態も発生しません。

さらに、コードで環境を管理し変更を行うため、開発と同じようにインフラも変更管理の対象となり、インフラの履歴管理が容易になります。KubernetesyamlファイルでPodを管理するため、リソースやスケーリング設定もコードで管理可能です。

また、コードでインフラを管理することで、開発、検証、本番環境を分離し、各環境ごとにインフラ設定を個別のファイルで作成できます。 さらに、インフラの構築や設定、セキュリティ作業において依存関係が大きく発生しないため、事前に環境設定を構成できます。 これらのコードはコピーして各環境に適した命名に変更し、replicasなどの設定だけを調整することで、インフラに関する繰り返し作業を避け、品質向上に集中できます。 また、コードとして残しておけば、新しいインフラ作業にも簡単に適用可能です。