はじめに
オンプレや仮想マシンに構築したKubernetesのサービスをクラスターのネットワーク外に公開したい場合に、その方法がすぐに思いつかなかったり、適切な方法がわからなかったりすることが多いと思います。本記事ではそのような場合になるべく迷わないように、Kubernetesで作ったサービスを外部公開する方法をまとめました。
要約
本記事で紹介する3つの公開方法のメリットとデメリットを、表にまとめます。
| 方法 | 長所 | 短所 |
|---|---|---|
| NodePort |
type:NodePort に指定すればよく、設定が簡単 |
使えるポート番号が30000-32767に限られる |
| externalIPs | 公開するIPアドレスをマニフェストに書いて対応可能 | IPアドレスを誤って設定するとトラブルになりかねない |
| hostNetwork | Pod系リソースに設定すればよく、Serviceリソースが不要 | ポート競合、ノードIP変更の可能性。capabilities追加が必要になる場合もある |
これ以外でも、MetalLBやIngress Controller、Gateway APIを使う方法がある。
Kubernetesネットワークのおさらい
最初に、Kubernetesのネットワークについて簡単におさらいしておきます。
Kubernetes には、主に2種類のネットワークがあります。一つは各PodにIPアドレスを割り当てて、Pod同士が直接通信するための「Podネットワーク」、もう一つはServiceリソースに仮想的なIPアドレス(ClusterIP)を割り当て、Podの集合に対して安定した通信経路を提供する「Serviceネットワーク」(ClusterIPネットワークとも言う)です。
例えばDeploymentリソースを作るとその背後にPodが作られて、Podには指定されたCIDRからIPアドレスが自動で付与されます。さらにServiceリソースを作りPodへルーティングすると、ServiceにもClusterIPのCIDRに基づいてIPアドレスが自動で付与されます。
これにより、Kubernetesクラスターの内部ではClusterIP(またはDNSで解決される名前)やPodのIPを使って、Podへ通信できるようになります。
しかし、このままではクラスター外からはアクセスできません。仮にクラスターがローカルネットワーク上に構築されていても、そのネットワークからClusterIPやPodのIPでService/Podへアクセスしようとしても到達できません。これらのネットワークはクラスター内部にのみ存在するためです。
以下の図は、その様子を表したものです。
このため、サービスを外部公開するには「ローカルネットワークからサービスにアクセスするための設定」が必要となります。
方法1:NodePortを使う
一つの方法は、ServiceリソースのtypeをNodePortに設定することです。NodePortのServiceを作ると、クラスター内の各ノードに対して30000–32767の範囲のポートが(自動or手動で)割り当てられ、ノードのIP:NodePortでアクセスできるようになります。
試しにkubectlコマンドで作ってみましょう。
kubectl create deployment nginx \
-n default \
--image=nginx \
--replicas=2
kubectl expose deployment nginx \
-n default \
--type=NodePort \
--port=80 \
--target-port=80
以下のように、NodePortのServiceリソースが作られました。
$ kubectl get svc -n default -l app=nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx NodePort 10.106.103.249 <none> 80:31980/TCP 53s
今回はポート番号を指定しなかったので31980のポートが割り当てられていますが、 30000–32767の範囲で値を指定して固定にすることも可能です。
例えばノードが2つあり、それぞれローカルネットワーク上のIPアドレスを 192.168.1.4、192.168.1.5 とすると、このサービスには次のようにアクセス可能です。
$ curl -I http://192.168.1.4:31980 2>/dev/null | head -n1
HTTP/1.1 200 OK
$ curl -I http://192.168.1.5:31980 2>/dev/null | head -n1
HTTP/1.1 200 OK
方法2:externalIPsを使う
2つ目は、ServiceリソースのexternalIPsを使って、クラスター外からアクセスするIPアドレスを直接指定する方法です。マニフェストでは、以下のように書きます。
apiVersion: v1
kind: Service
metadata:
labels:
app: nginx-ext
name: nginx-ext
namespace: default
spec:
externalIPs: # アクセスするIPのリストを書く
- 192.168.1.4
ports:
- port: 80
targetPort: 80
selector:
app: nginx
このマニフェストをもとに、Serviceリソースを作成します。
kubectl apply -f external-ips.svc.yaml
次のようなServiceリソースが作成されます。
$ kubectl get svc -n default -l app=nginx-ext
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-ext ClusterIP 10.108.27.41 192.168.1.4 80/TCP 33s
NodePortと異なる点は、以下です。
- TYPEが
ClusterIPである - EXTERNAL-IPが付与されている
- ポートはServiceと同じ80が使える
externalIPsに記載したIP 192.168.1.4 でアクセスすると、以下のように80番ポートでアクセスできます。
curl -I http://192.168.1.4:80 2>/dev/null | head -n1
HTTP/1.1 200 OK
しかしながら、NodePortでアクセスできていた 192.168.1.5 は externalIPs に記載していないので、アクセスできなくなります。
$ curl -I http://192.168.1.5:80
curl: (7) Failed to connect to 192.168.1.5 port 80 after 17 ms: Couldn't connect to server
192.168.1.5 からも到達可能にするには、externalIPsに追加すればOKです。
spec:
externalIPs: # アクセスするIPのリストを書く
- 192.168.1.4
- 192.168.1.5 # 追加
以下が、追加したマニフェストをapplyした後の結果です。
curl -I http://192.168.1.5:80 2>/dev/null | head -n1
HTTP/1.1 200 OK
以下は、externalIPs でつながる様子を表したものです。

問題:実在しないホストのIPでもつながってしまう
試しに externalIPs に、どのホストにも割り当てられていないでたらめなIPアドレスを追加してみましょう。
spec:
externalIPs: # アクセスするIPのリストを書く
- 192.168.1.4
- 192.168.2.4 # 実在しないIP
このマニフェストをapplyすると、Kubernetesのノードが存在するホストからに限りますが、なんと実在しないIPにつながってしまいます。
curl -I http://192.168.2.4:80 2>/dev/null | head -n1
HTTP/1.1 200 OK
原因は、iptablesのルーティングに関する設定で、以下のように 192.168.2.4 が書かれているからのようです。
$ sudo iptables -t nat -nL | grep 192.168.2.4
KUBE-EXT-E3ZCWTDGY4D3CK2O 6 -- 0.0.0.0/0 192.168.2.4 /* default/nginx-ext external IP */ tcp dpt:80
このように、ローカルネットワークに実在しないIPに到達できてしまうとネットワークトラブルの原因になり得るため、externalIPsはリスクを理解したうえで使ったほうがよさそうです。
方法3:hostNetworkを使う
3つ目は、PodのマニフェストでhostNetworkを有効にする方法です。以下のマニフェストのように、spec.hostNetworkをtrueに設定します。
apiVersion: v1
kind: Pod
metadata:
name: nginx-hostnetwork
namespace: default
labels:
app: nginx-hostnetwork
spec:
hostNetwork: true # ここで設定
dnsPolicy: ClusterFirstWithHostNet
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
protocol: TCP
このマニフェストを kubectl apply -f hostnetwork.pod.yaml でデプロイすると、PodにはローカルネットワークのノードのIP 192.168.1.4 が割り当てられます。
$ kubectl get po -n default nginx-hostnetwork -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-hostnetwork 1/1 Running 0 13m 192.168.1.4 node01 <none> <none>
この場合、Serviceリソースを使わずに、クラスターの外から直接リクエストが届きます。
$ curl -I http://192.168.1.4 2>/dev/null | head -n1
HTTP/1.1 200 OK
以下は、hostNetwork でつながる様子を表したものです。

注意点
ポート競合や、ノードIPの変更の可能性
hostNetworkを使うPodをDeploymentで作成すると、一つのノードに複数のPodが作成されてポートの競合を起こしてしまう可能性があるため、replica数を1にする等の対策が必要です。また、外部からアクセスできるのはPodがデプロイされたノードのIPに限られ、再起動等でノードが変わるとアクセス先IPも変わる可能性があります。IPアドレスを固定するには、マニフェストで nodeSelector を設定する等の工夫が必要になります。
これらの問題をシンプルに避けるなら、DaemonSetが有効です。DaemonSetによって全ノードにPodを配置できるため、どのノードのIPからもアクセス可能になります。
ポート0-1023を使うのに、capabilities追加が必要になる可能性
先ほどの例では、nginxのコンテナがroot権限で起動して問題なく80番ポートを利用できましたが、一般ユーザーで起動するコンテナの場合、Well-Knownポートの0-1023番が使えない可能性が高いです。この場合、コンテナの securityContext に、以下のように NET_BIND_SERVICE を追加する必要があります。
securityContext:
...
capabilities:
add: ["NET_BIND_SERVICE"] # 追加
drop: ["ALL"]
この内容はWebのみで調査したもので、実際の動作は未確認です。
(参考)その他の方法
それ以外だと、以下の方法でサービスの外出しが可能です。
-
type: LoadBalancerのServiceリソースを利用- MetalLB など
- Ingress Controllerを利用
- NGINX Ingress Controller
- Traefik Kubernetes Ingress provider など
- Gateway APIを利用
- Envoy Gateway
- Istio など
設定方法や仕組みは使うツールによって異なるので、この記事では説明を割愛いたします。
インターネットに公開するには?
これまでに説明した方法で、ローカルネットワークからKubernetesのサービスへアクセスできるようになりますが、インターネットに公開するには、クラスターの外側(ルーター/ファイアウォール/クラウド側)で外部からの通信を受けてクラスターへ転送する設定が別途必要です。
例えば自宅LANなら、ルーターでポートフォワーディング(NAT)を設定し、グローバルIPの特定ポートへの通信をクラスター内のノードのIPへ転送します。クラウド上のVMなら、セキュリティグループ/ファイアウォールで必要なポートを許可し、外部からVM(=ノード)へ到達できるようにします。あるいは、クラウドのマネージドなLoadBalancerを前段に置いて外部からの入口を用意する方法もあります。
※ 余談ですが、私の自宅LAN環境ではインターネット公開が簡単にできないことが判明しました。。。理由は、インターネット通信する際に使われるパブリックIPが、複数ユーザーで共有される「MAP-E」という方式で割り当てられているためです。こちらの記事を参考に、頑張ってVPN構築すれば突破できるかもしれません。
おわりに
私自身、このあたりの知識が曖昧だったので整理のために書きました。読んだ方にも参考になれば幸いです。

