この連載が書籍になりました!『Kubernetes完全ガイド

KubernetesのDiscovery&LBリソース(その2)

2018年5月16日(水)
青山 真也
前回に引き続き、Discovery&LBリソースのServiceとIngressを紹介する。

Discovery&LBリソース(その2)

前回に続いて、KubernetesのDiscovery & LBリソースに含まれるリソースを紹介していきます。

5種類に大別できるKubernetesのリソース

リソースの分類内容
Workloadsリソースコンテナの実行に関するリソース
Discovery&LBリソースコンテナを外部公開するようなエンドポイントを提供するリソース
Config&Storageリソース設定・機密情報・永続化ボリュームなどに関するリソース
Clusterリソースセキュリティやクォータなどに関するリソース
Metadataリソースリソースを操作する系統のリソース

Discovery&LBリソース

  • Service
    • ClusterIP
    • NodePort
    • LoadBalancer
    • ExternalIP
    • ExternalName
    • Headless(None)
  • Ingress

Headless Service

KubernetesのHeadless Serviceとは、PodのIP Addressが返ってくるServiceです。通常はPodのIP Addressはすぐ変動することが多いため、永続性の高いStatefulSetのみで利用可能となっています。前回の記事で紹介した他のServiceで負荷分散のために提供されるエンドポイントは、VIP的な動作をするような複数Pod宛のIPエンドポイントでした。

前回紹介したServiceと提供されるエンドポイント

Serviceエンドポイント
ClusterIPKubernetes Cluster内でのみ疎通可能なVIP
ExternalIP特定のKubernetes NodeのIP
NodePort全Kubernetes Nodeの全IP(0.0.0.0)
LoadBalancerクラスタ外で提供されているLoadBalancerのVIP

一方でHeadless ServiceはIPエンドポイントを提供せず、DNS Round Robin(DNS RR)を使ったエンドポイントを提供します。Headless ServiceのDNS RRでは、転送先のPodのIPアドレスがクラスタ内DNSから返ってくる形で負荷分散が行われるため、クライアント側でのキャッシュなどに注意する必要があります。

基本的にKubernetesは、PodのIP Addressを意識する必要はないように作られているため、PodのIP AddressをディスカバリするにはAPIを叩くしかありません。しかし、Headless Serviceを利用することで、StatefulSetに限りService経由でIP Addressをディスカバリすることが可能です。

LoadBalancer Serviceの作成

Headless Serviceを作成するには、下記の3つが条件となります。

  • Serviceのspec.typeClusterIPであること
  • Serviceのmetadata.nameがStatefulSetのspec.serviceNameと同じであること
  • Serviceのspec.clusterIPNoneであること

いずれかの条件が満たされていない場合は、ただのServiceとしてしか動作せず、Pod名を引くことができません。具体的には下記のような定義ファイルを作成する必要があります。

リスト1:Headless Serviceを作成するheadless_sample.yml

apiVersion: v1
kind: Service
metadata:
  name: sample-svc
spec:
  type: ClusterIP
  clusterIP: None
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 80
      targetPort: 80
  selector:
    app: sample-app

# Headless Serviceを作成
kubectl apply -f ./headless_sample.yml

Headless Serviceによるpod名の名前解決

通常はクラスタ内DNSでpodの名前解決をすることは出来ない仕様となっています。Serviceを作成した際には複数Podに対するエンドポイントが払い出され、そのエンドポイントに対しての名前解決は提供されていますが、個々のPodの名前解決は出来ません。

通常Serviceの名前解決は[Service名].[Namespace名].svc.[domain名]で引けるようになっていますが、Headless Serviceで正引きを行うとDNS Round RobinでPodのどれかのIPが返ってくるため、負荷分散には向かなくなります。

リスト2:Headless Serviceで正引きを行った結果

# dig sample-svc.default.svc.cluster.local
……
;; QUESTION SECTION:
;sample-svc.default.svc.cluster.local. IN   A

;; ANSWER SECTION:
sample-svc.default.svc.cluster.local. 30 IN A       10.8.0.30
sample-svc.default.svc.cluster.local. 30 IN A       10.8.1.34
sample-svc.default.svc.cluster.local. 30 IN A       10.8.2.55
……

ReplicaSetなどのリソースでも、上記のようにDNS Round RobinでIP Addressが返ってくるようにすることは可能です。しかし、StatefulSetの場合のみ、[Pod名].[Service名].[Namespace名].svc.[domain名]という形式で、下記のようにPod単位での名前解決を行えるようになっています。

リスト3:StatefulSetではPod単位の名前解決も可能

# dig sample-statefulset-0.sample-svc.default.svc.cluster.local
……
;; QUESTION SECTION:
;sample-statefulset-0.sample-svc.default.svc.cluster.local. IN A

;; ANSWER SECTION:
sample-statefulset-0.sample-svc.default.svc.cluster.local. 30 IN A 10.8.0.30
……

なお、コンテナのresolv.confなどにはsearchとして下記のようなエントリが入っていることが多いため、[Pod名].[Service名]や[Pod名].[Service名].[Namespace名]などでも引くことが可能となっています。

リスト4:コンテナ内のresolv.confの例

nameserver 10.11.240.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

ExternalName

ExternalNameは通常のService系リソースとは異なり、Service名の名前解決に対してCNAMEを返すリソースになります。使用する場面としては、別の名前を設定したい場合や、クラスタ内からのエンドポイントを切り替えやすくしたい場合などがあります。

ExternalName Serviceの作成

ExternalName Serviceの作成は、下記のようなYAMLファイルから作成します。

リスト5:ExternalName Serviceを作成するexternalname_sample.yml

kind: Service
apiVersion: v1
metadata:
  name: sample-externalname
  namespace: default
spec:
  type: ExternalName
  externalName: external.example.com

# ExternalName Serviceを作成
kubectl apply -f externalname_sample.yml

サービスを確認すると、External-IP部分にCNAME用のDNSが表示されています。

リスト6:ExternalName Serviceの確認

$ kubectl get svc
NAME                  TYPE           CLUSTER-IP    EXTERNAL-IP            PORT(S)          AGE
sample-externalname   ExternalName   <none>        external.example.com   <none>           18m

そのためコンテナ内からは、[Service名]や[Service名].[Namespace名].svc.[domain名]で正引きを行うとCNAMEが返ってくることが確認できます。

リスト7:CNAMEが返ってくることを確認

# dig sample-externalname.default.svc.cluster.local CNAME
……
;; ANSWER SECTION:
sample-externalname.default.svc.cluster.local. 30 IN CNAME external.example.com.
……

外部サービスとの疎結合性の確保

クラスタ内では、Pod宛の通信にServiceの名前解決を使うことでサービス間の疎結合性を保っていました。一方で、SaaSやIaaSなどの外部にあるサービスを利用する際にも、可能な限り疎結合にする必要があります。

アプリケーションなどに外部のエンドポイントを書き込んでしまうと、切り替えを実施する際にアプリケーション側の設定変更が必要になってしまいます。一方でExternalNameを利用すると、DNSの切り替えはExternalName Serviceの変更を行うだけで可能なため、Kubernetes上のオペレーションで完結することができ、外部とKubernetesクラスタを疎結合性を保てます。

ExternalNameを使った外部サービスとの疎結合性の確保

ExternalNameを使った外部サービスとの疎結合性の確保

外部サービスと内部サービス間の切り替え

またExternalNameの利用により、外部サービスとの疎結合性の確保に加え、外部サービスとKubernetes上で展開されたクラスタ内サービスの切り替えも柔軟に行えるようになります。

ExternalName を使ったクラスタ内サービスから外部サービスへの柔軟な切り替え

ExternalName を使ったクラスタ内サービスから外部サービスへの柔軟な切り替え

Ingress

IngressはL7 LoadBalancerを提供するリソースですが、厳密には「Kind: Service」タイプのリソースではなく、「Kind: Ingress」タイプのリソースです。また、KubernetesのNetwork PolicyリソースにIngress/Egressという設定項目がありますが、このIngressとは関係ありません。

Ingressの種類

Ingressは現行のKubernetes v1.10でもまだBetaリリースとなっており、仕様などが確定されていません。Ingressの実装は複数あり、その使い勝手も大きく異なるものもありますが、今回は実際によく使われているGKE Ingress Controller、Nginx Ingress Controllerの2つを紹介します。

なお、Ingressは下記の2種類に大別できます。

  • クラスタ外のロードバランサを利用したIngress
    • GKE
  • クラスタ内にIngress用のPodを展開するIngress
    • Nginx Ingress
    • Nghttpx Ingress

クラスタ外のロードバランサを利用したIngress

GKEのようにクラスタ外のロードバランサを利用したIngressの場合、Ingressリソースを作成するだけでLoadBalancerのVIPが払い出されて利用することが可能です。

そのため、Ingressのトラフィック的にはGCPのGCLB(Google Cloud Load Balancer)がトラフィックを受けた後、GCLBでHTTPS終端やパスベースルーティングなどを行い、NodePortへトラフィックを転送することで対象のPodまで到達します。

  1. [L7 LoadBalancer(NodePort経由)]
  2. [転送先の Pod]
クラスタ外のロードバランサを利用したIngress

クラスタ外のロードバランサを利用したIngress

クラスタ内に展開するIngress

クラスタ内に展開する場合は、L7 相当の処理を行うPodをクラスタ内で立てる形で実現しています。そのため、クラスタ外からアクセスできるように別途Ingress用のPod宛にLoadBalancer Serviceを作成するなどの準備が必要になります。またIngress用のPodがHTTPSの終端やパスベースのルーティングなどL7相当の処理を行うため、Podのレプリカ数のオートスケールも考慮する必要があります。

そのため、Ingressのトラフィック的には、LBは一旦Nginx Podまで転送し、NginxがL7相当の処理を行い対象のPodへ転送します。このとき、Nginx Podから対象のPodまではNodePortは通らず、直接PodのIP宛に送られます。

  1. [L4 LoadBalancer(type: LoadBalancer)]
  2. [Nginx Pod(Nginx Ingress Controller)]
  3. [転送先のPod]
クラスタ内に展開するIngress

クラスタ内に展開するIngress

Ingressリソースの作成

Ingressリソースの作成には、事前準備が必要です。Ingressは事前に作成されたServiceをバックエンドとして転送を行う仕組みになっています。バックエンドで利用するServiceはtype:NodePortを指定して下さい

リスト8:Ingressリソースの作成

# cat << _EOF_ | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-ingress-apps
spec:
  replicas: 1
  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
---
apiVersion: v1
kind: Service
metadata:
  name: svc1
spec:
  type: NodePort
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8888
      targetPort: 80
  selector:
    ingress-app: sample
_EOF_

また、IngressでHTTPSを利用する場合には、証明書は事前にSecretとして登録しておく必要があります。Secretは、証明書の情報を元にYAMLファイルを自分で作成するか、証明書ファイルを指定して作成します。

リスト9:Secretの作成

# 証明書の作成
$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /tmp/tls.key -out /tmp/tls.crt -subj "/CN=sample.example.com"

# Secret の作成 (証明書ファイルを指定した場合)
$ kubectl create secret tls tls-sample --key /tmp/tls.key --cert /tmp/tls.crt

IngressリソースはL7 LoadBalancerのため、特定のホスト名に対して、「リクエストパス > Serviceバックエンド」のペアで転送ルールを設定します。また、1つのIP Addressで複数のホスト名を扱うことも可能です。spec.rules[].http.paths[].backend.servicePortに設定するPort番号は、Serviceのspec.ports[].portを指定して下さい。

リスト10:Ingressリソースを作成するingress_sample.yml

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

# Ingressリソースを作成
$ kubectl apply -f ingress_sample.yml

IngressリソースとIngress Controller

Ingressと一言で言っても、その指し示していることは、様々です。例を挙げると「Ingressリソース」とは、YAMLファイルで登録されるAPIリソースのことを意味しますし、「Ingress Controller」はIngressリソースがKubernetesに登録された際に、何らかの処理を行うものになります。処理の例としては、GCPのGCLBを操作することによるL7 LoadBalancerの設定や、NginxのConfigを変更してリロードを実施するなどが挙げられます。

GKEの場合

GKEの場合には、デフォルトでGKE用のIngress Controllerがデプロイされており、特に意識することなくIngressリソースごとに自動でIP エンドポイントが払い出されます。

リスト11:GKEでの例

$ kubectl get ingress
NAME             HOSTS                ADDRESS           PORTS    AGE
sample-ingress   sample.example.com   xxx.xxx.xxx.xxx   80       5m

実際にリクエストを送ってみると、以下のように正しく返ってくることが確認できます。

リスト12:正しく動作していることを確認

# curl http://xxx.xxx.xxx.xxx/path1 -H "Host: sample.example.com" -LI -o /dev/null -w '%{http_code}\n' -s
200
# curl https://xxx.xxx.xxx.xxx/path1 -H "Host: sample.example.com" -LI -o /dev/null -w '%{http_code}\n' -s --insecure
200

Nginx Ingressの場合

Nginx Ingressを利用する場合には、Nginx Ingress Controllerを作成する必要があります。Nginx Ingressでは、Ingress Controller自体が「L7 相当の処理を行うPod」にもなっており、Controllerという名前ですが、実際の処理も行います。

クラスタ内に展開するIngress

クラスタ内に展開するIngress

GKEのようにクラスタ外からの疎通性を確保するには、Nginx Ingress Controller宛のLoadBalancer Service(NodePortなどでも可)を作成する必要があります。個別でServiceを作成する形になるため、kubectl get ingressでエンドポイントのIP Addressは確認できないので注意して下さい。

リスト13:Nginx Ingressでの例。IP Addressは確認できない

$ kubectl get ingress
NAME             HOSTS                ADDRESS    PORTS    AGE
sample-ingress   sample.example.com              80       5m

Nginx Ingressを利用する場合のYAMLは、下記のような形になります。ルールにマッチングしない場合のデフォルトの転送先を作成して置く必要がある点に注意して下さい。また、今回は説明のため簡略化したものを書いていますが、実際はRBAC・リソース制限・ヘルスチェック間隔など細かく設定しておく方が望ましいです。推奨設定はここを確認して下さい。

リスト14:Nginx Ingressを利用するYAMLのサンプル

apiVersion: apps/v1
kind: Deployment
metadata:
  name: default-http-backend
  labels:
    app: default-http-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: default-http-backend
  template:
    metadata:
      labels:
        app: default-http-backend
    spec:
      containers:
      - name: default-http-backend
        image: gcr.io/google_containers/defaultbackend:1.4
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
            scheme: HTTP
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: default-http-backend
  labels:
    app: default-http-backend
spec:
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: default-http-backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-ingress-controller
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ingress-nginx
  template:
    metadata:
      labels:
        app: ingress-nginx
    spec:
      containers:
        - name: nginx-ingress-controller
          image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.14.0
          args:
            - /nginx-ingress-controller
            - --default-backend-service=$(POD_NAMESPACE)/default-http-backend
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          ports:
          - name: http
            containerPort: 80
          - name: https
            containerPort: 443
          livenessProbe:
            httpGet:
              path: /healthz
              port: 10254
              scheme: HTTP
          readinessProbe:
            httpGet:
              path: /healthz
              port: 10254
              scheme: HTTP
---
apiVersion: v1
kind: Service
metadata:
  name: ingress-endpoint
  labels:
    app: ingress-nginx
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 8080
  selector:
    app: ingress-nginx

「デフォルトバックエンド用のPod」や「L7の処理を行うNginx Ingress Controller Pod」のReplica数が固定だと、トラフィックが増えたときにさばききれなくなる可能性があるため、Podのオートスケーリングを行うHorizontal Pod Autoscaler(HPA)の利用も検討して下さい。

また、デプロイしたIngress Controllerはクラスタ上の全てのIngressリソースを見てしまうため、衝突する可能性があります。その場合はIngress Classを利用することで、処理する対象のIngressリソースを分けることが可能です。

例えばService AとService Bがそれぞれあり、GKEのIngress Controllerで下記のようなIngressリソースを2つ作成した場合、2つのGCLBが作成され、エンドポイントは2つ払い出された状態となります。

予期している挙動(Ingress Classを利用しない状態)

予期している挙動(Ingress Classを利用しない状態)

Nginx Ingress Controllerにも上記の挙動を期待していると、予期せぬ状態となります。Ingress Controller自体がL7ロードバランサの実体であるように実装されているため、何も指定していない場合は全てのIngressリソースをwatchしてNginx Podの設定を更新するため、分離が行われません。

実際の挙動(Ingress Classを利用しない状態)

実際の挙動(Ingress Classを利用しない状態)

この分離性を確保するために、IngressリソースにIngress Classのアノテーションを付与し、Nginx Ingress Controllerに対象とするIngress Classを設定することで、対象の分離を行うことが可能です。

  • Nginx Ingress Controllerの起動時に--ingress-classオプションを渡す
    • /nginx-ingress-controller --ingress-class=system_a ……
  • Ingressリソースにannotationsをつける
    • kubernetes.io/ingress.class: "system_a"
Ingress Classを利用することによる対象の分離

Ingress Classを利用することによる対象の分離

この他、より詳細な仕様は下記を確認して下さい。

kubernetes/ingress-nginx

まとめ

KubernetesではPodのサービスディスカバリやL4ロードバランシング機能を提供するためにServiceリソースが用意されています。また似たようなリソースとして、L7ロードバランシング機能を提供するIngressも用意されています。

  • Service
    • L4 LoadBalancing
    • クラスタ内DNSによる名前解決
    • ラベルを利用したPodのサービスディスカバリ
  • Ingress
    • L7 LoadBalancing
    • HTTPS終端
    • パスベースルーティング

Serviceではエンドポイントを提供する複数のtypeが用意されており、要件に合わせて選択することが可能です。基本的には、内部向けエンドポイントを払い出したいときに使う「type: ClusterIP」と、外部向けエンドポイントを払い出したいときに使う「type: LoadBalancer」を利用することが多いです。

Serviceのtypeと提供されるエンドポイント

Serviceエンドポイント
ClusterIPKubernetes Cluster内でのみ疎通可能なVIP
ExternalIP特定のKubernetes NodeのIP
NodePort全Kubernetes Nodeの全IP(0.0.0.0)
LoadBalancerクラスタ外で提供されているLoadBalancerのVIP
ExternalNameCNAMEを用いた疎結合性の確保
HeadlessPodのIPを用いたDNS Round Robin

一方でIngressにはtypeなどはありませんが、実装により仕様が大きく異なるケースがあるため注意して下さい。

Ingressの実装と仕様

Ingressの種類実装
クラスタ外のロードバランサを利用したIngressGKE
クラスタ内にIngress用のPodを展開するIngressNginx Ingress
Nghttpx Ingress

次回は、Config&Storageリソースの解説を予定しています。

株式会社サイバーエージェント アドテク本部 Strategic Infrastructure Agency

2016年入社。OpenStackを使ったプライベートクラウドやGKE互換なコンテナプラットフォームをゼロから構築し、国内カンファレンスでのKeynoteに登壇。
その後、世界で2番目にCertified Kubernetes Application Developer、138番目にCertified Kubernetes Administratorの認定資格を取得。
現在はKubernetesやOpenStackなどOSSへのコントリビュート活動をはじめ、CNCF公式のCloud Native Meetup TokyoのOrganizerやJapan Container Daysの運営などコミュニティ活動にも従事している。

連載バックナンバー

仮想化/コンテナ技術解説
第9回

KubernetesのConfig&Storageリソース(その2)

2018/6/6
連載9回目となる今回は、Config&Storageリソースのうち、PersistentVolumeClaimについて解説する。
仮想化/コンテナ技術解説
第8回

KubernetesのConfig&Storageリソース(その1)

2018/5/31
今回と次回の2回に渡って、5種類に大別されるKubernetesのリソースのうち、Config&Storageリソースを解説する。
仮想化/コンテナ技術解説
第7回

KubernetesのDiscovery&LBリソース(その2)

2018/5/16
前回に引き続き、Discovery&LBリソースのServiceとIngressを紹介する。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています