CKAに合格してもIngressがよくわからない...
Kubernetesを学ぶと、最初の方でサービス系の基本的なリソースとしてPod, Deployment, Serviceが出てきます。ここまで知って、「Deploymentを使って複数のPodを作って冗長化して、Serviceで複数のPodにトラフィックをよしなに送ってくれるんだね。動いていないPodがあったらそこには通信しない仕組みもあるんだね。まあなんと素敵!!」と多くの人が(おそらく)感動します。
さらにConfigMapとかSecretとかPV,PVCとかがある程度わかってくると、「もうだいたいKubernetesわかってきたんじゃね?自分で作れるんじゃね!?」という気分になってきます。
そんな高揚感を打ち砕くように突如現れるのが、Ingress。
「うっ...、Ingressかぁ... Ingressねぇ...」
CKAやCKADで試験勉強をした場合、KodeKloudやkiller.shでIngressの問題が出てきます。よくわかっていないながらも何回かやっていくうちに一応解けるようになり、晴れてCKA合格!
だけど、Ingressに関してはずっと自信が持てない。。。
私にとってのIngressはそんな存在でした。
そこで本記事は、Ingressを使う必要性を考察しつつ、オンプレKubernetes上で実際にIngressリソースを作り理解を深めることを目指して執筆します。
なぜIngressを使うのか?
Ingressの説明については、以下のkubernetes.ioをご参照ください。
上の記事には「Ingressの機能として負荷分散、SSL終端、名前ベースの仮想ホスティングの3つがある」と書かれていますが、これらはIngressを使わなくても、例えばnginxで実現できます。nginxの設定ファイル(nginx.conf
等)で、負荷分散はlocation
ディレクティブ内、SSL終端は ssl_
で始まる設定値、名前ベースの仮想ホスティングはserver_name
にホスト名を設定すればOKです。
なので別にIngressを使わなくても、nginxのDeploymentとServiceを立ち上げれば同じことができるわけです。
ではなぜIngressを使うのか?
それは、Ingressを使うと「負荷分散」「SSL終端」「名前ベースの仮想ホスティング」等の設定をKubernetesマニフェストでシンプルに設定でき、汎用性も上がるからです。
もしリバースプロキシ的な機能をIngressではなくて、nginx単体で実装しようとするとどうなるでしょうか。
この場合、ConfigMapにnginx.conf
を自分で用意して、nginxのDeploymentにそれをマウントする手間が必要になります。また、nginx以外のリバースプロキシツール(TraefikやApache等)を使いたくなった場合に、設定ファイルを一から作り直さなければなりません。場合によっては、新しく使うツールの設定ファイルの仕様の調査から必要になってしまうでしょう。
これをIngressで実現すると、使うリバプロツールによらずに全く同じ書き方で、負荷分散やSSL終端等の設定を表現できます。
以下は簡単なIngressのマニフェストの例です。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: simple-ingress
namespace: default
spec:
ingressClassName: nginx # ここでリバプロツールを指定している
rules:
- host: example.com # 「名前ベースの仮想ホスティング」の設定
http:
paths:
- path: / # 「負荷分散」の設定
pathType: Exact
backend:
service:
name: example-service
port:
number: 80
tls: # 「SSL終端」の設定
- hosts:
- example.com
secretName: cert
マニフェストにもコメントしていますが、どのリバプロツールを使うかは spec.ingressClassName
で指定できます。例えばnginxからTraefikに変えたくなった場合は、ingressClassName: traefik
と書き換えるだけでおおかた済みます。Ingressを使って非常にシンプルになるのがおわかりいただけますでしょうか!
尚、後述しますが、nginxやTraefikを使うためには各々の「Ingressコントローラー」のインストールが必要です。使えるIngressコントローラーは以下をご参照ください。
Ingressを作ってみる
それでは実際にIngressを作ってみましょう。
事前状況の確認
kubectl
コマンドで、IngressのリソースはKubernetesのデフォルトで使える状態であることがわかります。
$ kubectl api-resources | grep -e ^NAME -e networking
(出力結果は少し編集)
NAME SHORTNAMES KIND
ingressclasses IngressClass
ingresses ing Ingress
networkpolicies netpol NetworkPolicy
しかしながら、IngressコントローラーをインストールしていないのでIngressを正常にデプロイすることはできません。Ingressコントローラーをインストールするとそれに対応するIngressClassリソースが作成されますが、初期状態だと以下のようにリソースが存在しないです。
$ kubectl get ingressclasses
No resources found
そこでまずは、何かしらのIngressコントローラーのインストールが必要となります。
Ingress NGINX Controller のインストール
今回は、nginxのIngressコントローラーをHelmチャートでインストールします。
HelmチャートのインストールはHelmfileが便利です。以下のYAMLファイルをhelmfile
コマンドでインストールします。
repositories:
- name: ingress-nginx
url: https://kubernetes.github.io/ingress-nginx
releases:
- name: ingress-nginx
namespace: ingress-nginx
createNamespace: true
chart: ingress-nginx/ingress-nginx
version: 4.11.2
実行コマンド
helmfile apply -f helmfile-ingress-nginx.yaml
上記を実施するには、helm
および helmfile
のインストールが必要です。
間もなくして、ingress-nginx-controller のリソースが作成されます。
$ kubectl get deploy -n ingress-nginx
NAME READY UP-TO-DATE AVAILABLE AGE
ingress-nginx-controller 1/1 1 1 22s
$ kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller LoadBalancer 10.110.19.229 <pending> 80:31958/TCP,443:31030/TCP 22s
ingress-nginx-controller-admission ClusterIP 10.102.14.65 <none> 443/TCP 22s
IngressClassにも、ingress-nginx-controllerに対応するリソースが追加されたことがわかります。
$ kubectl get ingressclasses
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 4m4s
先にお伝えしておくと、この後作成するIngressに宛てたリクエストはIngressコントローラーを経由してバックエンドのリソースに届きます。少しくどいですが、上の例だとIngressコントローラーは「ingress-nginx-controller
のServiceリソース」です。
したがって、疎通はingress-nginx-controller
のNodePortを使って以下のように確認できます。
※ 【注意!】現時点ではまだIngressを作っていないのでつながりません
httpの場合
curl http://[hostname]:31958
httpsの場合
curl https://[hostname]:31030
バックエンド用のリソースの作成
最初にバックエンド用のリソースを作成します。マニフェストで作るのが面倒なので、以下のコマンドでnginxとhttpdのDeployent,およびServiceリソースを作ります。
kubectl create deployment backend-nginx --image=nginx --replicas=1
kubectl expose deployment backend-nginx --port=80 --target-port=80 --name=backend-nginx
kubectl create deployment backend-httpd --image=httpd --replicas=1
kubectl expose deployment backend-httpd --port=80 --target-port=80 --name=backend-httpd
リソースが作成できたことを確認します。
$ kubectl get po -n default -l app
NAME READY STATUS RESTARTS AGE
backend-httpd-5f8458bbcd-nchlb 1/1 Running 0 13s
backend-nginx-8cb49db7d-d2jf2 1/1 Running 0 25s
$ kubectl get svc -n default -l app
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
backend-httpd ClusterIP 10.110.192.166 <none> 80/TCP 14s
backend-nginx ClusterIP 10.97.122.179 <none> 80/TCP 23s
Ingressの作成
準備は整ったので、お待ちかねのIngressリソースを作成します。
IngressもYAMLを書かずとも、以下のようなコマンドで一応作れます。
kubectl create ingress \
ing-multipath \
-n default \
--class=nginx \
--annotation nginx.ingress.kubernetes.io/rewrite-target=/ \
--rule="/nginx=backend-nginx:80" \
--rule="/httpd=backend-httpd:80"
上記コマンドの最後に --dry-run=client -o yaml
を加えてYAMLで出力すると、次のようなマニフェストになるのを確認できます。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: / # 後で説明
creationTimestamp: null
name: ing-multipath
namespace: default
spec:
ingressClassName: nginx
rules:
- http:
paths:
- backend:
service:
name: backend-nginx
port:
number: 80
path: /nginx
pathType: Exact
- backend:
service:
name: backend-httpd
port:
number: 80
path: /httpd
pathType: Exact
nginx.ingress.kubernetes.io/rewrite-target: /
というannotationは、バックエンドにリクエストを転送する際にパスを書き換えるのに必要です。例えば /nginx
のパスで来たリクエストは、backend-nginx
のServiceには /
のパスに書き換えられて転送されます。これを書いておかないと、backend-nginx
にも/nginx
のパスでリクエストされて "404 Not Found" が返ってきます。(おそらく多くの人が一度はここでハマるはず...)
各Ingressコントローラーに固有の設定は、上の例のように annotation に設定を書く必要があります。本格的に業務でIngressを使うことになった場合、 annotation を活用することが必須になるので、是非覚えておいてください!
さて、以下のようにIngressリソースが作成されました。
$ kubectl get ing -n default
NAME CLASS HOSTS ADDRESS PORTS AGE
ing-multipath nginx * 80 7s
実際にリクエストして、バックエンドにリソースが届くかを確認してみましょう。
今回作っているk8sクラスターのノードは一つで、IPアドレスが 10.0.0.4
です。
$ kubectl get node -owide | awk '{print $1 " " $5 " " $6}' | column -t
NAME VERSION INTERNAL-IP
ip-10-0-0-4 v1.29.8 10.0.0.4
そこで、10.0.0.4
とhttpのNodePortを目がけてcurlを叩いてみます。
$ curl http://10.0.0.4:31958/nginx 2>/dev/null | head -n 4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
$ curl http://10.0.0.4:31958/httpd 2>/dev/null | head -n 4
<html><body><h1>It works!</h1></body></html>
nginxとhttpdのバックエンドに、見事にリクエストが届きました!
補足:パスの置き換え規則について
上の例では、バックエンドのパスの置き換え規則を次のように書きました。
metadata:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec
rules:
- http:
paths:
- path: /nginx
pathType: Exact
...
これだとバックエンドのpathが /nginx
一つであれば問題ないですが、例えば /nginx/about-us.html
や /nginx/contact.html
のように、/nginx/
の下に複数のサイトがあるとうまく動きません。
この場合は次のように、rewrite-target
とpath
に正規表現を使うことで解決します。
metadata:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2 # pathの正規表現でマッチした2番目のグループのテキストに書き換える
spec
rules:
- http:
paths:
- path: /nginx(/|$)(.*) # 正規表現にする
pathType: ImplementationSpecific # 後述
...
pathType: ImplementationSpecific
はドキュメントを見ると「このpathTypeはIngressClassに依存する」と書かれているので、ingress-nginx-controller
がよしなに処理してくれるものと思われます。
pathType: Prefix
にしても動きはしたのですが、Ingress作成時に以下のワーニングが出たので、ImplementationSpecific
に変更しました。
Warning: path /nginx(/|$)(.*) cannot be used with pathType Prefix
Ingressのカスタマイズ
上で作った ing-multipath.yaml
のマニフェストをカスタマイズする事例を紹介します。
ホスト名を指定
spec.rules[].host
でクライアントがリクエストするホスト名を指定できます。
...
spec:
ingressClassName: nginx
rules:
- http:
paths:
...
host: example.com # 追加
この場合、クライアントはIPアドレスではなく、次のようにexample.com
でリクエストする必要があります。
curl http://example.com:31958/nginx
example.com
はクライアント側で名前解決してノードのIPアドレス(今回だと10.0.0.4
)に変換する必要があります。動作検証が目的であれば、/etc/hosts
に 10.0.0.4 example.com
を一行追記するのが手軽でしょう。
名前解決しなくても、以下のようにヘッダーにホスト名を追加することでもリクエスト可能です。
curl http://10.0.0.4:31958/nginx -H "Host: example.com"
ちなみにexample.com
を指定せずにIPアドレスでリクエストすると、ingress-nginx-controller
でホスト名が不正とみなされて404エラーになります。
$ curl -I http://10.0.0.4:31958/nginx 2>/dev/null | head -n 1
HTTP/1.1 404 Not Found
SSL証明書を使ってhttpsでリクエスト
spec.tls
にSSL(TLS)に関する情報を追記すると、httpsでセキュアに通信可能になります。
spec:
...
tls: # ここ以降は追加した行
- hosts:
- example.com
secretName: cert-selfsigned
secretName
で指定するSecretリソースにはドキュメントを参照すると、以下のようにSSL通信用のサーバ証明書と秘密鍵を格納する必要があります。
$ kubectl get secret -n default cert-selfsigned -oyaml | cut -c 1-48 | head -n 6
apiVersion: v1
kind: Secret
data:
ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk # CA証明書(必須ではないかも)
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tC # サーバ証明書
tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktL # 秘密鍵
SSLに関する証明書を自分で作成するのはだいぶ面倒臭いですが、例えば cert-manager を使うと自己(オレオレ)証明書であれば割と簡単に作れます。(作り方の詳細は本題から外れるため割愛いたします)
SSLに関するSecretリソースをIngressのマニフェストに追記して反映すると、以下のコマンドでhttpsでのリクエストが可能になります。
※ ちなみに -k
オプションは、SSL/TLS証明書の検証を無効化するためのオプションです。
curl -k https://10.0.0.4:31030/nginx
余談ですが、IngressコントローラーによってはSSLに関する設定を spec.tls
ではなくて、annotationで行うものもあります。例えばAWSのALB(Application Load Balancer)の場合は、alb.ingress.kubernetes.io/certificate-arn
の annotation で指定します。
IngressコントローラーをTraefikに変えてみる
続いて、Traefikを二つ目のIngressコントローラーとして作ってみます。
Ingress NGINX Controllerと同様に、以下のHelmfileを使って作成します。
repositories:
- name: traefik
url: https://traefik.github.io/charts
releases:
- name: traefik
namespace: ingress-traefik
createNamespace: true
chart: traefik/traefik
version: v30.1.0
以下のコマンドを実行します。
helmfile apply -f helmfile-ingress-traefik.yaml
TraefikのIngressコントローラーが作成されたことを確認します。
$ kubectl get deploy -n ingress-traefik
NAME READY UP-TO-DATE AVAILABLE AGE
traefik 1/1 1 1 11s
$ kubectl get svc -n ingress-traefik
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
traefik LoadBalancer 10.105.179.76 <pending> 80:30883/TCP,443:32586/TCP 16s
80:30883/TCP,443:32586/TCP
と書かれているので、httpだと30883のポート、httpsの場合は32586のポートにアクセスすれば問題なさそうです。
また、IngressClassにも traefik
が追加されているのを確認できます。
$ kubectl get ingressclasses
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 26h
traefik traefik.io/ingress-controller <none> 33s
準備ができたので、Traefik用のIngressリソースのマニフェストを作ります。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ing-multipath-traefik
namespace: default
annotations:
# nginxのIngressの "nginx.ingress.kubernetes.io/rewrite-target"
# と同等のアノテーション(詳細は後述)
traefik.ingress.kubernetes.io/router.middlewares: default-stripprefix@kubernetescrd
spec:
ingressClassName: traefik # nginxから変更
rules:
- http:
paths:
- backend:
service:
name: backend-nginx
port:
number: 80
path: /nginx
pathType: Exact
- backend:
service:
name: backend-httpd
port:
number: 80
path: /httpd
pathType: Exact
nginxのIngressからの変更点は「annotations
のKeyValue」と「ingressClassName
の値」、この2つだけです。
尚Traefikでは、パスを書き換えるのにMiddlewareリソースを別途作る必要があります。以下のように、Middlewareのマニフェストにおいてspec.stripPrefix
で削除したいプレフィックス列挙するものを作ります。
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: stripprefix
namespace: default
spec:
stripPrefix:
prefixes:
- "/nginx"
- "/httpd"
Ingressリソースのannotationで、作成したMiddlewareを以下のように参照する必要があります。
traefik.ingress.kubernetes.io/router.middlewares: default-stripprefix@kubernetescrd
値はどうやら [namespace]-[resource-name]@kubernetescrd
のように書く必要がありそうです。
上記のIngressおよびMiddlewareのリソースを作成すると、以下のようにTraefikのIngressコントローラーでもバックエンドにリクエストが届くことを確認できます!
$ curl http://10.0.0.4:30883/nginx 2>/dev/null | head -n 4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
$ curl http://10.0.0.4:30883/httpd 2>/dev/null | head -n 4
<html><body><h1>It works!</h1></body></html>
おわりに
実際にIngressを手を動かして作ってみることで、少し理解が進みました。
本記事の最初の方で、「もしIngressを使わなかったら、nginx.conf
のConfigMapを用意して、Deploymentにマウントする必要がある。これは手間だしイケていない」と書きましたが、実は私が個人で動かしているKubernetesクラスターが今その(イケていない?)作りになっています。なので近々Ingressに書き換えようかな。
そして次は、Istioを理解することを目指します!