NetworkPolicyのEgressルールで名前解決を可能にする

# はじめに

NetworkPolicyは、Kubernetesクラスター内のPod間の通信を制御するためのルールです。デフォルトではKubernetesのPodはすべてのPodに通信可能ですが、NetworkPolicyを使うことで特定の通信だけを許可できます。AWSのSecurity Group（SG）、AzureのNetwork Security Group（NSG）、Linuxのfirewalldやiptablesといった仕組みと同様に、KubernetesではNetworkPolicyを使って通信を制御できます。

AWSのSecurity Groupがインバウンドとアウトバウンドのルールを持つように、NetworkPolicyでも外からの通信を制御するIngressと、外への通信を制御するEgressを定義できます。

今回はあるPodに対してNetworkPolicyのEgressルールを定義し、外部に対してFQDN（ホスト名やドメイン名）でアクセスする際の注意点について、実際にリソースを作りながら説明します。

# 先に要約（TL;DR）

NetworkPolicyのEgressルールを定義して、外部に対してFQDN（ホスト名やドメイン名）でアクセス可能にするには、**DNSにアクセスするためのルールを自分で追加** する必要があります。

# 環境情報

### Kubernetesのバージョン

Kubernetesクラスターはv1.32.2で構築しています。
```
$ kubectl version | grep -i server
Server Version: v1.32.2
```

### CNI

Ciliumのv1.16.5を使っています。
```
$ kubectl get ds -n kube-system -l k8s-app=cilium -owide | awk '{print $1 " " $10}' | column -t
NAME    CONTAINERS
cilium  quay.io/cilium/cilium:v1.16.5@sha256:758ca0793f5995bb938a2fa219dcce63dc0b3fa7fc4ce5cc851125281fb7361d
```

# 検証用リソースの作成

以下のリソースを作成します。

## curl

外部にHTTPリクエストを送信するためのPodです。

```
kubectl run curl -n default --image=alpine/curl --command -- tail -f /dev/null
```

後ほど、NetworkPolicyのEgressルールをこのPodに適用します。


## nginx

HTTPのサービスを提供するPodです。

```
kubectl run nginx -n default --image=nginx
```

後ほどcurlのPodからここにアクセスして、NetworkPolicyの設定がどのように効いてくるのかを確認します。

nginxは以下のコマンドでServiceリソースも作っておきましょう。

```
kubectl expose pod nginx -n default --port=80 --target-port=80 --name=nginx
```

## 作成されたリソースの確認

defaultのnamespaceに次のようなリソースが作られました。

```
$ kubectl get po -n default
NAME    READY   STATUS    RESTARTS   AGE
curl    1/1     Running   0          25s
nginx   1/1     Running   0          20s
```
```
$ kubectl get svc -n default | grep -v ^kubernetes | column -t
NAME   TYPE       CLUSTER-IP   EXTERNAL-IP  PORT(S)  AGE
nginx  ClusterIP  10.98.61.53  <none>       80/TCP   24s
```

# curl から nginx につながることの確認

NetworkPolicyを作る前に、現在の状態でcurlのPodからnginxのPodにアクセスできることを確認します。

## nginxのPodのIPアドレスで接続可能かの確認

nginxのPodのIPアドレスを事前に確認します。

```
$ kubectl get pod -n default -owide | grep ^nginx
NAME    READY   STATUS    RESTARTS   AGE   IP              NODE          NOMINATED NODE   READINESS GATES
nginx   1/1     Running   0          13m   192.168.1.88    ip-10-0-0-4   <none>           <none>
```

`192.168.1.88`ということがわかったので、curlのPodからアクセスしてみましょう。

```
$ kubectl exec -it curl -- curl -I 192.168.1.88 2>/dev/null | head -n 1
HTTP/1.1 200 OK
```

問題なくアクセスできました。

## nginxのService経由で接続可能かの確認

続いて、nginxのServiceリソースを経由してアクセスできるかも確認しておきます。

```
kubectl exec -it curl -- sh
```

コンテナ内でcurlを実行すると、ServiceのIPアドレスだけではなくて、FQDNでもアクセス可能なことがわかります。

```
/ # curl -I 10.98.61.53 2>/dev/null | head -n 1
HTTP/1.1 200 OK
/ # curl -I nginx 2>/dev/null | head -n 1
HTTP/1.1 200 OK
/ # curl -I nginx.default.svc.cluster.local 2>/dev/null | head -n 1
HTTP/1.1 200 OK
```

※ `10.98.61.53` は `nginx` のServiceリソースのクラスターIPで、先ほど ``kubectl get svc`` を実行した際に出力されていたのを使いました。


# EgressのNetworkPolicyを作成

続いて、「curlのPodから外への通信は、ポート80のTCPのみを許可する」NetworkPolicyのマニフェストを作成します。

「適用対象がcurlのPodである」ことをNetworkPolicyのマニフェストで表現するために、curlのPodに付与されているラベルを確認します。

```
$ kubectl get pod curl --show-labels
NAME   READY   STATUS    RESTARTS   AGE   LABELS
curl   1/1     Running   0          37m   run=curl
```

``run=curl``というラベルが付いていることがわかりました。この情報を元に作成したNetworkPolicyのマニフェストが以下です。


```yaml:curl-allow-external-80.netpol.yaml
apiVersion: "networking.k8s.io/v1"
kind: NetworkPolicy
metadata:
  name: curl-allow-external-80
  namespace: default
spec:
  podSelector:     # ルールの適用対象がcurlのPodであることを表す
    matchLabels:
      run: curl
  policyTypes:     # Egressのルールのみを適用（Ingressは対象外）
  - Egress
  egress:          # 80番ポートでTCPのみを許可
  - ports:
    - port: 80
      protocol: TCP
```

上記のマニフェストを元に、NetworkPolicyを作成します。

```
kubectl apply -f curl-allow-external-80.netpol.yaml
```

どのようなルールが作成されたかを確認する場合は、以下のように``kubectl describe netpol``を実行するとわかりやすく表示されます。
```
$ kubectl describe netpol -n default curl-allow-external-80
Name:         curl-allow-external-80
Namespace:    default
Created on:   2025-03-16 17:14:01 +0900 JST
Labels:       <none>
Annotations:  <none>
Spec:
  PodSelector:     run=curl
  Not affecting ingress traffic
  Allowing egress traffic:
    To Port: 80/TCP
    To: <any> (traffic not restricted by destination)
  Policy Types: Egress
```

# curl から nginx につながることの確認（NetworkPolicy作成後）

NetworkPolicyを作成した後に、curlのPodからnginxのサービスにアクセスできるかを確認します。「Egressでアクセス制限はしているけど、80番のポートは空けているからnginxのサービスにはアクセスできるはずだ」という前提です。

まずは、curlのPodのコンテナに入ります。

```
kubectl exec -it curl -- sh
```

curlのPod、およびServiceのIPアドレスにアクセスすると、想定通りつながります
```
/ # curl -I 192.168.1.88 2>/dev/null | head -n 1
HTTP/1.1 200 OK
/ # curl -I 10.98.61.53 2>/dev/null | head -n 1
HTTP/1.1 200 OK
```

ところが、curlのServiceに対応するFQDNを使うと、アクセスできません...

```
/ # curl -I nginx
curl: (6) Could not resolve host: nginx
/ # curl -I nginx.default.svc.cluster.local
curl: (6) Could not resolve host: nginx.default.svc.cluster.local
```

出力からわかるように、「curlの引数で指定したFQDNを名前解決できない」のが原因です。

AWS、Azureなどのクラウドプロバイダーで今回と同様にアウトバウンドルールを作ると、各クラウドプロバイダーが提供するデフォルトのDNSで名前解決してくれますが、KubernetesのNetworkPolicyはそうではないようです。そのため、「DNSの名前解決を許可するためのEgressルール」を自前で追加する必要があります。

# kube-dnsにアクセスするためのEgressルールを追加

KubernetesクラスターのDNSは、``kube-dns``というServiceリソースで提供されます。このリソースは、以下のコマンドで ``kube-system``というnamespaceでデプロイされていることがわかります。

```
$ kubectl get svc -n kube-system -l k8s-app=kube-dns
NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
kube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   20d
```

ちなみに``kube-dns``のServiceのバックエンドにあるDNSのPodは、``coredns``と呼ばれるサービスです。
```
$ kubectl get pod -n kube-system -l k8s-app=kube-dns
NAME                       READY   STATUS    RESTARTS   AGE
coredns-668d6bf9bc-2mjdf   1/1     Running   0          20d
coredns-668d6bf9bc-krz5b   1/1     Running   0          20d
```

上記コマンドの引数で指定していますが、DNS関連のリソースには``k8s-app=kube-dns``というラベルがついています。

そこで、curlのPodが、この ``kube-dns``のService（= ``coredns``のPod）にアクセスするためのルールを以下のように作ります。

```yaml:curl-allow-dns.netpol.yaml
apiVersion: "networking.k8s.io/v1"
kind: NetworkPolicy
metadata:
  name: "curl-allow-dns"
  namespace: default
spec:
  podSelector:    # ルールの適用対象がcurlのPodであることを表す
    matchLabels:
      run: curl
  policyTypes:    # Egressのルールのみを適用（Ingressは対象外）
  - Egress
  egress:
  - to:
    - namespaceSelector:  # kube-systemのnamespaceが対象
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:        # kube-dnsのService（=corednsのPod）が対象
        matchLabels:
          k8s-app: kube-dns
    ports:
    - port: 53
      protocol: UDP
    - port: 53
      protocol: TCP
```

``to:``の部分がやや複雑になっていますが、「『namespaceが``kube-system``』かつ『``k8s-app=kube-dns``のラベルがついている』リソースの53ポートに、UDPまたはTCPでアクセス可能」というルールを記述しています。

上記のマニフェストを元に、新たなNetworkPolicyを作成します。

```
kubectl apply -f curl-allow-dns.netpol.yaml
```

以下のようなルールが追加されました。

```
$ kubectl describe netpol -n default curl-allow-dns
Name:         curl-allow-dns
Namespace:    default
Created on:   2025-03-16 18:09:56 +0900 JST
Labels:       <none>
Annotations:  <none>
Spec:
  PodSelector:     run=curl
  Not affecting ingress traffic
  Allowing egress traffic:
    To Port: 53/UDP
    To Port: 53/TCP
    To:
      NamespaceSelector: kubernetes.io/metadata.name=kube-system
      PodSelector: k8s-app=kube-dns
  Policy Types: Egress
```


# curl から nginx にFQDNでもつながることの確認（NetworkPolicy修正後）

再度、curlのPodからFQDNでつながるかを確認します。

```
kubectl exec -n default -it curl -- sh
```

```
/ # curl -I nginx 2>/dev/null | head -n 1
HTTP/1.1 200 OK
/ # curl -I nginx.default.svc.cluster.local 2>/dev/null | head -n 1
HTTP/1.1 200 OK
```

問題なく繋がりました！

Kubernetesクラスター内部だけでなく、外部の80ポートのサーバにもFQDNでつながることも確認できます。

```
/ # curl -I google.com 2>/dev/null | head -n 1
HTTP/1.1 301 Moved Permanently
/ # curl -I example.com 2>/dev/null | head -n 1
HTTP/1.1 200 OK
```

# おわりに

Kubernetesの標準リソースの中で、NetworkPolicyはやや取っ付きにくい印象を持っていましたが、今回の検証で理解を深めることができました。本記事が皆さまのお役に立てれば幸いです。