はじめに
- こんにちは、新川です。先日執筆した以下の記事「Amazon EKS + Karpenter ハンズオン」では、EKSクラスタと高性能なオートスケーラーであるKarpenter の構築手順をハンズオン形式でご紹介しました。また、EKS の重要な機能であるPod Identity、AWS Load Balancer Controller に焦点を当てたハンズオンも続編のブログに執筆しています。
[ハンズオン] EKS + Karpenter のデプロイ・トラブルシューティング演習
EKS Pod Identity:S3アクセスを例に学ぶPod単位のIAM制御
EKS と AWS Load Balancer Controller で実現するPodへのアクセス制御
- 今回の記事では、前回の続編として、EKSネットワークのハンズオン後半を行います。後半では、Pod 単位にファイアウォール(セキュリティグループ)を割り当て、より高度なアクセス制御を実現するハンズオンに挑戦します!
EKSネットワークハンズオン前半:Pod間通信と名前解決を学ぶ
- 本記事では、上記のハンズオンで作成したEKS と Karpenter 環境があることを前提に進めます。
Pod単位のセキュリティグループはどのような技術で実現できるか?
- Amazon VPC CNI plugin for Kubernetes というアドオンを使用して実現します。このプラグインは、Pod に VPC の IPv4 または IPv6 アドレスを割り当てます。さらにこのプラグインを活用することで、Pod セキュリティグループ(Security groups for Pods)が使用できます。
従来の課題:ノード単位のセキュリティグループ
- Amazon VPC CNI plugin によって、各Podは、自身が稼働しているノードのENI(Elastic Network Interface)からセカンダリIPアドレスが割り当てられます。その際、デフォルトでは各Pod はノードと同じセキュリティグループを使用します。
- この仕組みは効率的ですが、同じノード上で動作するすべてのPod において、ノードのENIに設定された単一のセキュリティグループを共有することになります。これでは、「このPodはデータベースへのアクセスを許可し、あのPodは拒否する」といった、Podごとのきめ細やかなアクセス制御はできませんでした。
Amazon VPC CNI plugin のアドオンでPod セキュリティグループを使用する
- Amazon VPC CNI plugin のアドオン設定で、ENABLE_POD_ENIという環境変数をtrue に設定することで、特定のPod の動作モードを切り替えます。デフォルトのセカンダリIPアドレスを割り当てる代わりに、そのPod 専用の新しいENI を動的にプロビジョニングします。
- 指定されたセキュリティグループが、この新しく作成された専用ENIに直接設定されます。
- 上記の結果、Pod はノードのENI やセキュリティグループから完全に独立し、自身専用のネットワーク環境を持つことができます。これにより、あたかもPod がVPC 内に存在する独立したEC2 インスタンスのように、AWSネイティブなアクセス制御が可能になります。
SecurityGroupPolicyリソースを使った設定
- このPod セキュリティグループ(Security groups for Pods)をどのPodで有効にするかを定義するのが、SecurityGroupPolicyというKubernetesのカスタムリソースです。
- ユーザーは、特定のラベルを持つPod を対象(podSelector)とし、適用したいセキュリティグループのID(groupIds)を指定したYAMLファイルを作成します。このSecurityGroupPolicy をEKS クラスタに適用することで、ENABLE_POD_ENI が有効化されたAmazon VPC CNI plugin は対象のPod を認識し、Pod 専用のENI のプロビジョニングを開始します。
今回の検証シナリオ
- このハンズオンのシナリオを記載します。このハンズオンでは、Pod 単位のセキュリティグループが正しくアクセス制御を行うことを確認します。
- サーバーPod(backend-pod): Nginx サーバーを起動します。このPodには、特定のクライアントからの通信のみを許可するセキュリティグループ sg-backend を割り当てます。
- 許可されたクライアントPod (client-pod-allowed):サーバーへのアクセスが許可されているセキュリティグループ sg-client-allowed を設定します。このPod からサーバーへの通信が成功することを確認します。
- 拒否されたクライアントPod (client-pod-denied): サーバーへのアクセスが許可されていないセキュリティグループ sg-client-denied を設定します。このPod からサーバーへの通信がセキュリティグループによってブロックされる(失敗する)ことを確認します。
【ハンズオン】Pod単位のセキュリティグループ制御の実践
手順1:Amazon VPC CNI plugin のENABLE_POD_ENI 機能を有効化する
- コンソールで、Amazon VPC CNI plugin のアドオンが導入されていることを確認します。
- ノード用のセキュリティグループに設定されているタグを編集します。
- キー:kubernetes.io/cluster/
- 値:owned → shared
- キー:kubernetes.io/cluster/
- 以下のコマンドを使用して、Amazon VPC CNI plugin のアドオンを再導入します。
aws eks delete-addon \ --cluster-name ${CLUSTER_NAME} \ --addon-name vpc-cni export IAM_ROLE_ARN_ADDON_VPC_CNI="arn:aws:iam::111111111111:role/vpc-cniアドオンのロール名" aws eks create-addon \ --cluster-name ${CLUSTER_NAME} \ --addon-name vpc-cni \ --addon-version v1.19.5-eksbuild.1 \ --service-account-role-arn ${IAM_ROLE_ARN_ADDON_VPC_CNI} \ --configuration-values '{ "env": { "ENABLE_POD_ENI": "true" } }' \ --resolve-conflicts PRESERVE
- 以下、実行した際のログです。アドオンの導入が完了するまで数分待ちます。
~ $ aws eks describe-addon --cluster-name ${CLUSTER_NAME} --addon-name vpc-cni --query "addon.configurationValues" null ~ $ export IAM_ROLE_ARN_ADDON_VPC_CNI="arn:aws:iam::111111111111:role/eksctl-niikawa-karpenter-demo-addon-vpc-cni-Role1-1JbNyaRaA4kU" ~ $ aws eks create-addon \ > --cluster-name ${CLUSTER_NAME} \ > --addon-name vpc-cni \ > --addon-version v1.19.5-eksbuild.1 \ > --service-account-role-arn ${IAM_ROLE_ARN_ADDON_VPC_CNI} \ > --configuration-values '{ "env": { "ENABLE_POD_ENI": "true" } }' \ > --resolve-conflicts PRESERVE { "addon": { "addonName": "vpc-cni", "clusterName": "niikawa-karpenter-demo", "status": "CREATING", "addonVersion": "v1.19.5-eksbuild.1", "health": { "issues": [] }, "addonArn": "arn:aws:eks:us-west-2:111111111111:addon/niikawa-karpenter-demo/vpc-cni/8acc6b4b-a925-8e41-54af-7aca3651673f", "createdAt": "2025-08-23T11:01:38.125000+00:00", "modifiedAt": "2025-08-23T11:01:38.138000+00:00", "serviceAccountRoleArn": "arn:aws:iam::111111111111:role/eksctl-niikawa-karpenter-demo-addon-vpc-cni-Role1-1JbNyaRaA4kU", "tags": {}, "configurationValues": "{ \"env\": { \"ENABLE_POD_ENI\": \"true\" } }" } } ~ $ ~ $ aws eks describe-addon --cluster-name ${CLUSTER_NAME} --addon-name vpc-cni --query "addon.configurationValues" "{ \"env\": { \"ENABLE_POD_ENI\": \"true\" } }" ~ $ aws eks describe-addon --cluster-name ${CLUSTER_NAME} --addon-name vpc-cni --query "addon.status" "ACTIVE"
- コンソールで、Amazon VPC CNI plugin のアドオンが再導入され、ENABLE_POD_ENI が有効化されていることを確認します。
手順2:検証用のセキュリティグループを作成する
- 以下のコマンドを使用して、セキュリティグループを3つ作成します。最後に、サーバーPod用のSGのインバウンドルールを追加しています。
# サーバー用のSG export SG_BACKEND_ID=$(aws ec2 create-security-group \ --group-name pod-sg-backend \ --description "SG for backend pod" \ --vpc-id ${VPC_ID} \ --query 'GroupId' --output text) # 通信を許可するクライアント用のSG export SG_CLIENT_ALLOWED_ID=$(aws ec2 create-security-group \ --group-name pod-sg-client-allowed \ --description "SG for allowed client pod" \ --vpc-id ${VPC_ID} \ --query 'GroupId' --output text) # 通信を拒否するクライアント用のSG export SG_CLIENT_DENIED_ID=$(aws ec2 create-security-group \ --group-name pod-sg-client-denied \ --description "SG for denied client pod" \ --vpc-id ${VPC_ID} \ --query 'GroupId' --output text) echo "Backend SG ID: ${SG_BACKEND_ID}" echo "Allowed Client SG ID: ${SG_CLIENT_ALLOWED_ID}" echo "Denied Client SG ID: ${SG_CLIENT_DENIED_ID}" aws ec2 authorize-security-group-ingress \ --group-id ${SG_BACKEND_ID} \ --protocol tcp \ --port 80 \ --source-group ${SG_CLIENT_ALLOWED_ID}
- 以下、実行した際のログです。
~ $ echo $VPC_ID vpc-05d986ed2d9580b6a ~ $ # サーバー用のSG ~ $ export SG_BACKEND_ID=$(aws ec2 create-security-group \ > --group-name pod-sg-backend \ > --description "SG for backend pod" \ > --vpc-id ${VPC_ID} \ > --query 'GroupId' --output text) ~ $ # 通信を許可するクライアント用のSG ~ $ export SG_CLIENT_ALLOWED_ID=$(aws ec2 create-security-group \ > --group-name pod-sg-client-allowed \ > --description "SG for allowed client pod" \ > --vpc-id ${VPC_ID} \ > --query 'GroupId' --output text) ~ $ # 通信を拒否するクライアント用のSG ~ $ export SG_CLIENT_DENIED_ID=$(aws ec2 create-security-group \ > --group-name pod-sg-client-denied \ > --description "SG for denied client pod" \ > --vpc-id ${VPC_ID} \ > --query 'GroupId' --output text) ~ $ echo "Backend SG ID: ${SG_BACKEND_ID}" Backend SG ID: sg-099b80bf5fb81d4f4 ~ $ echo "Allowed Client SG ID: ${SG_CLIENT_ALLOWED_ID}" Allowed Client SG ID: sg-0ca32cc8f76daf2d8 ~ $ echo "Denied Client SG ID: ${SG_CLIENT_DENIED_ID}" Denied Client SG ID: sg-093b6907b5ff89f53 ~ $ ~ $ aws ec2 authorize-security-group-ingress \ > --group-id ${SG_BACKEND_ID} \ > --protocol tcp \ > --port 80 \ > --source-group ${SG_CLIENT_ALLOWED_ID} { "Return": true, "SecurityGroupRules": [ { "SecurityGroupRuleId": "sgr-0b8b08d713f6e2176", "GroupId": "sg-099b80bf5fb81d4f4", "GroupOwnerId": "111111111111", "IsEgress": false, "IpProtocol": "tcp", "FromPort": 80, "ToPort": 80, "ReferencedGroupInfo": { "GroupId": "sg-0ca32cc8f76daf2d8", "UserId": "111111111111" }, "SecurityGroupRuleArn": "arn:aws:ec2:us-west-2:111111111111:security-group-rule/sgr-0b8b08d713f6e2176" } ] } ~ $
- コンソールで、セキュリティグループが作成されていることを確認します。
手順3:SecurityGroupPolicyリソースを作成しPod とSG を関連付ける
- まずSecurityGroupPolicy を定義した以下のYAML を作成します。
- 以下に示すコードを使用します。(例:sgp-policies.yaml)
apiVersion: vpcresources.k8s.aws/v1beta1 kind: SecurityGroupPolicy metadata: name: backend-policy namespace: default spec: podSelector: matchLabels: app: backend securityGroups: groupIds: - ${SG_BACKEND_ID} --- apiVersion: vpcresources.k8s.aws/v1beta1 kind: SecurityGroupPolicy metadata: name: client-allowed-policy namespace: default spec: podSelector: matchLabels: app: client-allowed securityGroups: groupIds: - ${SG_CLIENT_ALLOWED_ID} --- apiVersion: vpcresources.k8s.aws/v1beta1 kind: SecurityGroupPolicy metadata: name: client-denied-policy namespace: default spec: podSelector: matchLabels: app: client-denied securityGroups: groupIds: - ${SG_CLIENT_DENIED_ID}
- 上記の内容でsgp-policies.yamlを作成するために、以下のコマンドを実行します。次に上記コードを貼り付け、続けて”EOF” を入力し、JSONファイルを作成します。
cat << EOF > sgp-policies.yaml
- 次に、Deployment、Service、Pod を定義した以下のYAML を作成します。
- 以下に示すコードを使用します。(例:pods-and-service.yaml )
- コードの内容を一部説明します。
- argsは、commandで指定されたコマンド(この場合は/bin/sh -c)に渡される引数を定義します。ここでは、Pod の起動後にネットワーク疎通テストを自動で実行し続けるための、一行のシェルスクリプトを渡しています。
apiVersion: v1 kind: Service metadata: name: backend-service namespace: default spec: selector: app: backend ports: - protocol: TCP port: 80 targetPort: 80 --- apiVersion: apps/v1 kind: Deployment metadata: name: backend-pod namespace: default spec: replicas: 1 selector: matchLabels: app: backend template: metadata: labels: app: backend # SecurityGroupPolicyが参照するラベル spec: containers: - name: nginx image: public.ecr.aws/nginx/nginx:latest ports: - containerPort: 80 --- apiVersion: v1 kind: Pod metadata: name: client-pod-allowed labels: app: client-allowed # SecurityGroupPolicyが参照するラベル spec: containers: - name: netshoot image: nicolaka/netshoot:latest command: ["/bin/sh", "-c"] args: - while true; do echo "--- Trying from ALLOWED pod ---"; curl -s -m 2 http://backend-service && echo ""; sleep 5; done restartPolicy: Never --- apiVersion: v1 kind: Pod metadata: name: client-pod-denied labels: app: client-denied # SecurityGroupPolicyが参照するラベル spec: containers: - name: netshoot image: nicolaka/netshoot:latest command: ["/bin/sh", "-c"] args: - while true; do echo "--- Trying from DENIED pod ---"; curl -s -m 2 http://backend-service || echo "Failed as expected."; sleep 5; done restartPolicy: Never
手順4:SecurityGroupPolicyリソース、Pod リソースをデプロイする
- まずSecurityGroupPolicyリソースをデプロイします。
~ $ kubectl apply -f sgp-policies.yaml securitygrouppolicy.vpcresources.k8s.aws/backend-policy created securitygrouppolicy.vpcresources.k8s.aws/client-allowed-policy created securitygrouppolicy.vpcresources.k8s.aws/client-denied-policy created
- 次に、 Deployment、Service、Pod をデプロイします。
~ $ kubectl apply -f pods-and-service.yaml service/backend-service created deployment.apps/backend-pod created pod/client-pod-allowed created pod/client-pod-denied created
- Pod が起動されたこと、Pod にセキュリティグループが割り当てられたことを確認します。
- kubectl describe pod コマンドの出力から、以下のEvents が確認出来ました。無事に、Pod 単位にセキュリティグループが割り当てられたようです。
- Pod will get the following Security Groups
~ $ kubectl get pods NAME READY STATUS RESTARTS AGE backend-pod-6dfcfdbb7c-jz6fq 1/1 Running 0 54s client-pod-allowed 1/1 Running 0 54s client-pod-denied 1/1 Running 0 54s ~ $ kubectl describe pod client-pod-allowed Name: client-pod-allowed Namespace: default Priority: 0 Service Account: default Node: ip-10-192-20-141.us-west-2.compute.internal/10.192.20.141 Start Time: Sat, 23 Aug 2025 12:05:36 +0000 Labels: app=client-allowed Annotations: vpc.amazonaws.com/pod-eni: [{"eniId":"eni-034536c070c150014","ifAddress":"02:fb:1b:9e:6e:9d","privateIp":"10.192.20.124","ipv6Addr":"","vlanId":2,"subnetCidr":"10.19... Status: Running IP: 10.192.20.124 IPs: IP: 10.192.20.124 Containers: netshoot: Container ID: containerd://b8f6d9952e6fc71a1810fbfa64e481391f30fdaa5f7badb07aa9849d7b5910a2 Image: nicolaka/netshoot:latest Image ID: docker.io/nicolaka/netshoot@sha256:7f08c4aff13ff61a35d30e30c5c1ea8396eac6ab4ce19fd02d5a4b3b5d0d09a2 Port: Host Port: Command: /bin/sh -c Args: while true; do echo "--- Trying from ALLOWED pod ---"; curl -s -m 2 http://backend-service && echo ""; sleep 5; done State: Running Started: Sat, 23 Aug 2025 12:05:38 +0000 Ready: True Restart Count: 0 Limits: vpc.amazonaws.com/pod-eni: 1 Requests: vpc.amazonaws.com/pod-eni: 1 Environment: Mounts: /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-cvw5t (ro) Conditions: Type Status PodReadyToStartContainers True Initialized True Ready True ContainersReady True PodScheduled True Volumes: kube-api-access-cvw5t: Type: Projected (a volume that contains injected data from multiple sources) TokenExpirationSeconds: 3607 ConfigMapName: kube-root-ca.crt Optional: false DownwardAPI: true QoS Class: BestEffort Node-Selectors: Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s node.kubernetes.io/unreachable:NoExecute op=Exists for 300s vpc.amazonaws.com/pod-eni:NoSchedule op=Exists Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 73s default-scheduler Successfully assigned default/client-pod-allowed to ip-10-192-20-141.us-west-2.compute.internal Normal SecurityGroupRequested 73s vpc-resource-controller Pod will get the following Security Groups [sg-0ca32cc8f76daf2d8] Warning FailedCreatePodSandBox 73s kubelet Failed to create pod sandbox: rpc error: code = Unknown desc = failed to setup network for sandbox "46092c80fb683b2d91e3bbdeca2707c14ad8e74d81cca3a5d24840028604974b": plugin type="aws-cni" name="aws-cni" failed (add): add cmd: failed to assign an IP address to container Normal ResourceAllocated 72s vpc-resource-controller Allocated [{"eniId":"eni-034536c070c150014","ifAddress":"02:fb:1b:9e:6e:9d","privateIp":"10.192.20.124","ipv6Addr":"","vlanId":2,"subnetCidr":"10.192.20.0/24","subnetV6Cidr":"","associationID":"trunk-assoc-098d3f3420292f2bc"}] to the pod Normal Pulling 72s kubelet Pulling image "nicolaka/netshoot:latest" Normal Pulled 71s kubelet Successfully pulled image "nicolaka/netshoot:latest" in 634ms (634ms including waiting). Image size: 207893848 bytes. Normal Created 71s kubelet Created container: netshoot Normal Started 71s kubelet Started container netshoot ~ $
手順5:動作確認(アクセス制御のテスト)
- kubectl exec コマンドでPod に接続し、オペレーションを行います。
- その後、Pod 内で、nslookup コマンドを実行すると、以下のようにタイムアウトエラーが発生しました。原因は、カスタムセキュリティグループを持つPod から、クラスタのDNSサーバー(CoreDNS)への通信が、ノードのセキュリティグループによってブロックされているためです。
~ $ kubectl exec -it client-pod-allowed -- /bin/sh ~ # hostname client-pod-allowed ~ # nslookup backend-service ;; communications error to 172.20.0.10#53: timed out ;; communications error to 172.20.0.10#53: timed out ;; communications error to 172.20.0.10#53: timed out ;; no servers could be reached ~ # ~ # curl -v http://backend-service * Could not resolve host: backend-service * shutting down connection #0 curl: (6) Could not resolve host: backend-service ~ # ~ # exit command terminated with exit code 6 ~ $
- この問題を解決するために、ノードのセキュリティグループにPod からのDNS に関する通信を許可する必要がありました。これは、coredns-pod が稼働しているノードのENIに設定されたセキュリティグループのインバウンドルールに許可される必要があるためです。
- まずノードのインスタンス名を確認します。
~ $ kubectl get pods -n kube-system -l k8s-app=kube-dns -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES coredns-7bf648ff5d-6knjr 1/1 Running 0 3h54m 10.192.21.212 ip-10-192-21-181.us-west-2.compute.internal ... coredns-7bf648ff5d-lp6j8 1/1 Running 0 3h54m 10.192.21.153 ip-10-192-21-181.us-west-2.compute.internal ...
- 次に、ノードのインスタンスに設定されているセキュリティグループを変更し、各クライアントPod のセキュリティグループからDNS 通信(TCP・UDPのポート53)を許可するインバウンドルールを追加します。
- 以下、セキュリティグループ変更後のコンソール画面です。
- セキュリティグループのルールを修正後、再度、kubectl exec コマンドでPod に接続し、名前解決を試みます。
~ $ kubectl exec -it client-pod-allowed -- /bin/sh ~ # hostname client-pod-allowed ~ # nslookup backend-service ;; Got recursion not available from 172.20.0.10 Server: 172.20.0.10 Address: 172.20.0.10#53 Name: backend-service.default.svc.cluster.local Address: 172.20.212.33 ;; Got recursion not available from 172.20.0.10 ~ # curl -v http://backend-service * Host backend-service:80 was resolved. * IPv6: (none) * IPv4: 172.20.212.33 * Trying 172.20.212.33:80... * Connected to backend-service (172.20.212.33) port 80 * using HTTP/1.x > GET / HTTP/1.1 > Host: backend-service > User-Agent: curl/8.14.1 > Accept: */* > * Request completely sent off < HTTP/1.1 200 OK < Server: nginx/1.29.1 < Date: Sat, 23 Aug 2025 12:19:13 GMT < Content-Type: text/html < Content-Length: 615 < Last-Modified: Wed, 13 Aug 2025 14:33:41 GMT < Connection: keep-alive < ETag: "689ca245-267" < Accept-Ranges: bytes < ** 省略 ** * Connection #0 to host backend-service left intact ~ # exit ~ $
- nslookup コマンドで、無事に名前解決ができ、curl で通信できることを確認できました。
- 最後に、kubectl logs コマンドで、本来の目的である許可されたクライアントPod (client-pod-allowed)のログを確認し通信が成功していること、拒否されたクライアントPod (client-pod-denied)のログを確認し通信がブロック(失敗)していることを確認します。
~ $ kubectl logs -f client-pod-allowed | more ... --- Trying from ALLOWED pod --- ** 一部省略 ** Welcome to nginx! If you see this page, the nginx web server is successfully installed and working. Further configuration is required.
~ $ kubectl logs -f client-pod-denied | more ..... --- Trying from DENIED pod --- Failed as expected.
まとめ
- 今回のEKSネットワークハンズオンでは、前半・後半を通じて、Kubernetes におけるネットワークの基本から、EKS の高度な機能を使った実践的なアクセス制御までを学びました。前半のハンズオンでは、ServiceとCoreDNS がどのように連携し、IPアドレスが変わりやすいPod に対して安定した通信を提供しているのか(サービスディスカバリー)を経験しました。これにより、EKSクラスタ内部の基本的なネットワークの流れを理解することができました。
- そして本記事の後半では、その基本を応用し、Amazon VPC CNI plugin が提供するカスタムネットワーキング機能を利用して、Pod 1つ1つにAWSのセキュリティグループを割り当てました。その結果、特定のPod からの通信のみを許可し、それ以外をブロックするというきめ細やかなPod 単位のアクセス制御を実現できました。
- EKS には、今回学んだ機能以外にも多くの機能があります。さらにEKS を理解、活用できるようにチャレンジしてみてください。
参考資料
Amazon Web Services ブログ
Amazon EKSベストプラクティスガイド
Amazon EKSユーザーガイド