catcwagent-configmap.yaml# create configmap for cwagent configapiVersion:v1data:# Configuration is in Json format. No matter what configure change you make,# please keep the Json blob valid.cwagentconfig.json:| {"logs":{"metrics_collected":{"kubernetes":{"cluster_name":"eks-work-cluster","metrics_collection_interval":60 } },"force_flush_interval":5 } }kind:ConfigMapmetadata:name:cwagentconfignamespace:amazon-cloudwatch
기존에 설정한 v1.2.2는 베타 버전이 들어가 있어 cluster-autoscaler 버전을 업그레이드 해봤는데 이미지가 풀이 되지 않는 문제가 계속되고 있다.
Cluster Autoscaler에서 주의할 점
Cluster Autoscaler는 requests 값으로 스케줄링을 판단한다. 파드를 배치할 때만 스케일링을 판단하지 파드가 배치되면 노드의 스케줄링은 실행되지 않는다. 3.7절에서 이야기했던 대로 requests와 limits의 값을 적절히 조절해 리소스 사용량을 적절하게 맞춰야 한다.
AWS 오토스케일링 기능을 이용한 예방적 오토스케일링
문제가 발생했을 때 오토스케일링 하는 것이 아니라 임계값을 넘으면 노드를 추가하는 것이 운영 측면에선 더 좋다.
생성되는 Pod를 Pending 상태로 만들지 않으려면 노드의 부하를 판단할 필요가 있는데 이 때 CPU 사용률(pod_cpu_reserved_capacity)가 좋을 것이다. 이는 Container Insights를 활성화하면 자동 등록된다.
4.3.2 Horizontal Pod Autoscaler를 이용한 파드 오토스케일링
노드 오토스케일링은 파드를 원하는 수만큼 동작시키기 위해 용량을 확보하는 기능이므로 애플리케이션 스케일링을 하려면 쿠버네티스의 Horizontal Pod Autoscaler 기능을 사용해 파드 리소스 사용률에 맞춰 파드의 스케일 아웃, 인 기능을 구현할 수 있다.
HPA로 파드 수를 자동 조절하려면 클러스터 내부에서 파드 리소스를 파악하고 있어야 한다. HPA에서는 메트릭 서버슬 사용해 사용 현황을 파악한다.
kubectl은 인증 설정에서 AWS CLI를 사용하도록 설정하면 EKS 인증 시 해당 도구의 aws eks get-token 명령을 사용해 토큰을 생성하고 그것을 클러스터 인증 문자열로 사용한다. 토큰은 Go용 AWS SDK의 sts.GetCallerIdentityRequest에서 생성된 URL을 base64 인코딩하고 k8s-aws-v1 접두사를 붙인 문자열이다.
kubectl은 이 문자열을 사용해 EKS의 컨트롤 플레인에 요청을 보낸다. EKS는 받은 토큰을 디코딩하고 sts.GetCallerIdentity 요청을 발행해 응답에 포함된 IAM 사용자나 IAM 역할의 ARN을 확인한다.
인가
kubectl로 접속한 요청이 정상적으로 인증된 후에는 요청한 조작이 허가를 받은지 점검할 필요가 있다. 쿠버네티스에서는 Role-Based Access Control 기능을 이용해 구현한다. Role, RoleBinding이라는 쿠버네티스 오브젝트를 사용해 설정한다. 롤은 어떤 조작을 허가할지 권한 셋을 정의하고 롤바인딩은 어떤 사용자에게 어떤 롤을 부여할지 정의한다.
AWS는 aws-auth 컨피그맵 안에 AWS CLI 인증 정보로 설정한 IAM 사용자, IAM 역할의 아마존 리소스 이름, 쿠버네티스 오브젝트로 그룹을 연결해놓고 롤바인딩으로 그 그룹과 롤을 연결시키는 것으로 구현한다.
이런 방법을 통해 각 관리자가 특정 리소스에만 접근 가능하도록 권한을 부여할 수 있다.
애플리케이션 개발자로 리소스 접속이 제한된 사용자를 예를 들어서 특정 네임스페이스의 파드와 파드 로그, 디플로이먼트와 레플리카셋, 서비스만 참조 가능한 사용자를 등록해보자.
kubectlgetpod,deployments,replicaset,service Error from server (Forbidden): pods is forbidden: User "rbacTest" cannot list resource "pods" in API group "" in the namespace "eks-work"
Error from server (Forbidden): deployments.apps is forbidden: User "rbacTest" cannot list resource "deployments" in API group "apps" in the namespace "eks-work"
Error from server (Forbidden): replicasets.apps is forbidden: User "rbacTest" cannot list resource "replicasets" in API group "apps" in the namespace "eks-work"
Error from server (Forbidden): services is forbidden: User "rbacTest" cannot list resource "services" in API group "" in the namespace "eks-work"
kubectlcreatedeploymenttnginx-test--image=nginx:latest error: failed to create deployment: deployments.apps is forbidden: User "rbacTest" cannot create resource "deployments" in API group "apps" in the namespace "eks-work"
rbac-test-ns에도 추가 불가
kubectlcreatedeploymenttnginx-test--image=nginx:latest-nrbac-test-ns error: failed to create deployment: deployments.apps is forbidden: User "rbacTest" cannot create resource "deployments" in API group "apps" in the namespace "rbac-test-ns"
4.4.2 노드 보안
파드에 부여하는 호스트 권한 제어
클러스터를 관리하면서 어떤 파드든 무조건 생성해도 되는 것은 아니다. 쿠버네티스는 파드에 부여할 권한을 관리하는 파드 시큐리티 폴리시라는 기능을 가지고 있다. 현재 EKS 클러스터에는 아무 제한이 걸려있지 않다. 파드를 특권 컨테이너로 이동시켜 노드 쪽 권한을 사용할 수도 있다.
kubectlapply-f-<<EOFheredoc> apiVersion: v1heredoc>heredoc> kind: Podheredoc> metadata: name: nginxspec: containers: - name: nginx image: nginx:latestheredoc> EOFError from server (Forbidden): error when creating "STDIN": pods "nginx" is forbidden: violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
여기서 nginx 컨테이너를 실행하려하면 권한에 막힌다.
과거에는 Pod Security Policy로 제어했지만 지금 해당 feature는 Deprecated되었고, Pod Security Admission 혹은 3rd party plugin으로 대체할 수 있다.
과거의 파드 시큐리티 폴리시를 사용해 파드에서 사용할 수 있는 권한을 제어함으로써 노드를 보호할 수 있었다. 만약 특정 사용자에게만 권한제한을 적용하고 싶을 경우 파드 시큐리티 폴리시를 생성해 해당 파드 시큐리티 폴리시를 룰로 선언하고 롤바인딩으로 롤과 사용자를 매핑시키면 파드 시큐리티 폴리시를 적용할 수 있었다.
4.4.3 파드 보안
컨테이너 라이프사이클에 맞춘 보안 대책의 필요성
컨테이너 보안은 라이프사이클에 맞춰 수행하며 컨테이너 이미지를 생성할 때는 취약성을 점검하고 동작시킬 때는 동작을 감시한다.
컨테이너 이미지 취약성: 컨테이너 이미지를 스캔해 문제가 있는 소프트웨어 버전이 포함되었는지 자동으로 확인하게함으로써 취약한 컨테이너가 배포되는 것을 방지한다.
동작 감시(런타임 보호): 실제 컨테이너에 문제되는 동작이 실행되는지 확인한다.
4.4.4 네트워크 보안
네트워크 수준의 보안은 인그레스와 내부통신 두 가지 관점으로 생각할 수 있다.
인그레스는 AWS 기능을 이용, 내부통신은 쿠버네티스 기능을 사용해 대응한다.
엔드포인트 IP 주소 제한
EKS에서는 엔드포인트의 IP 주소 제한 기능이 있으며 콘솔에서 설정할 수 있다.
kubectl을 VPC 내부로 제한하는 프라이빗 엔드포인트
기본적으로 EKS 클러스터 엔드포인트는 인터넷에 공개되는데 IAM으로 인증하고 앞에서 설명한 IP 주소로 접속도 제한할 수 있다.
프라이빗 엔드포인트도 제공해 VPC 내부에서만 접속할 수 있는 클러스터를 생성할 수도 있다.
네트워크 정책을 사용한 클러스터 내부 통신 제어
파드에는 VPC 내부의 IP 주소가 할당돼 클러스터 외부와 통신할 수 있지만 어떤 노드에 어떤 IP 주소가 할당되는지 제어할 수 없다. 또, 모든 노드에 공통 보안 그룹이 설정돼있어 클러스터 내부 통신을 보안 그룹으로 제어하는 것은 어렵다.
클러스터 내부 서비스 사이의 통신 제어는 네트워크 정책이라는 클러스터 내부 구조를 이용해 실현한다.
매니페스트를 통해 살펴보면 대상 네임스페이스 내에서는 모든 서비스 사이의 통신을 거부한다.
catnetwork-policy-all-deny.yamlapiVersion:networking.k8s.io/v1kind:NetworkPolicymetadata:name:default-denyspec:podSelector:# 모든 파드(Pod)에 적용matchLabels:{}
이후 서비스 B에서 http 요청만 허가하는 네트워크 정책을 생성한다.
.spec.podSelector.matchLabels에서 어떤 파드에 네트워크 정책을 적용할지 설정한다.
여기선 app: serviceA에 .spec.ingress에는 전달받는 통신의 허가 조건을 설정한다. 이렇게 두 설정으로 그림과 같은 구성을 실현할 수 있다.
catnetwork-policy-allow-http-from-serviceB2ServiceA.yamlapiVersion:networking.k8s.io/v1kind:NetworkPolicymetadata:# 적용 대상 네임스페이스 설정name:network-policy-for-serviceaspec:podSelector:matchLabels:# 적용 대상 파드 레이블 설정app:ServiceApolicyTypes:-Ingressingress:# 수신 규칙 설정-from:-podSelector:matchLabels:app:ServiceBports:-protocol:TCPport:80
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/calico-vxlan.yaml
poddisruptionbudget.policy/calico-kube-controllers created
serviceaccount/calico-kube-controllers created
serviceaccount/calico-node created
serviceaccount/calico-cni-plugin created
configmap/calico-config created
customresourcedefinition.apiextensions.k8s.io/bgpconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/bgpfilters.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/bgppeers.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/blockaffinities.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/caliconodestatuses.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/clusterinformations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/felixconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/globalnetworkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/globalnetworksets.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/hostendpoints.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamblocks.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamconfigs.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipamhandles.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ippools.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/ipreservations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/kubecontrollersconfigurations.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networkpolicies.crd.projectcalico.org created
customresourcedefinition.apiextensions.k8s.io/networksets.crd.projectcalico.org created
clusterrole.rbac.authorization.k8s.io/calico-kube-controllers created
clusterrole.rbac.authorization.k8s.io/calico-node created
clusterrole.rbac.authorization.k8s.io/calico-cni-plugin created
clusterrolebinding.rbac.authorization.k8s.io/calico-kube-controllers created
clusterrolebinding.rbac.authorization.k8s.io/calico-node created
clusterrolebinding.rbac.authorization.k8s.io/calico-cni-plugin created
daemonset.apps/calico-node created
deployment.apps/calico-kube-controllers created
ECR에 새 도커 이미지를 푸시하고, EKS 클러스터에 매니페스트 적용, 새로운 도커 이미지를 ECR에서 풀해서 배포한다.
우선 kustomization.yaml의 이미지 태그와 ECR URI를 변경한다.
catcicd/kustomization/prod/kustomization.yamlapiVersion:kustomize.config.k8s.io/v1beta1kind:Kustomizationbases:-../base# 기반이 되는 매니페스트 장소images:-name:backend-app-image# 여기는 바꾸지 않음newTag:1.0.1# 애플리케이션 버전 번호newName:566620743708.dkr.ecr.ap-northeast-2.amazonaws.com/k8sbook/backend-app# ECR 레지스트리 URI
[Container] 2024/06/15 01:38:26.869089 Running command python get-pip.pyERROR: This script does not work on Python 3.7 The minimum supported Python version is 3.8. Please use https://bootstrap.pypa.io/pip/3.7/get-pip.py instead.
[Container] 2024/06/15 01:38:26.993854 Command did not exit successfully python get-pip.py exit status 1[Container] 2024/06/15 01:38:26.997881 Phase complete: PRE_BUILD State: FAILED[Container] 2024/06/15 01:38:26.997900 Phase context status code: COMMAND_EXECUTION_ERROR Message: Error while executing command: python get-pip.py. Reason: exit status 1
환경에 따라 다른 정보들을 동적으로 변경시키기 위해서 kustomize는 동적으로 값을 변경하고 싶다는 요구사항을 해결하는 구조를 제공한다.
kustomize는 kubectl의 서브 명령어로 여러 개의 매니페스트나 환경에 의존적인 설정값을 합해 매니페스트 하나로 만들어 준다.
디렉토리 각각의 kustomization.yaml에는 기본 매니페스트가 어디에 있는지 기본 설정을 작성한다. 이렇게 하면 test 디렉토리의 kustomization.yaml와 deployment.yaml은 기본 파일을 덮어쓰며 생성할 리소스는 -test 접미사를 붙인다.
catcicd/kustomization/test/kustomization.yamlapiVersion:kustomize.config.k8s.io/v1beta1kind:Kustomizationbases:-../base# 기본이 되는 매니페스트 디렉토리patches:-deployment.yaml# deployment.yaml의 내용을 덮어쓰기하는 설정nameSuffix:-test# 모든 리소스 이름 끝에 -test를 붙인다commonLabels:# 모든 리소스에 설정하는 레이블app:backend-app-testimages:-name:centosnewTag:# 애플리케이션 버전 번호newName:# ECR 레지스트리 URI
그리고 덮어쓰기하는 deployment.yaml에는 덮어쓰기 대상의 디플로이먼트 이름을 설정하고 레플리카셋 수만 적어둔다.
catcicd/kustomization/test/deployment.yamlapiVersion:apps/v1kind:Deploymentmetadata:name:backend-app# 덮어쓰기 대상 리소스 이름spec:replicas:1# 이 부분만 base 매니페스트 덮어쓰기
이 구조를 파이프라인에 도입하면 환경에 따라 변경되는 값들을 동적으로 변경하는 파이프라인을 구성할 수 있다.
4.6 버전 관리
쿠버네티스 버전 업데이트에 필요한 작업과 방법, 고려사항을 설명한다.
4.6.1 쿠버네티스 버전 업데이트 계획과 지원 정책
쿠버네티스는 연간 4번 새로운 버전이 출시되며 EKS도 최신버전을 포함해 3개의 버전을 서포트한다. 하나의 버전의 수명은 9개월 정도라고 보면 된다.
4.6.2 버전 업데이트에 필요한 작업
쿠버네티스는 컨트롤 플레인과 데이터 플레인을 각각 업데이트 해야 되는데 EKS에서 eksctl 관련 명령을 사용하면 효율적으로 실행할 수 있다.
eksctlupgradecluster--nameeks-work-cluster--approve2024-06-1620:34:34 [ℹ] will upgrade cluster "eks-work-cluster" control plane from current version "1.29" to "1.30"2024-06-1620:43:10 [✔] cluster "eks-work-cluster" control plane has been upgraded to version "1.30"2024-06-1620:43:10 [ℹ] you will need to follow the upgrade procedure for all of nodegroups and add-ons2024-06-1620:43:10 [ℹ] re-building cluster stack "eksctl-eks-work-cluster-cluster"2024-06-1620:43:10 [✔] all resources in cluster stack "eksctl-eks-work-cluster-cluster" are up-to-date2024-06-1620:43:10 [ℹ] checking security group configuration for all nodegroups2024-06-1620:43:10 [ℹ] all nodegroups have up-to-date cloudformation templates
데이터플레인에서 동작하는 시스템 컴포넌트 파드인 kube-proxy와 coredns를 업데이트한다.
eksctlutilsupdate-kube-proxy--clustereks-work-cluster--approve2024-06-1620:43:34 [ℹ] "kube-proxy" is now up-to-dateeksctlutilsupdate-coredns--clustereks-work-cluster--approve2024-06-1620:43:41 [ℹ] replaced "kube-system:Service/kube-dns"2024-06-1620:43:41 [ℹ] replaced "kube-system:ServiceAccount/coredns"2024-06-1620:43:41 [ℹ] replaced "kube-system:ConfigMap/coredns"2024-06-1620:43:41 [ℹ] replaced "kube-system:Deployment.apps/coredns"2024-06-1620:43:41 [ℹ] replaced "ClusterRole.rbac.authorization.k8s.io/system:coredns"2024-06-1620:43:42 [ℹ] replaced "ClusterRoleBinding.rbac.authorization.k8s.io/system:coredns"2024-06-1620:43:42 [ℹ] "coredns" is now up-to-date
계속해서 데이터 플레인 업데이트를 실시한다.
데이터 플레인 업데이트
데이터플레인은 실제 서비스가 동작하고 있기 때문에 주의해야 한다. 새로운 노드 그룹으로 새로운 버전의 데이터 플레인을 생성하고 기존 데이터 플레인에서 전환하는 방법을 사용할 수 있다.
# 현재 노드 그룹 설정 확인
eksctl get nodegroups --cluster=eks-work-cluster
eksctl create nodegroup --cluster eks-work-cluster --version 1.29 --name eks-work-nogroup-2 --node-type t2.small --nodes 2 --nodes-min 2 --nodes-max 5 --node-ami auto
모든 노드가 Ready상태가 되면 다음 명령으로 기존 노드 그룹을 삭제한다. 기존 노드 그룹을 삭제하면 파드가 자동으로 새로운 노드에 재배치된다.