Ingress - NGINX Ingress Controller

| 23 min read
Author: noboru-kudo noboru-kudoの画像

KubernetesのIngressリソースを導入しましょう。

Ingressとは、クラス環境内にデプロイされたアプリ(Pod)に対して、ロードバランシングを行うKubernetesの機能です(こちら参照)。

環境構築編では、動作確認用にServiceリソースをLoadBalancerとして定義することでL4ロードバランサーを作成(実態はELB)しました。
この方法はシンプルですが、ルーティング機能が貧弱(L4)で、様々なアプリケーションがデプロイされると、エンドポイントごとにロードバランサーを配置する必要がある等、柔軟性やコストの観点で劣ります。
Ingressを導入し、Ingressのマニフェストにルーティングのルールを反映すると、1つのロードバランサーで様々なアプリケーションへのエンドポイントを提供することが可能となります。
実際のプロジェクトでも、Ingressを利用して外部にアプリケーションを公開することが多いかと思います。

Ingressリソース自体は、インターフェース(マニフェスト構造)のみを規定し、その実装であるIngress Controller(カスタムコントローラ)が実際のトラフィックルーティングを担います。
Ingress Controllerは様々なものがあり、利用可能なものは以下の公式ドキュメントに記載されています。

今回はKubernetesが公式サポートするNGINX Ingress Controllerを導入してIngress機能を確認します。
NGINX Ingress Controllerは、おそらくIngress Controllerで最もよく知られているもので、その名の通りNginxをロードバランサーとして利用します。

完成形は、以下のようなイメージになります。

事前準備

#

以下のいずれかの方法で事前にEKS環境を作成しておいてください。

また、Ingress Controllerのインストールにk8sパッケージマネージャーのhelmを利用します。
ローカル環境にこちら を参考にv3以降のバージョンをセットアップしてください[1]

Ingress Controllerインストール

#

NGINX Ingress Controllerには様々なインストール方法が用意されています。

今回はHelmチャートとして公開されているNginxのIngress Controllerをインストールします[2]

まずは以下のコマンドでhelmにリポジトリを追加して最新化します。

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

後はインストールコマンドを実行するだけです。
ここでは様々なオプションが指定できます。こちらで利用可能なオプションを確認できます[3]

ほとんどのものはデフォルトで構いませんが、Nginxは2つのレプリカ、AWSリソースとしてNLB(Network LoadBalancer)[4]を使うようにセットアップしましょう。
任意の名前(ここではvalues.yaml)でYAMLファイルを作成し、以下の内容を記述します。

controller:
  # nginxのreplica数
  replicaCount: 2

  # AWS側のLBとしてNLBを指定
  service:
    annotations:
      service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp
      service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: 'true'
      service.beta.kubernetes.io/aws-load-balancer-type: nlb

これでインストールする準備ができました。以下のコマンドを実行しましょう。

helm upgrade ingress-nginx ingress-nginx/ingress-nginx \
  --install --version 4.0.5 \
  --namespace ingress-nginx --create-namespace \
  --values values.yaml \
  --wait

上記コマンドで、helmはHelmチャートのテンプレートとパラメータファイル(--values)からk8sのマニフェストを生成し、クラスタ環境に反映します。
また、作成するNamespaceはingress-nginxで、存在しない場合は作成するように指定しています(--namespace/--create-namespace)。
--versionではNGINX Ingress ControllerのHelmチャートのバージョンを指定しています。Helmチャートは時間とともにバージョンアップされていきますので、予期しないアップデート(デフォルトは最新)を避けるために必ずバージョン[5]を指定しておくようにするのが望ましいでしょう。

NginxのIngress Controllerが起動すると、Helmコマンドで状態を確認できます(内部的には作成したNamespaceのSecretに保存されます)。

helm list -n ingress-nginx
NAME         	NAMESPACE    	REVISION	UPDATED                             	STATUS  	CHART              	APP VERSION
ingress-nginx	ingress-nginx	1       	2021-10-10 13:07:27.980349 +0900 JST	deployed	ingress-nginx-4.0.5	1.0.3      

ingress-nginxがファーストリビジョンとしてデプロイされたことが分かります。

それでは、作成されたKubernetesリソースを確認してみましょう。

Service

#

以下コマンドを実行します。

kubectl get svc -n ingress-nginx 
NAME                                 TYPE           CLUSTER-IP       EXTERNAL-IP                                                                          PORT(S)                      AGE
ingress-nginx-controller             LoadBalancer   10.100.149.143   xxxxxxxxxxxxxx-xxxxxxxxxxxxxxx.elb.ap-northeast-1.amazonaws.com   80:31415/TCP,443:30825/TCP   5m56s
ingress-nginx-controller-admission   ClusterIP      10.100.248.171   <none>                                                                               443/TCP                      5m56s

ingress-nginx-controllerが、外部に公開するIngress Controllerのエンドポイントになります。
ingress-nginx-controller-admissionは、Ingressリソースのバリデーションをする際に使われる内部的なServiceリソースで、ここでは気にする必要はありません[6]

EXTERNAL-IPに、外部公開用のエンドポイント(xxxxxx.elb.ap-northeast-1.amazonaws.com)が設定されていることが分かります。これがIngress Controllerのエンドポイントになります。
実際に運用する際は、これをそのまま使うのではなく、別途ドメインの作成と該当エンドポイントに対して、DNSレコードを追加することが一般的です。

AWS側の状態も確認してみましょう。このLoadBalancerのServiceリソースの生成を検知してNLB(Network LoadBalancer)が生成されているはずです。
マネジメントコンソールからEC2 -> ロードバランサーと選択してみましょう。

上記のようにAWS上にNLBが生成されていることが分かります。

Pod

#

以下コマンドを実行します。

kubectl get pod -n ingress-nginx
NAME                                        READY   STATUS    RESTARTS   AGE
ingress-nginx-controller-646d5d4d67-4rdt2   1/1     Running   0          42m
ingress-nginx-controller-646d5d4d67-wzdmn   1/1     Running   0          47s

レプリカ数として指定した2つのPodが、実行中であることが分かります。
1つのPodのコンテナイメージで、何が使われているのか見てみましょう[7]

kubectl get pod -n ingress-nginx $(kubectl get pod -n ingress-nginx -o jsonpath='{.items[0].metadata.name}') \
  -o jsonpath='{.spec.containers[0].image}'
k8s.gcr.io/ingress-nginx/controller:v1.0.3@sha256:4ade87838eb8256b094fbb5272d7dda9b6c7fa8b759e6af5383c1300996a7452

PodリソースではNGINX Ingress Controllerのコンテナ(k8s.gcr.io/ingress-nginx/controller)が起動していることが分かります。
この中でIngressリソースの変更を監視して、変更時にはNginxの設定に反映するようプログラムが動作しています。

実際のNGINX Ingress Controllerのログも見てみましょう。

kubectl logs -n ingress-nginx $(kubectl get pod -n ingress-nginx -o jsonpath='{.items[0].metadata.name}')
-------------------------------------------------------------------------------
NGINX Ingress controller
  Release:       v1.0.3
  Build:         6e125826ad3968709392f2056023d4d7474ed4f5
  Repository:    https://github.com/kubernetes/ingress-nginx
  nginx version: nginx/1.19.9

-------------------------------------------------------------------------------

W1010 04:07:31.336417       6 client_config.go:615] Neither --kubeconfig nor --master was specified.  Using the inClusterConfig.  This might not work.
I1010 04:07:31.336936       6 main.go:221] "Creating API client" host="https://10.100.0.1:443"
I1010 04:07:31.347958       6 main.go:265] "Running in Kubernetes cluster" major="1" minor="20+" git="v1.20.7-eks-d88609" state="clean" commit="d886092805d5cc3a47ed5cf0c43de38ce442dfcb" platform="linux/amd64"
I1010 04:07:31.495290       6 main.go:104] "SSL fake certificate created" file="/etc/ingress-controller/ssl/default-fake-certificate.pem"
I1010 04:07:31.522215       6 ssl.go:531] "loading tls certificate" path="/usr/local/certificates/cert" key="/usr/local/certificates/key"
I1010 04:07:31.549442       6 nginx.go:253] "Starting NGINX Ingress controller"
I1010 04:07:31.558138       6 event.go:282] Event(v1.ObjectReference{Kind:"ConfigMap", Namespace:"ingress-nginx", Name:"ingress-nginx-controller", UID:"2b8518a5-0afd-44a9-9c5a-56e4d4022cfc", APIVersion:"v1", ResourceVersion:"24050", FieldPath:""}): type: 'Normal' reason: 'CREATE' ConfigMap ingress-nginx/ingress-nginx-controller
I1010 04:07:32.750207       6 nginx.go:295] "Starting NGINX process"
I1010 04:07:32.750205       6 leaderelection.go:243] attempting to acquire leader lease ingress-nginx/ingress-controller-leader...
I1010 04:07:32.750961       6 nginx.go:315] "Starting validation webhook" address=":8443" certPath="/usr/local/certificates/cert" keyPath="/usr/local/certificates/key"
I1010 04:07:32.751306       6 controller.go:152] "Configuration changes detected, backend reload required"
I1010 04:07:32.766735       6 leaderelection.go:253] successfully acquired lease ingress-nginx/ingress-controller-leader
I1010 04:07:32.766898       6 status.go:84] "New leader elected" identity="ingress-nginx-controller-646d5d4d67-4rdt2"
I1010 04:07:32.808211       6 controller.go:169] "Backend successfully reloaded"
I1010 04:07:32.808361       6 controller.go:180] "Initial sync, sleeping for 1 second"

NGINX Ingress Controllerがクラスタ構成で起動している様子が分かります。これで準備は完了です。

サンプルアプリのデプロイ

#

それでは、作成したIngress経由でアプリケーションに対して、リクエストがルーティングできることを試してみましょう。
まずは、アプリケーションを用意する必要があります。
本来はソースコード記述、イメージビルド、プッシュ等の手順を踏む必要がありますが、今回の本題ではありませんので簡略化します。ConfigMap上にNode.jsのスクリプトを保管し、それをNode.jsの公式コンテナで直接実行するようにします。
また、ルーティングの確認をするために、2種類のアプリケーション(ソースコードは同じ)を準備します。
任意の名前(ここではapp.yamlとしました)でYAMLファイルを作成し、以下の内容を記述しましょう。

# サンプルアプリスクリプト
apiVersion: v1
kind: ConfigMap
metadata:
  name: server
data:
  index.js: |
    const http = require('http');

    const server = http.createServer((req, res) => {
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/plain');
      res.end(`${process.env.POD_NAME}: hello sample app!\n`);
    });

    const hostname = '0.0.0.0';
    const port = 8080;
    server.listen(port, hostname, () => {
      console.log(`Server running at http://${hostname}:${port}/`);
    });
---
# 1つ目のアプリ
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: app1
  template:
    metadata:
      labels:
        app: app1
    spec:
      containers:
        - name: app1
          image: node:16
          ports:
            - name: http
              containerPort: 8080
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
          command: [sh, -c, "node /opt/server/index.js"]
          volumeMounts:
            - mountPath: /opt/server
              name: server
      volumes:
        - name: server
          configMap:
            name: server
---
# 2つ目のアプリ
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app2
spec:
  replicas: 2
  selector:
    matchLabels:
      app: app2
  template:
    metadata:
      labels:
        app: app2
    spec:
      containers:
        - name: app2
          image: node:16
          ports:
            - name: http
              containerPort: 8080
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
          command: [sh, -c, "node /opt/server/index.js"]
          volumeMounts:
            - mountPath: /opt/server
              name: server
      volumes:
        - name: server
          configMap:
            name: server
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: app1
  name: app1
spec:
  selector:
    app: app1
  ports:
    - targetPort: http
      port: 80
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: app2
  name: app2
spec:
  selector:
    app: app2
  ports:
    - targetPort: http
      port: 80

少し長いですが内容は非常に単純です。前述の通りここの内容については重要ではありませんのでコピペで構いません。

ConfigMap(server)

#

Node.jsのスクリプト本体です。今回はここにソースコードを記述しています。
Node.jsのサーバーを起動し、リクエストが来た際には環境変数に保存されたPod名と固定文字列を返します。

Deployment(app1/app2)

#

2種類のアプリケーションそれぞれを、Deploymentリソースとして作成します。ポイントは以下のとおりです。

  • レプリカ数2の冗長構成(replicas)
  • ConfigMapをマウント(volueme/volumeMounts)
  • Pod起動時にNode.jsのnodeコマンド(Deploymentリソースのcommandフィールド参照)でスクリプト実行
  • 環境変数としてPod名を設定(envフィールド)

Service(app1/app2)

#

最後に上記をServiceリソースとして公開しています。前回のクラスタ環境構築と違う点としてtypeを指定していません。
これにより、そのServiceはクラスタ内でのみアクセス可能なエンドポイントを提供するClusterIPとして作成されます。
この場合は、もちろんEXTERNAL-IPは設定されませんので、type=LoadBalancerとしたときのようにELBが生成されることはありません。

これらのリソースをデプロイしましょう。

kubectl apply -f app.yaml

デプロイしたアプリの状態を確認しましょう。

kubectl get cm,deployment,pod,svc
# 関連部分のみ抜粋
NAME                                  DATA   AGE
configmap/server                      1      8m31s

NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/app1   2/2     2            2           8m31s
deployment.apps/app2   2/2     2            2           8m31s

NAME                        READY   STATUS    RESTARTS   AGE
pod/app1-7ff67dc549-dttr8   1/1     Running   0          8m31s
pod/app1-7ff67dc549-rlw7x   1/1     Running   0          8m31s
pod/app2-b6dc558b5-8q6rw    1/1     Running   0          8m31s
pod/app2-b6dc558b5-q6cq2    1/1     Running   0          8m31s

NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/app1         ClusterIP   10.100.249.6    <none>        80/TCP    8m31s
service/app2         ClusterIP   10.100.192.77   <none>        80/TCP    8m31s

app1/app2でそれぞれPodが2つずつ起動して実行中であることが分かります。ServiceリソースについてもTYPEがClusterIPとして作成されていることが分かります(EXTERNAL-IPの割当なし)。

Ingressリソース作成

#

では、作成した2つのアプリを外部に公開するためのIngressリソースを作成しましょう。
今回はドメインやDNSは作成せずに、任意のドメインに対して動作シミュレーションするようにします。

任意のYAMLファイル(ここではingress.yamlとしました)を作成し、以下の内容を記述します。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
spec:
  ingressClassName: nginx # k8s 1.19以降で必要
  rules:
    # app1へのルーティングルール
    - host: sample-app1.mamezou-tech.com
      http:
        paths:
          - backend:
              service:
                name: app1
                port:
                  number: 80
            path: /
            pathType: Prefix
    # app2へのルーティングルール
    - host: sample-app2.mamezou-tech.com
      http:
        paths:
          - backend:
              service:
                name: app2
                port:
                  number: 80
            path: /
            pathType: Prefix

apiVersion: networking.k8s.io/v1/kind: Ingressと記述することでIngressリソースのマニフェストになります。
注意点としてingressClassNameフィールドにnginxと記述する必要があります。対応するIngressClassリソースはNGINX Ingress Controllerのインストール時に既に作成されています[8][9]
最も重要な部分はその下のrulesフィールド配下です。ここにホスト名、パスに対してルーティングするServiceの名前を記述します。
これにもとづいて、Ingress ControllerはNginxの設定ファイル(nginx.conf)を更新します。
上記では、2つのホストに対して、それぞれapp1/app2へとリクエストを流すように指定しています。

もちろん、Nginxはこれ以外にもリバースプロキシーとして数多くの機能を持っています。別途annotationsフィールド[10]やConfigMap[11]を更新することで様々なカスタマイズを行うことができます。

では、Ingressリソースについてkubectlコマンドで反映しましょう。

kubectl apply -f ingress.yaml

反映したらIngressリソースの内容を確認しましょう。Ingress ControllerがIngressリソースの作成を検知するまで少し時間がかかる場合もあります。

kubectl describe ingress app-ingress
Name:             app-ingress
Namespace:        default
Address:          xxxxxxxxxxxxxxxx-xxxxxxxxxxxxx.elb.ap-northeast-1.amazonaws.com
Default backend:  default-http-backend:80 (<error: endpoints "default-http-backend" not found>)
Rules:
  Host                          Path  Backends
  ----                          ----  --------
  sample-app1.mamezou-tech.com  
                                /   app1:80 (192.168.29.81:8080,192.168.52.46:8080)
  sample-app2.mamezou-tech.com  
                                /   app2:80 (192.168.15.185:8080,192.168.45.143:8080)
Annotations:                    <none>
Events:
  Type    Reason  Age                From                      Message
  ----    ------  ----               ----                      -------
  Normal  Sync    53m (x2 over 53m)  nginx-ingress-controller  Scheduled for sync
  Normal  Sync    53m (x2 over 53m)  nginx-ingress-controller  Scheduled for sync

Rulesを確認すると、以下のようにルーティング設定されていることが分かります。

ホスト パス Service Pod(IP:Port)
sample-app1.mamezou-tech.com / app1:80 192.168.29.81:8080
192.168.52.46:8080
sample-app2.mamezou-tech.com / app2:80 192.168.29.81:8080
192.168.52.46:8080

Nginxに詳しい方は、実際のnginx.confの中身が気になるところでしょう。
以下のように、Ingress ControllerのPod内の/etc/nginx/nginx.confを見ると、実際のNginxの設定ファイルを参照できます。

kubectl exec -n ingress-nginx \
  $(kubectl get pod -n ingress-nginx -o jsonpath='{.items[0].metadata.name}') \
  -- cat /etc/nginx/nginx.conf

非常に長いので内容は省略しますが、ルーティングルールがnginx.confに反映されている様子が分かります。
このファイルは、Ingress ControllerがIngressリソースの変更を検知して更新していくものですので、直接変更しないでください。

動作確認

#

それでは実際にIngress経由でアプリにアクセスしてみましょう。
Ingressはホスト名(Hostヘッダ)に基づいてルーティングを行います。このため通常は、Ingressリソースのルールに指定したホスト名とエンドポイント(NLB/Ingress ControllerのURL)をDNSに登録する必要があります。

こちらは今回の本題ではありませんので、curlでHostヘッダを指定してシミュレーションしましょう。
実際のアクセス先については、kubectl get svc ingress-nginx-controller -n ingress-nginxコマンドのEXTERNAL-IPを確認してください。

今回はIngressのエンドポイントを事前に設定しておきます。

INGRESS_URL=$(kubectl get svc ingress-nginx-controller -n ingress-nginx -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
echo $INGRESS_URL

xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxx.elb.ap-northeast-1.amazonaws.comといった形のURLが出力されればOKです。
それではapp1/app2それぞれに対してリクエストを送ってみましょう。

# app1
curl -H 'Host:sample-app1.mamezou-tech.com' $INGRESS_URL
# app2
curl -H 'Host:sample-app2.mamezou-tech.com' $INGRESS_URL
app1-7ff67dc549-rlw7x: hello sample app!
app2-b6dc558b5-q6cq2: hello sample app!

出力内容から、app1/app2それぞれのPodに対して、ルーティングされていることが分かります。

最後に、冗長構成のPodに対して、負荷分散が正しく行われているかを確認してみましょう。
app1に対して10回リクエストを連続で送信してみます。

for i in {1..10}; do curl -H 'Host:sample-app1.mamezou-tech.com' $INGRESS_URL; done
app1-7ff67dc549-dttr8: hello sample app!
app1-7ff67dc549-rlw7x: hello sample app!
app1-7ff67dc549-dttr8: hello sample app!
app1-7ff67dc549-dttr8: hello sample app!
app1-7ff67dc549-dttr8: hello sample app!
app1-7ff67dc549-rlw7x: hello sample app!
app1-7ff67dc549-dttr8: hello sample app!
app1-7ff67dc549-rlw7x: hello sample app!
app1-7ff67dc549-rlw7x: hello sample app!
app1-7ff67dc549-dttr8: hello sample app!

2つのPodそれぞれにリクエストが負荷分散されている様子が確認できます。

セッション維持(Session Affinity)

#

一般的にコンテナやサーバーレスアーキテクチャで実行するアプリは、ステートレスが原則です。
とは言っても、インメモリのHttpSessionを使うようなレガシーアプリをリフト&シフトで移行する場合に、同一クライアントからのリクエストは全て同じPodに振り向ける必要があることも多いでしょう。
今回は、この要件に対応できるようNginxのセッション維持機能を導入してみましょう[12]

Nginxでは、Cookieによるセッション維持をサポートしていますのでこれを利用します。
先程のファイルに以下を追記します。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  # ここから
  annotations:
    nginx.ingress.kubernetes.io/affinity: "cookie"
  # ここまで
spec:
# 以下は変更なし

変更点はannotationsに定義(nginx.ingress.kubernetes.io/affinity: "cookie")を追加しただけです。

他にもCookieに関して指定できるオプションがありますので、詳細はこちらを参照してください。

こちらで先程のIngressを更新しましょう。

kubectl apply -f ingress.yaml

再度リクエストを送って、レスポンスヘッダを確認してみましょう。

curl -i -H 'Host:sample-app1.mamezou-tech.com' $INGRESS_URL
HTTP/1.1 200 OK
Date: Sun, 10 Oct 2021 09:03:31 GMT
Content-Type: text/plain
Content-Length: 41
Connection: keep-alive
Set-Cookie: INGRESSCOOKIE=1633856612.574.32.257509|84b1c2de93d4453da32050254a7bce65; Path=/; HttpOnly

app1-7ff67dc549-rlw7x: hello sample app!

Set-Cookieヘッダに、Ingressリソースで指定したINGRESSCOOKIEが設定されていることが分かります。

ブラウザの動作をシミュレートして、先程のCookieの値をリクエストに指定して10回連続で送信してみましょう。

for i in {1..10}; do curl -b 'INGRESSCOOKIE=1633856612.574.32.257509|84b1c2de93d4453da32050254a7bce65' \
  -H 'Host:sample-app1.mamezou-tech.com' $INGRESS_URL; done
app1-7ff67dc549-rlw7x: hello sample app!
app1-7ff67dc549-rlw7x: hello sample app!
app1-7ff67dc549-rlw7x: hello sample app!
app1-7ff67dc549-rlw7x: hello sample app!
app1-7ff67dc549-rlw7x: hello sample app!
app1-7ff67dc549-rlw7x: hello sample app!
app1-7ff67dc549-rlw7x: hello sample app!
app1-7ff67dc549-rlw7x: hello sample app!
app1-7ff67dc549-rlw7x: hello sample app!
app1-7ff67dc549-rlw7x: hello sample app!

全て最初のリクエストと同じPod(app1-xxx)に、ルーティングされていることが確認できます。

流量制御(Rate Limiting)

#

最後に、DDoS攻撃対策としてNginxのRate Limitingによる流量制御をかけてみましょう。
同一クライアントに対して1秒あたりのリクエスト数(RPS: Request Per Seconds)を10に制限してみましょう。

変更点は以下のとおりです。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    # ここから
    nginx.ingress.kubernetes.io/limit-rps: "5"
    # ここまで
spec:
# 以下は変更なし

先程のSession Affinityと同様に、annotationsnginx.ingress.kubernetes.io/limit-rps: "5"を指定しました。

注意点として、この設定はNginx Ingress ControllerのPodごとに反映されるため、これにレプリカ数を乗じてRPSを計算する必要があります。今回はレプリカ数2のため5x2=10RPSでPodにルーティングされます。
また、NginxはLeaky bucketアルゴリズムで、バーストトラフィックに備えてキューイングする点(デフォルトではlimit-rpsの5倍)も考慮する必要があります。今回のサンプルだと5x5x2=50のキューがあります。

こちらも反映させましょう。

kubectl apply -f ingress.yaml

今回は、負荷テストツールとしてNode.jsのloadtestを利用します。
事前にnpm install -g loadtestでインストールしておいてください。
ここでは、1000リクエストをRPS50で投げてみました。

loadtest -n 1000 --rps 50 -H 'Host:sample-app1.mamezou-tech.com' http://$INGRESS_URL

以下のような結果となりました。

Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
Requests: 205 (21%), requests per second: 41, mean latency: 22.4 ms
Errors: 25, accumulated errors: 25, 12.2% of total requests
Requests: 455 (46%), requests per second: 50, mean latency: 20.7 ms
Errors: 148, accumulated errors: 173, 38% of total requests
Requests: 706 (71%), requests per second: 50, mean latency: 20.1 ms
Errors: 151, accumulated errors: 324, 45.9% of total requests
Requests: 956 (96%), requests per second: 50, mean latency: 20.8 ms
Errors: 150, accumulated errors: 474, 49.6% of total requests

Target URL:          http://xxxxxxxxxx-xxxxxxxx.elb.ap-northeast-1.amazonaws.com
Max requests:        1000
Concurrency level:   1
Agent:               none
Requests per second: 50

Completed requests:  1000
Total errors:        501
Total time:          20.881590101999997 s
Requests per second: 48
Mean latency:        20.9 ms

Percentage of the requests served within a certain time
  50%      20 ms
  90%      25 ms
  95%      28 ms
  99%      36 ms
 100%      46 ms (longest request)

 100%      46 ms (longest request)

  503:   501 errors

時間経過と共にキュー(Bucket)が溢れて、リクエストがエラーになっていく様子が分かります。
最終的には、501/1000と約50%のリクエストが、Podにルーティングされることなく503(ServiceUnavailable)エラー[13]を返しました。

クリーンアップ

#

今回のリソース削除する際にはまず作成したk8sリソースを削除してから、クラスタ環境を削除するようにしましょう。
以下の手順でk8sのリソースを削除できます。

# app1/app2
kubectl delete -f app.yaml
# Ingress
kubectl delete -f ingress.yaml
# Ingress Controller
helm uninstall -n ingress-nginx ingress-nginx

クラスタ環境については環境構築編のクリーンアップ手順を参照してください。


参照資料


  1. Helmのv3以降ではサーバー環境へのインストール(tiller)が不要となり、セキュリティ面でも安心して使えるようになりました。 ↩︎

  2. Helmチャートの中身に興味がある方はこちらを見てみるとよいでしょう。 ↩︎

  3. helm show values ingress-nginx/ingress-nginxコマンドでも参照できます。 ↩︎

  4. 何も指定しない場合は、前世代のELBであるCLB(Classic LoadBalancer)として作成されます。L7レイヤの機能はNginxが担うため、AWSのELBとしてはNLBを使うのが望ましいでしょう。 ↩︎

  5. バージョン情報はIngressControllerのGithubやArtifactHubでも確認できます。 ↩︎

  6. https://kubernetes.github.io/ingress-nginx/how-it-works/#avoiding-outage-from-wrong-configuration ↩︎

  7. コマンドの-o jsonpath='{...}'JSONPathの記法を使用してPodのマニフェストから特定のフィールドのみを出力するように整形しています。これを使いこなすとkubectlの操作が格段に楽になりますので是非使いこなせるようになりましょう。 ↩︎

  8. Ingress Controllerは複数配置することもあります。以前はannotationsでどのIngress Controllerを使用するかを指定していましたが、k8s 1.19からは新たにIngressClassというリソースが追加されています。 ↩︎

  9. 対応するIngressClassが存在しない場合(kubectl get ingressClass -n ingress-nginx)は、こちらを参考に別途作成してください。 ↩︎

  10. https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/ ↩︎

  11. https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/ ↩︎

  12. と言ってもオンプレと違い、コンテナ環境ではPodだけでなくNodeについても頻繁に増減させるため、これは完全な代替とはなりません。アプリをステートレスにできるのであればそうすべきです。 ↩︎

  13. 429(Too Many Requests)にしたいところですね。こちらはConfigMapの方で対応可能です。こちらを参照してください。 ↩︎

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。