AEWS Study #2 – EKS Networking

AEWS 2회차는 EKS Networking에 대한 내용을 다룬다.
지난 kOps Study 때 학습, 실습한 내용들과 중복되는 내용들이 꽤 많았어서 이해하기가 수월했다. (물론 실습은 별개의 난이도지만…ㅎㅎ)
이번 실습부터는 가시다님이 제공해주시는 one click 배포 스크립트를 활용해서 진행할 예정이다.

0. 환경 구성

앞서 밝힌 것과 같이 이번 실습부터는 가시다님이 제공해주신 One Click 배포 스크립트를 활용해서 환경 구성을 진행한다.
아래 스크립트를 참고하면 여러분들도 충분히 배포할 수 있다. 물론 배포 이전에 keypair는 생성해둬야 한다.

# YAML 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/eks-oneclick.yaml

# CloudFormation 스택 배포
aws cloudformation deploy --template-file eks-oneclick.yaml --stack-name myeks --parameter-overrides KeyName=<My SSH Keyname> SgIngressSshCidr=<My Home Public IP Address>/32 MyIamUserAccessKeyID=<IAM User의 액세스키> MyIamUserSecretAccessKey=<IAM User의 시크릿 키> ClusterBaseName='<eks 이름>' --region ap-northeast-2

Cloudformation이 배포되는 시간 동안 해당 yaml 파일이 어떤 내용을 담고있는지 확인해보도록 한다.
1개의 VPC, 각각 3개의 Public/Private Subnet, IGW의 네트워크 자원이 생성된다.
그리고 EKS Cluster가 생성되고 밑에 Node Group 1개와 3개의 Worker Node가 생성된다. Control Plane은 지난 회차에서 설명한 것처럼 별도로 EC2로 생성되지 않기 때문에 Worker Node 3개만 생성 된다. 거기에 추가로 작업을 위한 용도로 Bastion EC2가 생성된다.
Bastion EC2에는 kubectl, helm, eksctl, awscli, krew 등이 설치된다.
일정 시간이 지나면 아래와 같이 Cloudformation 배포가 완료되고 Node 3개가 올라온 것도 확인할 수 있다.

문제 없이 배포가 완료되었다면 아래 스크립트를 참고해서 Bastion EC2에 SSH 접속을 하고 설치 정보를 확인한다.

# CloudFormation 스택 배포 완료 후 작업용 EC2 IP 출력
aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text

# 마스터노드 SSH 접속
ssh -i ~/.ssh/<My SSH Keyname>.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)

1. AWS VPC CNI?

k8s Network에 대한 기본적인 내용은 지난 스터디였던 kOps 스터디 때의 Network 내용과 유사한 점이 많아 kOps 스터디 때 정리했던 내용으로 대체한다.

https://bs-yang.com/42

2. Pod 생성 수량 변경 (진행 중)

kOps에서도 Pod 수량 변경은 진행했었으나 EKS에서 한 번 더 진행해보도록 한다.
우선 Pod 최대 수량 계산식을 사용해 사용 가능 수량을 계산한다.
Worker Node의 EC2 Instance Type 별로 Pods 생성 수량은 제한 된다. Pods 생성 수량 기준은 인스턴스에 연결 가능한 ENI 숫자와 할당 가능한 IP 수에 따라 결정되게 된다.
aws-node, kube-proxy Pods는 Host의 IP을 같이 사용하기 때문에 배포 가능 최대 수량에서 제외 된다. (IP 조건에서 제외되기 때문에)
Pods 최대 생성 수량 계산 : {Instance에 연결 가능한 최대 ENI 수량 x (ENI에 할당 가능한 IP 수량 – 1)} + 2
위 식을 계산하기 위해서는 내가 사용하는 Node Instance Type의 ENI 및 IP 할당 제한을 확인할 필요가 있는데 그럴 때는 아래와 같이 확인 할 수 있다.
Values=t3.* 부분을 확인하고 싶은 Instance Type으로 변경하면 된다.

aws ec2 describe-instance-types --filters Name=instance-type,Values=t3.* \
 --query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
 --output table

내가 사용하는 Instance Type은 t3.medium이고 해당 Instance의 경우 연결 가능한 ENI는 3개이고 ENI당 할당 가능한 IP는 6개이다. 위 계산식에 대입하면 {3 x (6-1)} + 2가 되고 aws-node, kube-proxy을 제외하면 총 15개의 Pods를 생성할 수 있게 된다.
Pods 생성 수량(Replicas)을 늘리면서 Worker Node의 ENI 정보와 IP 주소에 어떤 변화가 있는지 확인해보겠다.
우선 어떠한 Pods도 배포하지 않았을 때 Worker Node 1의 ENI/IP 정보와 pods 정보 화면이다.
eth0으로 192.168.1.x/24만 표현되는 것을 알 수 있다.

테스트용 Pod을 배포했다. 해당 배포는 Pod Replicas가 2개로 설정되어 있다.
Worker Node 1에 eth1이 새로 생기고 eni가 1개 더 추가된 것을 확인할 수 있다. 그리고 Pod 정보에 2개의 Pod가 각각 Worker Node에서 사용하는 IP 대역을 활용하여 배포된 것을 볼 수 있다.

여기서 Replicas를 8개로 추가해보았다. Worker Node 1에 eni가 2개 더 추가됐고 pod 정보에 8개의 Pod가 올라온 것을 확인할 수 있다. 여기서는 Replicas가 8개라 기존 2개일 때는 Worker Node 1, 2에 각각 1개씩 Pod가 배포됐지만 지금은 Worker Node 1~3에 골고루 배포가 된 것을 알 수 있다.

30개 까지로 올렸을 때도 문제 없이 Pod가 잘 올라오는 것을 확인할 수 있다. 그리고 eth2가 추가된 것도 확인할 수 있었다.

여기서 Replicas를 50개로 늘려보았다. 7개의 Pod가 배포되지 않고 Pending 상태로 머물러 있는 것을 확인할 수 있었다. 배포 되지 않은 오류 원인을 확인해보면 Too many Pods라고 나온다. 위에서 설명한 EC2에서 받아들일 수 있는 최대 Pod 수를 초과했기 때문이다.

여기서 의문이 생긴다. 위에서 계산한 공식에 따르면 1대의 EC2 Worker Node에서는 17개의 Pod 생성이 가능하다. 그런데 왜 43개밖에 생성이 되지 않았을까? 15*3은 45개이기 때문에 45개가 배포되어야 하는데 말이다.
이 의문을 확인하기 위해 Worker Node에서 내가 방금 배포한 Pod 말고 사용 중인 Pod가 있는지 확인해보도록 한다.
kube-proxy, aws-node이외에도 coredns를 2개의 Node에서 사용하고 있는 것을 확인할 수 있다. 그렇기 때문에 총 45-2=43개의 Pod가 배포 된 것이다.

그럼 이 제한 수량을 초과하는 Pod는 배포할 수 없을까? 아니다 배포가 가능하다.
Prefix Delegation 방식을 사용해서 MAX IP 제한 수를 해제할 예정이다.

현재 설정이 어떻게 되어있는지 확인하기 위해 aws-node의 정보를 yaml로 출력해 spec.containers.env를 확인해봤다. ENABLE_PREFIX_DELEGATION 항목이 false로 되어있는 것을 확인할 수 있다. 이 부분을 true로 바꿔주면 PREFIX DELEGATION을 활성화할 수 있다.

ENABLE_PREFIX_DELEGATION 활성화를 위해 kubectl set env 명령어를 사용해서 true로 바꿔주고 rollout을 해준다. 그 뒤에 다시 aws-node containers 값을 확인했을 때 true로 변경된 것을 확인할 수 있었다.

kubectl set env daemonset aws-node -n kube-system ENABLE_PREFIX_DELEGATION=true
kubectl rollout restart ds aws-node -n kube-system

ENABLE_PREFIX_DELEGATION이 true로 잘 설정됐다면 IPv4 Prefix 정보가 /28로 나뉘어져 있는 2개의 Prefix 있는 것을 확인 할 수 있다.

여기까지 진행하면 Pod 배포 수량 제한이 해제되어야 하는데 해제가 되지 않는 상황이다. 여기저기 확인해봐도 확인 방법을 알 수가 없어서 이 부분은 차후에 다시 진행할 예정이다.

3. AWS LoadBalancer Controller

Kubernetes의 Network Service에는 ClusterIP, NodePort, Loadbalancer Type이 있다.

ClusterIP : Control Plane의 iptables을 이용하여 내부에서만 접근 가능한 방식으로 외부에서의 접근은 불가능하다.

NodePort : 위 ClusterIP는 외부에서 접근이 불가능하기 때문에 외부에서 접근 가능하게 하기 위해서 Worker Node의 Port을 맵핑하여 통신할 수 있도록 한다.

Loadbalancer : NodePort 방식과 마찬가지로 외부에서 접근할 수 있는 방식이다. 다만, Worker Node의 Port을 통해 접근하는 방식이 아닌 앞단에 위치한 LoadBalancer을 통해 접근하게 된다.

Loadbalancer Controller : Public Cloud을 사용할 경우 각 CSP에서 제공하는 Loadbalancer을 사용하기 위해 설치하는 Controller이다. 이를 통해 CSP의 LB을 제어할 수 있게 된다.

AWS Loadbalancer Controller을 사용하기 위해 AWS LB Controller 배포를 진행한다.

# OIDC 확인
aws eks describe-cluster --name $CLUSTER_NAME --query "cluster.identity.oidc.issuer" --output text
aws iam list-open-id-connect-providers | jq

# IAM Policy (AWSLoadBalancerControllerIAMPolicy) 생성
curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.4.7/docs/install/iam_policy.json
aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json

# 생성된 IAM Policy Arn 확인
aws iam list-policies --scope Local
aws iam get-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy
aws iam get-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --query 'Policy.Arn'

# AWS Load Balancer Controller를 위한 ServiceAccount를 생성 >> 자동으로 매칭되는 IAM Role 을 CloudFormation 으로 생성됨!
# IAM 역할 생성. AWS Load Balancer Controller의 kube-system 네임스페이스에 aws-load-balancer-controller라는 Kubernetes 서비스 계정을 생성하고 IAM 역할의 이름으로 Kubernetes 서비스 계정에 주석을 답니다
eksctl create iamserviceaccount --cluster=$CLUSTER_NAME --namespace=kube-system --name=aws-load-balancer-controller \
--attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --override-existing-serviceaccounts --approve

## IRSA 정보 확인
eksctl get iamserviceaccount --cluster $CLUSTER_NAME

## 서비스 어카운트 확인
kubectl get serviceaccounts -n kube-system aws-load-balancer-controller -o yaml | yh

# Helm Chart 설치
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME \
  --set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller

## 설치 확인
kubectl get crd
kubectl get deployment -n kube-system aws-load-balancer-controller
kubectl describe deploy -n kube-system aws-load-balancer-controller
kubectl describe deploy -n kube-system aws-load-balancer-controller | grep 'Service Account'
  Service Account:  aws-load-balancer-controller

Loadbalancer Service Account가 제대로 생성된 것을 확인할 수 있다. 이후 nlb 테스트 용 pod을 배포해서 부하분산이 되는지 테스트를 진행해본다.

# 모니터링
watch -d kubectl get pod,svc,ep

# 작업용 EC2 - 디플로이먼트 & 서비스 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/2/echo-service-nlb.yaml
cat echo-service-nlb.yaml | yh
kubectl apply -f echo-service-nlb.yaml

# 확인
kubectl get deploy,pod
kubectl get svc,ep,ingressclassparams,targetgroupbindings
kubectl get targetgroupbindings -o json | jq

# AWS ELB(NLB) 정보 확인
aws elbv2 describe-load-balancers | jq
aws elbv2 describe-load-balancers --query 'LoadBalancers[*].State.Code' --output text

# 웹 접속 주소 확인
kubectl get svc svc-nlb-ip-type -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "Pod Web URL = http://"$1 }'

# 파드 로깅 모니터링
kubectl logs -l app=deploy-websrv -f

# 분산 접속 확인
NLB=$(kubectl get svc svc-nlb-ip-type -o jsonpath={.status.loadBalancer.ingress[0].hostname})
curl -s $NLB
for i in {1..100}; do curl -s $NLB | grep Hostname ; done | sort | uniq -c | sort -nr
  52 Hostname: deploy-echo-55456fc798-2w65p
  48 Hostname: deploy-echo-55456fc798-cxl7z

# 지속적인 접속 시도 : 아래 상세 동작 확인 시 유용(패킷 덤프 등)
while true; do curl -s --connect-timeout 1 $NLB | egrep 'Hostname|client_address'; echo "----------" ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; done

부하분산 테스트를 진행해보니 54:46으로 비교적 정교하게 부하분산이 된 것을 확인할 수 있었다.

Replicas를 3개로 변경한 뒤 테스트를 진행해보니 완전 균등은 아니여도 적당하게 부하분산이 되는 것을 알 수 있다. 아마 보다 많은 트래픽을 넣다보면 좀 더 정교하게 부하분산이 되지 않을까 생각 된다.

4. Ingress

Ingress는 위에 Network Service에서 소개 한 ClusterIP, NodePort, LB을 HTTP/HTTPS 방식으로 외부에 노출하는 Web Proxy 역할을 말한다.

위 3번에서 설치한 Load Balancer Controller와 ALB의 조합으로 실습을 진행할 수 있다.
예전 스터디 때부터 자주 쓰던 배포 샘플인 game-2048을 이용해서 배포를 진행한다.

# 게임 파드와 Service, Ingress 배포
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/ingress1.yaml
cat ingress1.yaml | yh
kubectl apply -f ingress1.yaml

# 모니터링
watch -d kubectl get pod,ingress,svc,ep -n game-2048

# 생성 확인
kubectl get-all -n game-2048
kubectl get ingress,svc,ep,pod -n game-2048
kubectl get targetgroupbindings -n game-2048
NAME                               SERVICE-NAME   SERVICE-PORT   TARGET-TYPE   AGE
k8s-game2048-service2-e48050abac   service-2048   80             ip            87s

# Ingress 확인
kubectl describe ingress -n game-2048 ingress-2048

# 게임 접속 : ALB 주소로 웹 접속
kubectl get ingress -n game-2048 ingress-2048 -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "Game URL = http://"$1 }'

# 파드 IP 확인
kubectl get pod -n game-2048 -owide

배포가 잘 된 것을 확인했으니 ALB URL을 확보하여 테스트를 진행해본다.
이전 스터디 때 익숙하게 봐왔던 화면이 보이는 것을 확인할 수 있다. Ingress는 결국 어려운 개념이 아니라 뒷단에 있는 pod들의 서비스를 ALB에서 HTTP/HTTPS로 사용자에게 보여주는 방식이라고 보면 될 것 같다.

5. 정리

이번 실습 중에서 Pod 제한을 해제하는 건 현재 해결하지 못했다.
뭔가 어떤 부분에서 틀어막혀 있는 것 같은데… 이 부분은 여기저기 좀 물어봐서 해결을 해야할 것 같다. 그리고 External DNS의 경우에는 현재 도메인을 관리하는 Route53이 다른 Account에 있기 때문에 이 R53을 현재 실습 중인 Account에서 컨트롤 할 수 있는 방안을 찾아서 다음 실습에 배포되는 서비스에 적용하여 사용할 예정이다.

AEWS Study #1 – Amzaon EKS 설치 및 기본 사용

PKOS Study가 끝나고 프로젝트와 개인적인 일이 바빠 다음 스터디는 조금 뒤로 미루어야지 하고 생각했었다. 하지만 EKS라는 매력적인 스터디를 넘어갈 수 없어…이번에도 신청했고 감사하게도 참여할 수 있게 됐다.
AEWS 는 AWS EKS Workshop Study의 약자이다. 앞으로 EKS를 기초부터 고급 활용까지 다양하게 배우고 또 그 내용을 정리할 예정이다. 그리고 내 블로그를 티스토리에서 워드프레스로 옮기고 첫 스터디이니 더 잘해볼 생각이다.

0. AWS EKS?

AWS EKS는 Kubernetes를 쉽게 실행할 수 있는 관리형 서비스로 AWS 환경에서 k8s Control Plane, Node 등을 직접 설치할 필요 없이 사용하게끔 해주는 서비스입니다.
https://docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html

위 그림을 보면 EKS Control Plane이 있고 각 Worker Nodes는 VPC 내에 배포되는 것을 알 수 있다. 그리고 Control Plane은 하나의 AZ에만 배포되는 것이 아닌 여러 AZ에 배포되어 가용성도 확보되어있다.
ECR을 포함해서 AWS의 여러 기존 서비스와 통합해서 사용할 수 있기 때문에 AWS 종속성이 강한 인프라에서 사용하기 좋은 서비스라고 생각 된다.

위 그림은 앞선 그림보다 조금 더 상세하게 표현이 되어있는 그림이다. Control Plane은 AWS Managed VPC에 배포가 되어있는데 여기에는 API Server와 etcd가 있다.
여기서 etcd는 k8s Control Plane 컴포넌트에서 필요한 정보를 저장하고 사용자가 정의한 구성과 리소스 정보를 저장하는 k8s의 중요 구성 요소이다. EKS는 완전관리형 서비스이기 때문에 EKS CP에 속하는 etcd 또한 직접적인 관리가 필요하지 않고 EKS에서 자동 프리비저닝해 사용할 수 있다.

Node Group은 완전 관리형 노드그룹과 셀프형 노드그룹 그리고 서버리스인 AWS Fargate로 나뉜다
완전 관리형 노드그룹 : AWS Managed AMI 사용
셀프형 노드그룹 : Custom AMI 사용 가능, ASG/OS 관리 등을 직접 해야 함
서버리스 Fargate 노드그룹 : EC2 관리 요소 없음. 제공되는 Micro VM 사용

1. AWS EKS 구축

EKS에 대해 간단하게 알아봤고 이제 본격적으로 구축을 해보려고 한다.
EKS를 구축하는 방법에는 여러가지가 있다. AWS 콘솔에서, eksctl에서도 가능하고 Terraform이나 Cloudformation 등으로도 물론 가능하다.
나는 이번 구축은 스터디에서 제공한 yaml 파일을 사용해 Host EC2를 Cloudformation을 이용해 배포하고 해당 Host EC2에서 eksctl을 이용해 EKS을 배포를 할 예정이다.
배포 이전에 사전 준비해야 하는 내용은 많지 않다. ec2 연결에 사용할 Keypair와 SG에 지정할 접속지(보통은 집, 사무실) 공인 IP 주소이다. 2가지를 사전에 준비해놨고 아래 방법으로 배포를 진행했다.

# yaml 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/myeks-1week.yaml

# 배포
aws cloudformation deploy --template-file ~/Downloads/myeks-1week.yaml --stack-name myeks --parameter-overrides KeyName=aewspair SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2

위에서 사용한 yaml 파일은 각각 2개의 Public Subnet, Private Subnet을 갖고 있으며 NAT GW는 없고 Internet GW가 생성된다.
Host EC2에는 user-data로 kubectl, helm 등이 설치 된다.

Host EC2가 배포가 완료됐다면 접속해서 제대로 user-data가 진행됐는지 확인해본다.

# (옵션) cloud-init 실행 과정 로그 확인
sudo tail -f /var/log/cloud-init-output.log

# 사용자 확인
sudo su -
whoami

# 기본 툴 및 SSH 키 설치 등 확인
kubectl version --client=true -o yaml | yh
  gitVersion: v1.25.7-eks-a59e1f0

eksctl version
0.138.0

aws --version
aws-cli/2.11.15 Python/3.11.3 Linux/4.14.311-233.529.amzn2.x86_64 exe/x86_64.amzn.2 prompt/off

ls /root/.ssh/id_rsa*

# 도커 엔진 설치 확인
docker info

Host EC2가 잘 준비됐다면 eks 배포를 진행한다.

#vpcid와 Subnetid을 변수 입력
export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" | jq -r .Vpcs[].VpcId)
echo "export VPCID=$VPCID" >> /etc/profile
export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
echo "export PubSubnet1=$PubSubnet1" >> /etc/profile
echo "export PubSubnet2=$PubSubnet2" >> /etc/profile

# 변수 확인
echo $AWS_DEFAULT_REGION
echo $CLUSTER_NAME
echo $VPCID
echo $PubSubnet1,$PubSubnet2

# 옵션 [터미널1] EC2 생성 모니터링
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table

# eks 클러스터 & 관리형노드그룹 배포 전 정보 확인
eksctl create cluster --name $CLUSTER_NAME --region=$AWS_DEFAULT_REGION --nodegroup-name=$CLUSTER_NAME-nodegroup --node-type=t3.medium --node-volume-size=30 --vpc-public-subnets "$PubSubnet1,$PubSubnet2" --version 1.24 --ssh-access --external-dns-access --dry-run | yh
apiVersion: eksctl.io/v1alpha5
cloudWatch:
  clusterLogging: {}
iam:
  vpcResourceControllerPolicy: true
  withOIDC: false
kind: ClusterConfig
kubernetesNetworkConfig:

# eks 클러스터 & 관리형노드그룹 배포 
eksctl create cluster --name $CLUSTER_NAME --region=$AWS_DEFAULT_REGION --nodegroup-name=$CLUSTER_NAME-nodegroup --node-type=t3.medium --node-volume-size=30 --vpc-public-subnets "$PubSubnet1,$PubSubnet2" --version 1.24 --ssh-access --external-dns-access --verbose 4
2023-04-28 23:59:29 [ℹ]  will create 2 separate CloudFormation stacks for cluster itself and the initial managed nodegroup
2023-04-28 23:59:29 [ℹ]  if you encounter any issues, check CloudFormation console or try 'eksctl utils describe-stacks --region=ap-northeast-2 --cluster=myeks'
2023-04-28 23:59:29 [ℹ]  Kubernetes API endpoint access will use default of {publicAccess=true, privateAccess=false} for cluster "myeks" in "ap-northeast-2"

배포가 잘 되는지 확인해보기 위해 Cloudformation과 EKS 그리고 EC2, ASG 등을 확인해보았다.

배포가 잘 된 것을 확인할 수 있었다.
kubectl get node 을 통해 node을 확인하면 일전에 kOps 스터디 때 결과와는 다르게 Control Plane은 나타나지 않는 것을 확인할 수 있다. api Server와의 통신은 가능하지만 get node에 별도로 Control Plane 정보를 표시해주진 않는다. 아래는 kOps 때 kubectl get node 결과이다 비교해보자.

Control Plane이 포함 된 kOps와 AWS Managed 영역에 Control Plane이 배포되는 EKS와 get node 결과가 다른 것을 확인할 수 있었다.

설치가 제대로 됐으니 이제 Worker Node에 접속을 해보겠다.

# 노드 IP 확인 및 PrivateIP 변수 지정
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
N1=192.168.1.149
N2=192.168.2.226

# eksctl-host 에서 노드의IP나 coredns 파드IP로 ping 테스트
ping -c 2 $N1
ping -c 2 $N2
PING 192.168.1.149 (192.168.1.149) 56(84) bytes of data.
--- 192.168.1.149 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1020ms
#현재는 ping이 가지 않는다.

# 노드 보안그룹 ID 확인
aws ec2 describe-security-groups --filters Name=group-name,Values=*nodegroup* --query "SecurityGroups[*].[GroupId]" --output text
NGSGID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=*nodegroup* --query "SecurityGroups[*].[GroupId]" --output text)
echo $NGSGID

# 노드 보안그룹에 eksctl-host 에서 노드(파드)에 접속 가능하게 룰(Rule) 추가 설정
aws ec2 authorize-security-group-ingress --group-id $NGSGID --protocol '-1' --cidr 192.168.1.100/32

# eksctl-host 에서 노드의IP나 coredns 파드IP로 ping 테스트
ping -c 2 $N1
ping -c 2 $N2
(k8sadmin@myeks:default) [root@myeks-host ~]# ping -c 2 $N1
PING 192.168.1.149 (192.168.1.149) 56(84) bytes of data.
64 bytes from 192.168.1.149: icmp_seq=1 ttl=255 time=0.535 ms
64 bytes from 192.168.1.149: icmp_seq=2 ttl=255 time=0.347 ms
--- 192.168.1.149 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1031ms
rtt min/avg/max/mdev = 0.347/0.441/0.535/0.094 ms
#이후 PING 테스트를 다시 하면 정상적으로 PING이 가는 것을 알 수 있다.

# 워커 노드 SSH 접속
ssh -i ~/.ssh/id_rsa ec2-user@$N1 hostname
ip-192-168-1-149.ap-northeast-2.compute.internal
ssh -i ~/.ssh/id_rsa ec2-user@$N2 hostname
ip-192-168-2-226.ap-northeast-2.compute.internal
ssh -i ~/.ssh/id_rsa ec2-user@$N1 
exit
ssh -i ~/.ssh/id_rsa ec2-user@$N2
exit

보안그룹에 Rule 추가 후 Ping 뿐 아니라 SSH 접속까지 문제 없이 되는 것을 확인할 수 있었다.

조금 재밌지만 어쩌면 당연한 사실은 ENI을 보면 Amazon EKS myeks로 설명이 되어있는 Control Plane ENI의 경우 소유자와 요청자의 Account ID가 다른 것을 확인할 수 있다. ENI의 소유자는 사용자 Account고 요청자는 다른 Account이다. ENI에 연결 된 인스턴스(AWS 콘솔 상에서는 확인되지 않는 CP Node)의 소유자 또한 사용자 Account가 아닌 별도의 Account임을 확인할 수 있다.

2. AWS EKS 기본 사용

EKS 설치를 확인했으니 간단한 기본 사용법을 확인해본다.
우선 사용 이전에 명령어 간소화를 위해 alias 축약 설정을 진행한다.

# 자동 완성 및 alias 축약 설정
source <(kubectl completion bash)
alias k=kubectl

(k8sadmin@myeks:default) [root@myeks-host ~]# k get node
NAME                                               STATUS   ROLES    AGE   VERSION
ip-192-168-1-149.ap-northeast-2.compute.internal   Ready    <none>   34m   v1.24.11-eks-a59e1f0
ip-192-168-2-226.ap-northeast-2.compute.internal   Ready    <none>   34m   v1.24.11-eks-a59e1f0

Sample Replica3개를 설정해서 Pod 배포를 진행해본다.

# 터미널1 (모니터링)
watch -d 'k get pod'

# 터미널2
# Deployment 배포(Pod 3개)
kubectl create deployment my-webs --image=gcr.io/google-samples/kubernetes-bootcamp:v1 --replicas=3
kubectl get pod -w

# 파드 증가 및 감소
kubectl scale deployment my-webs --replicas=6 && kubectl get pod -w
kubectl scale deployment my-webs --replicas=3
kubectl get pod

# 강제로 파드 삭제
kubectl delete pod --all && kubectl get pod -w
kubectl get pod

deployment 이후 3개의 Pod가 올라온 것을 확인할 수 있다.

Replicas을 6으로 설정하면 3개 Pod가 더 추가돼서 총 6개의 Pod가 올라오게 된다.

Replicas을 다시 3으로 조정하면 3개의 Pod가 줄어들어 기존 3개 올라왔던 Pod가 남게 된다.

Delete Pod -all로 강제로 Pod 삭제를 진행하게 되면 기존 올라왔던 Pod 3개가 지워지고 신규 Pod 3개가 다시 올라오는 것을 알 수 있다. (Replicas=3이기 때문에)

간단하게 Pod 배포 테스트를 통해 이상 없이 Pod 배포 및 Replicas 조절에 이상 없음을 확인할 수 있었다.

3. AWS EKS with Fargate

위 1번에서는 EKS 노드그룹 배포를 ASG를 통한 EC2 배포로 진행했었다. 여기서 추가로 노드그룹을 배포하면서 이번엔 Fargate로 배포를 진행해보려고 한다.


진행은 아래와 같이 진행해보았다.
fargate을 사용하는 eks cluster을 생성해준다.

# fargate을 사용하는 eks cluster을 생성
eksctl create cluster --name eks-fargate --region $AWS_DEFAULT_REGION --fargate
(k8sadmin@myeks:default) [root@myeks-host ~]# eksctl create cluster --name eks-fargate --region $AWS_DEFAULT_REGION --fargate
2023-04-29 01:07:34 [ℹ]  eksctl version 0.139.0
2023-04-29 01:07:34 [ℹ]  using region ap-northeast-2
2023-04-29 01:07:34 [ℹ]  setting availability zones to [ap-northeast-2c ap-northeast-2a ap-northeast-2b]


위와 같이 fargate profile과 node가 잘 생성 된 것을 확인했으면 pod 배포를 진행해서 테스트해보면 된다. 이때 동일 Account에 2개 이상의 EKS Cluster가 생성되었기 때문에 kubectl 명령어 사용 시 충돌이 발생하지 않게 하기 위해 kubeconfig 설정을 먼저 진행한다.

#현재 선택 된 context 확인
kubectl config current-context
(k8sadmin@eks-fargate:N/A) [root@myeks-host .kube]# kubectl config current-context
k8sadmin@eks-fargate.ap-northeast-2.eksctl.io

#eks-fargate cluster가 선택된 것을 확인했으나 혹시 앞서 실습한 myeks cluster일 경우 변경 진행
kubectl config use-context eks-fargate

#배포 시 context 사용하여 배포도 가능
kubectl apply -f filepath --context clustercontext

Fargate Node을 사용하는 k8s deployment 파일을 작성하고 배포한다.

#demo-fargate-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app
        image: nginx:latest
        ports:
        - containerPort: 80

#yaml 파일 통한 배포
kubectl apply -f demo-fargate-deployment.yaml
deployment.apps/my-app created

#배포 확인
k get pods --output wide

위와 같이 제대로 배포가 완료된 것을 확인할 수 있다.

4. 정리

kOps 스터디를 진행한지 얼마 안 돼서 그런가 k8s가 조금은 익숙한 상태로 시작한 것 같다.
그럼에도 kOps와 EKS는 다른 부분들이 있어 그 부분들을 하나하나 짚어보며 상세히 알아가는 스터디가 될 것 같아서 기대가 된다. 그리고 이번 실습 때는 간단하게나마 fargate을 사용해봤는데 AWS을 쓰면서 fargate는 처음 사용해봐서 오늘 실습은 조금 더 의미가 있었던 것 같다.
다음에는 Private Repo을 활용하는 부분도 실습해볼 예정이다.

PKOS Study #5 – Security

PKOS Study #5는 Security 세션으로 Node의 Metadata 탈취와 그에 대한 보안을 IRSA을 통해 진행하고 kubescape, Polaris을 통해 Deployment, Pod에 대해 점검하는 부분을 진행할 예정이다.

0. 환경 구성

이전 실습 때의 환경을 그대로 사용할 예정이다. 다만, 이전 실습 때 2a AZ에 대해 Worker Node를 2개로 확장했던 부분(Node Down Alert Test 목적)이 있어 이건 다시 1개로 축소하고 진행할 예정이다.

ap-northeat-2a에 대해 Size을 1로 축소하는 명령과 Update을 진행했다.

Update가 완료됐고 기존 2대였던 2a의 Worker Node가 1개로 변경되었다.

Pod에서 EC2의 Metadata을 탈취하기 위해 2대의 Worker Node 중 2a AZ에 있는 Worker Node의 IMDSv2 보안을 제거해야 한다.

변경 전 화면이다

위처럼 변경을 진행하고 Rolling Update을 진행한다.

생각해보면 앞서 Worker Node을 2대에서 1대로 변경할 때 해당 내용도 같이 적용해서 변경했으면 어땠을까 하는 생각이 들었다.

사전에 원복하고 진행해야 겠다는 생각이 먼저 들고 이후 환경 설정을 하다보니 Rolling Update 하는데 시간이 2배로 발생하게 됐다.

위와 같이 Rolling Update가 완료됐고 본격적으로 스터디 실습을 시작하도록 하겠다.

1. EC2 IAM&Metadata

실습 환경 구성 때 2대의 Worker Node 중에서 1대의 Worker Node(2a AZ)에 대해 IMDSv2 보안을 제거하였었다.

제대로 설정이 진행되었다면 2a AZ Node에 배포 된 Pod에서는 Node EC2에 대한 Metadata를 확인할 수 있을 것이다.

아래와 같이 확인을 진행해보았다.

<em># 파드 이름 변수 지정</em>
PODNAME1=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[0].metadata.name})
PODNAME2=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[1].metadata.name})

<em># EC2 메타데이터 정보 확인</em>
kubectl exec -it $PODNAME1 -- curl 169.254.169.254 ;echo
kubectl exec -it $PODNAME2 -- curl 169.254.169.254 ;echo

Pod 1, 2에 대해 각각 변수 지정을 해주고 exec을 통해 각 Pod에서 AWS Metadata 호출인 curl 169.254.169.254을 실행해주었다.

Pod1은 Metadata가 확인되지만 Pod2에서는 Metadata가 확인되지 않는다. 이렇게만 보면 Pod1이 2a AZ Node에 배포 된 Pod로 보이지만 혹시 모르니 추가적으로 확인을 진행해보았다.

kubectl get pod -o wide로 2개의 Pod에 대해 Node 정보를 확인했고 AWS Console에서 EC2 정보를 통해 Instance ID값을 불러왔다.

Pod1의 이름인 netshoot-pod-7757d5dd99-5g9bw의 Node는 i-0ec2490cd0951b339이다. i-0ec2490cd0951b339의 Instance Name은 2a AZ의 EC2임을 확인할 수 있었다. 이와 같이 IMDSv2 보안을 제거한 EC2에 배포 된 Pod는 해당 EC2의 Metadata을 볼 수 있음을 확인할 수 있다. 이 점은 보안 취약점이라고 볼 수 있다. Pod의 특정 권한이 탈취되었을 때 Pod을 통해 Node 역할을 하는 EC2의 정보 조회는 물론 제어권까지 가져올 수 있기 때문이다.

특히 위처럼 security credentials 정보를 조회하면 Accesskey, SecretKey 그리고 Token까지 확인할 수 있기 때문에 AWS Account 측면의 보안에도 매우 취약하게 된다.

그럼 위에서 확인한 Accesskey, Secretkey, Token으로 어떤 작업까지 가능한지 한 번 확인을 진행해본다.

awscli가 설치 된 Image을 이용해서 Pod을 배포한다음 테스트를 진행한다.

Pod에 bash 연결해 Accesskey, Secretkey, Token 입력 후 aws cli을 통해 instance 정보를 출력해보았는데 역시나 정보를 받아올 수 있었다. 이는 사실 IMDSv2 보안이 제거된 것과는 무관하다. 무슨 뜻이냐면 IMDSv2 보안 제거가 되었기 때문에 정보를 확인할 수 있는 것이 아니라 보안이 제거 된 상태에서 긁어온 Security Credentials 정보를 통해 AWSCLi에 인증 받아 정보를 받아온 것이기 때문이다.

여기서 탈취한 Credentials 정보의 권한이 FullAccess라면 해당 정보를 통해 어떤 작업이든 가능하다고 볼 수 있다. 물론 Expired Time에 따라 시간이 지나면 사용할 수 없을 수 있지만 이미 IMDSv2 보안 제거로 인해 얼마든지 Metadata을 불러올 수 있기 때문에 이 부분은 전혀 문제가 되지 않는다.

앞서는 IMDSv2 보안이 제거 된 Node Instance의 정보를 통해 AWS Account의 접근까지 위험한 상황이 발생하는 것에 알아보았다면 이제는 이 부분에 대한 방어책을 알아봐야 한다.

IRSA(IAM Roles for Service Accounts)을 사용해서 진행할 계획이다. 위에 내용을 보면 생성 된 Pods는 Node의 IAM을 가져오게 되어있다. 이 부분을 IRSA을 통해 Pod들 마다의 권한을 부여할 계획이다. 그렇게 되면 Metadata 유출로 인한 보안 사고를 방지할 수 있다. 

스터디 실습 환경은 EKS가 아닌 kOps을 활용해서 진행하기 때문에 kOps에서 IRSA을 사용하는 방안으로 진행할 예정이다. IRSA을 kOps에서 진행하기 위한 설정은 아래 링크를 참고하였다.Cluster Resource – kOps – Kubernetes OperationsThe Cluster resource The Cluster resource contains the specification of the cluster itself. The complete list of keys can be found at the Cluster reference page. On this page, we will expand on the more important configuration keys. The documentation for tkops.sigs.k8s.io

사전에 준비해야 하는 내용은 다음과 같다.

IAM Policy : 나는 S3에 대해 ListAllMyBucket을 적용한 Policy을 미리 생성하였다.

S3 Bucket : Public Read가 가능한 Bucket을 생성하였다.

<em>#kops edit cluster</em>

spec:
  podIdentityWebhook:
    enabled: true
  serviceAccountIssuerDiscovery:
    discoveryStore: s3://publicbucket
    enableAWSOIDCProvider: true
  iam:
    allowContainerRegistry: true
    legacy: false
    serviceAccountExternalPermissions:
    - aws:
        policyARNs:
        - arn:aws:iam::accountNo:policy/irsa-pol
      name: irsa-iam
      namespace: default

위와 같이 cluster edit을 해주고 rolling update를 진행했다.

참고로 policyARN에 표시 된 Policy는 위 화면과 깉이 ListAllMyBuckets 작업이 허용되어있다. 후에 해당 권한으로 S3 ls을 실행해서 테스트할 예정이다.

Rolling Update가 진행되는 동안 Public Bucket으로 설정한 S3 Bucket에 들어가보았고 위 화면과 같이 s3bucket/openid/v1/jwks 경로에 토큰 정보가 생성된 것을 확인할 수 있었다.

마찬가지로 IAM 자격 증명 공급자에도 위와 같이 공급자가 생성된 것을 확인할 수 있다. 공급자 주소는 위에서 사용 된 s3 bucket의 경로가 표시된다.

긴 기다림 끝에 Rolling Update도 완료되었다.

이제 ServiceAccount을 통해 Pod에 권한을 부여하고 해당 Pod에서 권한을 잘 받았는지 확인해보려고 한다.

우선 Service Account 없이 생성한 awscli pod에서는 역시나 권한이 없다고 나온다.

그럼 이제 Service Account을 포함해서 배포한 Pod에서 테스트를 진행해본다.

S3 Bucket List을 불러오는데 성공했다. 이렇게 Pod에 각각 IAM 권한을 부여하는 방식으로 보안을 유지할 수 있게 된다.

물론, IMDSv2 보안을 유지함으로 인해 Metadata 자체를 노출시키지 않는 것도 중요하니 이 부분을 까먹지 말아야 한다.

2. kubescape을 통한 Cluster 취약점 점검

kubescape는 보안 권고 사항 기반으로 k8s Cluster(yaml, helm 등)의 취약점을 점검해주는 서비스이다.

curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash

kubescape download artifacts
tree ~/.kubescape/
cat ~/.kubescape/attack-tracks.json | jq

설치는 간단하게 install.sh 파일을 통해 진행 된다. 설치를 하고 articats를 다운 받는다.

설치도 하고 Artifacts도 받았으니 이제 Scan을 진행해보도록 한다.

Scan을 실행하면 위와 화면이 나오면서 Scan이 진행된다.

Scan이 완료되면 위와 같이 결과값이 나온다. 생각보다 Critical이 꽤 많은 상황이다. 아마도 이것저것 테스트를 하며 설정을 변경했기 때문인 것 같다.

kubescape은 간략하게 확인하는 내용만 진행하고 polaris로 넘어간다. Polaris는 Webpage도 제공하기 때문에 Cirtical, Warning 등 내용을 확인하며 수정하는 테스트를 병행하기 수월하기 때문이다.

3. polaris을 통한 보안 점검 진행

위에서 진행한 kubescape와 비슷한 서비스인 Polaris를 실습해보려고 한다.

Polaris는 리소스 구성을 검증하고 수정하는 k8s용 오픈소스 Policy Engine이다. 30개 이상의 기본 정책을 포함하고 있고 JSON 기반의 사용자 정책도 적용할 수 있다.

설치는 위와 같이 진행 된다. GitOps 이후 오랜만에 helm install을 이용해서 설치하는 실습인 것 같다.

배포 된 후 연결 된 도메인에 접근하면 위와 같은 화면을 볼 수 있다.

Score는 Polaris가 갖고있는 모범 사례 정책 대비 내 구성 내역의 점수를 나타낸다. 68%라는 건 32% 정도 부족한 부분을 갖고 있다고 보면 좋을 것 같다. 당연히 점수가 높을수록 권고 사항을 많이 갖추고 있다고 볼 수 있다.

Namespace Filter을 적용할 수도 있다. 나는 GitOps 때 생성한 argocd를 Filter해보았다.

argocd Namespace만의 점수를 볼 수 있다. 아까 전체적인 점수는 68%에 Grade는 D+였는데 argocd는 78%에 C+로 전체보다는 비교적 높은 점수임을 알 수 있다. 그리고 dangerous checks도 0개이니 비교적 안전하게 유지 중이라고 볼 수 있다.

이번엔 Deployment을 분석하기 위해서 Namespace Filter을 다시 Default로 변경하였고 그 중에 아까 실습 때 배포한 netshoot-pod Deployment을 분석해보았다. 기준 정책 도달한 내역보다 도달하지 못한 내역들이 더 많은 것으로 보인다. 그 중에 수정할만한 내용을 찾아보겠다. Should not be allowed to run as root 메시지를 보면 Root 권한으로 실행시킨다는 것을 알 수 있다. 이 문제를 해결하기 위해 yaml 파일을 수정한다.

    spec:
      containers:
      - name: netshoot-pod
        image: nicolaka/netshoot
        command: ["tail"]
        args: ["-f", "/dev/null"]
        <em>#secuirtyContext을 runAsNonRoot 설정해준다.</em>
        securityContext:
          runAsNonRoot: true

위와 같이 securityContext에 runAsNonRoot을 입력하고 다시 배포를 진행한다.

Container netshoot-pod 섹션에 root 관련 문구가 Passing Check로 바뀐 것을 알 수 있다. 나머지 2개의 Critical도 수정해보았다.

image Tag을 입력해주었고 앞서 설정한 runAsNonRoot 밑에 privileged을 false로 작성하고 Capabilities을 설정해 권한을 제한하였다.

Critical을 모두 제거된 것을 확인할 수 있다. Warning도 비슷한 방식으로 제거할 수 있지만 우선은 Critical만 제거하는 것으로 진행한다.

지금까지는 Polaris을 통해 이미 배포한 Pod에 대한 점검만을 진행했었다. 그럼 점검 사항에 위배되는 배포를 진행할 때 방지하는 방법은 없을까? polaris webhook을 사용하면 된다.

<em># webhook 활성화 적용</em>
helm upgrade polaris fairwinds-stable/polaris --namespace polaris --version 5.7.2 --reuse-values --set webhook.enable=true 
kubectl get pod,svc -n polaris

위처럼 Webhook 기능을 활성화 하고 Pod 배포를 진행한다.

배포에 사용한 yaml은 위에 Critical 해소 때 사용한 구문을 지우고 수정한 버전의 yaml을 사용하였다.

배포가 진행되지 않은 것을 알 수 있다. Privilege, run as root, Image Tag 등에 대해 문제가 있다고 나오고 get pod를 실행했을 때도 해당 pod가 배포되지 않은 것을 확인할 수 있었다.

4. 마치며

이번 세션을 마지막으로 kOps 스터디가 마무리되었다. 5주라는 짧은 시간 동안 kOps을 통해 k8s을 배울 수 있었다. 단순하게 k8s을 구성하고 pod들을 배포하는 실습이 아닌 GitOps 환경을 구성하고 Prometheus와 Grafana을 통해 Monitoring&Alert 설정도 진행했었다.

그리고 마지막으로는 Security 부분까지 진행해볼 수 있었는데 이 부분이 제일 도움이 되었던 것 같다. 프로젝트를 진행하다보면 요건 사항 중에 보안에 대한 부분이 사실 가장 많이 나올 수밖에 없는데 이때 Public Cloud Native로 조절할 수 있는 부분과 다른 서비스를 사용하는 방안이 있는데 이런 것들을 병합하여 사용할 수 있다는 걸 알게 된 게 가장 큰 수확이라고 생각한다.

스터디는 종료됐지만 미흡했거나 제대로 이해하지 못했던 부분들은 다시 한 번 처음부터 진행해보면서 나름대로 정리하는 기회로 삼으면 좋을 것 같다.

유익한 스터디를 기획하고 진행해주신 가시다님(Blog링크)께 감사 인사를 드린다.

PKOS Study #4 – Monitoring

PKOS Study #4은 Prometheus, Grafana을 사용하여 k8s 환경 Monitoring, Alert을 진행해보려 한다.

0. 환경 구성

이번 실습은 지난 Study #3의 환경에 그대로 병행해서 진행할 예정이다. 리소스를 지우고 다시 만드는데 시간이 소요되고 kOps설치 시 레이턴시 이슈인지 한 번에 되지 않을 때가 있어 이번엔 이전 환경을 삭제하지 않고 그대로 진행한다.

1. Metric-server 확인

Metric-server : kubelet으로부터 리소스 매트릭을 수집 및 집계하는 클러스터 애드온 구성 요소이다. 실습에 사용 된 yaml에는 Metric-server가 올라오는 낸용이 포함되어 있기 때문에 실습 내용으로 진행하게 되면 Metric-server을 바로 확인 할 수 있다.

node(cp, worker)들의 CPU, Memory 사용량을 확인할 수 있고 각 pod들의 CPU, Memory 점유량도 확인할 수 있다. 나는 지난 실습 환경을 그대로 유지하고 있기 때문에 gitlab, argocd pod들의 내용도 확인할 수 있다.

2. Prometheus를 통한 Metric 수집

Prometheus : Soundcloud에서 제작한 오픈소스 시스템 Monitoring, Alert 툴킷이다. 

특징으로는 간단한 구조를 갖추고 있어 수월한 운영을 할 수 있고 강력한 쿼리 기능을 통해 다양한 결과값을 도출할 수 있다. 뒤에 실습할 Grafana와의 조합을 통해 시각화를 할 수 있고 ELK와 같은 로깅 방식이 아니라 시스템으로부터 모니터링 지표를 수집하여 저장하는 시스템이다. 자세한 구조는 아래 그림과 같다.

설치는 아래와 같이 진행한다. 이번엔 실습 때 제공되는 코드에서 인증서 ARN과 Cluster 주소를 한 번에 넣어주는 방식을 사용한다.

예제로 제공 받은 코드는 Grafana도 같이 helm Install을 사용하여 설치를 진행하게 된다. 아래 코드로 설치를 진행하면 Prometheus와 Grafana가 같이 설치되는 점 참고하여 진행한다.

<em># 모니터링</em>
kubectl create ns monitoring
watch kubectl get pod,pvc,svc,ingress -n monitoring

<em># 사용 리전의 인증서 ARN 확인</em>
CERT_ARN=`aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text`
echo "alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN"

<em># 설치</em>
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts

<em># 파라미터 파일 생성</em>
cat <<EOT > ~/monitor-values.yaml
alertmanager:
  ingress:
    enabled: true
    ingressClassName: alb

    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/success-codes: 200-399
      alb.ingress.kubernetes.io/group.name: "monitoring"

    hosts:
      - alertmanager.$KOPS_CLUSTER_NAME

    paths:
      - /*

grafana:
  defaultDashboardsTimezone: Asia/Seoul
  adminPassword: prom-operator

  ingress:
    enabled: true
    ingressClassName: alb

    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/success-codes: 200-399
      alb.ingress.kubernetes.io/group.name: "monitoring"

    hosts:
      - grafana.$KOPS_CLUSTER_NAME

    paths:
      - /*

prometheus:
  ingress:
    enabled: true
    ingressClassName: alb

    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/success-codes: 200-399
      alb.ingress.kubernetes.io/group.name: "monitoring"

    hosts:
      - prometheus.$KOPS_CLUSTER_NAME

    paths:
      - /*

  prometheusSpec:
    podMonitorSelectorNilUsesHelmValues: false
    serviceMonitorSelectorNilUsesHelmValues: false
    retention: 5d
    retentionSize: "10GiB"
EOT

<em># 배포</em>
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 45.7.1 \
--set prometheus.prometheusSpec.scrapeInterval='15s' --set prometheus.prometheusSpec.evaluationInterval='15s' \
-f monitor-values.yaml --namespace monitoring

위와 같이 정상적으로 배포 된 것을 확인할 수 있다.

프로메테우스가 각 서비스의 metric 경로에 HTTP GET 방식으로 메트릭 정보를 가져와 TSDB 형식으로 저장을 하게 된다.

<em># 아래 처럼 프로메테우스가 각 서비스의 9100 접속하여 메트릭 정보를 수집</em>
kubectl get node -owide
kubectl get svc,ep -n monitoring kube-prometheus-stack-prometheus-node-exporter

<em># 마스터노드에 lynx 설치</em>
ssh -i ~/.ssh/id_rsa ubuntu@api.$KOPS_CLUSTER_NAME hostname
ssh -i ~/.ssh/id_rsa ubuntu@api.$KOPS_CLUSTER_NAME sudo apt install lynx -y

<em># 노드의 9100번의 /metrics 접속 시 다양한 메트릭 정보를 확인할수 있음 : 마스터 이외에 워커노드도 확인 가능</em>
ssh -i ~/.ssh/id_rsa ubuntu@api.$KOPS_CLUSTER_NAME lynx -dump localhost:9100/metrics

Prometheus 주소(Ingress와 연결 된 Domain 주소)에 접속하면 아래와 같이 접속 화면을 확인할 수 있다.

2. Grafana을 통한 시각화

Grafana는 Grafana Labs에서 개발한 Metric/Log 시각화 Dashboard이다. 다양한 Datasource을 연결해서 시각화하여 제공할 수 있다.

실습 예제로 제공 된 코드에는 Grafana도 Prometheus와 같이 Helm install를 통해 설치하기 때문에 별도 설치하는 내용은 건너뛰고 Grafana에 접근하여 진행하도록 한다. 초기 제공 되는 계정을 사용하여 로그인하고 암호를 변경한다.

암호 변경하고 Data Source을 확인해보면, http://kube-prometheus-stack-prometheus.monitoring:9090/ 가 입력되어 있는 것을 알 수 있다.

kube-system 이외에 상태를 체크하기 위해 테스트용 pod을 배포한 다음 대쉬보드를 통해 확인해 볼 예정이다.

우선 Dashobard을 생성한다. 추천 받은 Dashobard 목록 중 한국어 버전으로 나온 걸 추가해볼 생각이다.

Dashboard->Import을 선택한다.

Import via Grafana.com에 추천 받은 Dashboard의 번호를 입력한다. 여기서는 13770을 입력하고 Load을 클릭한다.

Name과 Data Source을 확인하고 Import을 클릭한다.

몇 번의 클릭만으로 훌륭한 대쉬보드를 가져올 수 있게 됐다. 여기서 불필요한 건 제거하고 필요하다고 생각되는 걸 추가하는 식으로 나만의 Dashboard을 꾸밀 수 있다.

테스트 목적의 Application은 nginx 웹서버를 배포할 예정이다. 배포할 때 참고하는 yaml에는 monitoring 옵션을 활성화해서 바로 Prometheus에서 모니터링할 수 있도록 한다.

위 사진 처럼 Prometheus에 바로 해당 내용을 확인할 수 있다. 예제로 Connections_active을 Graph로 확인해봤다. 메트릭 수집이 정상적으로 되는 것을 확인했으니 이를 Grafana에서 확인해보도록 하겠다.

위에서 Dashboard을 Import할 때와 같은 방식으로 Import ID는 12708을 사용해서 생성한 대쉬보드이다. 요청 수나 Active Connection 수 등을 확인할 수 있다. 앞서 Prometheus에서 확인한 Connections_active와 동일한 모양임을 알 수 있다.

이와 같이 Prometheus에서 수집되는 데이터를 바탕으로 Grafana에서 시각화할 수 있는 것을 확인해보았다.

3. kwatch을 통한 Application 문제  전송

Grafana로 시각화하는 것까지 확인했으니 이제 kwatch을 사용해 Application 충돌 등 이슈를 확인해 Webhook 전송하는 것을 진행하려 한다.

kwatch : Kubernetes(K8s) 클러스터의 모든 변경 사항을 모니터링하고, 실행 중인 앱의 충돌을 실시간으로 감지하고, 채널(Slack, Discord 등)에 즉시 알림을 게시하도록 도와주는 서비스이다. 나는 Study에서 제공 된 webhook이 아닌 개인 스터디 목적으로 생성한 Workspace에 별도 webhook을 생성해서 진행할 예정이다.

우선 간단하게 웹훅을 생성하는 방법을 소개한다.

내가 속해있는 Workspace에서 “+ 앱 추가”을 선택한다.

검색창에 webhook을 검색하고 나온 결과 중 incoming-webhooks에 추가 버튼을 클릭한다.

브라우저로 URL이 연결되면서 위와 같은 페이지를 볼 수 있다. 여기서 Slack에 추가를 선택한다.

Webhook을 초대할 채널명을 선택한다. 여기서는 #스터디 를 선택하였다. 이후 “수신 웹후크 통합 앱 추가”를 선택한다.

Webhook 이름과 아이콘(선택사항)을 변경하고 웹후크URL을 따로 복사해놓고 설정 저장을 클릭한다. 복사한 웹후크 URL은 아래 kwatch 배포 코드에서 webhook 주소에 해당 URL을 입력해주면 된다.

Slack-bot이 정상적으로 추가 된 것을 확인할 수 있다. (위 추가 된 건 테스트 용도로 생성한 거니 무시해도 된다.)

이제 Webhook을 생성했으니 kwatch 배포를 진행한다.

<em># configmap 생성</em>
cat <<EOT > ~/kwatch-config.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: kwatch
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: kwatch
  namespace: kwatch
data:
  config.yaml: |
    alert:
      slack:
        webhook: 'https://mywebhookurl'
        <em>#title:</em>
        <em>#text:</em>
    pvcMonitor:
      enabled: true
      interval: 5
      threshold: 70
EOT
kubectl apply -f kwatch-config.yaml

<em># 배포</em>
kubectl apply -f https://raw.githubusercontent.com/abahmed/kwatch/v0.8.3/deploy/deploy.yaml

위 코드에서 webhook URL만 내 웹훅 URL로 변경하고 실행하면 된다.

배포를 하게 되면 내 채널에 kwatch started라는 메시지가 뜨게 된다.

이제 kwatch가 정상적으로 역할을 수행하는지 확인하기 위해서 잘못 된 이미지 정보를 갖고 있는 pod을 배포해보도록 한다.

<em># 터미널1</em>
watch kubectl get pod

<em># 잘못된 이미지 정보의 파드 배포</em>
kubectl apply -f https://raw.githubusercontent.com/junghoon2/kube-books/main/ch05/nginx-error-pod.yml
kubectl get events -w

Error가 나오면서 배포가 완료되지 않는 것을 알 수 있다.

마찬가지로 Slack에서도 위와 같은 경고를 보내준다. Logs까지 상세하게 잡아줘서 대략적으로 어떤 문제인지 확인할 수 있다.

이제 문제가 발생했으니 해결해야 할 차례다. 위에서 사용한 yaml을 불러와 본다.

image: nginx:1.19.19 는 존재하지 않는 이미지이기 때문에 배포가 제대로 되지 않았음을 알 수 있다.

nginx-19 pod의 이미지 정보를 1.19.19에서 1.19로 변경하는 작업을 진행해줬다.

ImagePullBackOff 상태였던 nginx-19가 제대로 Running 상태로 변경된 것을 확인할 수 있었다.

4. Grafana을 통한 경고 전송

kwatch에서 Pod의 문제를 확인하여 트러블슈팅하는 과정을 진행해보았다면 이번엔 Grafana에서 시각화 된 메트릭 중 장애를 확인하여 경고 전송해주는 내용을 진행해볼 예정이다. 스터디에서는 Prometheus Alert Manager을 사용했지만 나는 이번 실습에서 Grafana을 통해 진행해보려고 한다.

경고 전송을 위해 우선 Contact points을 생성한다.

Alerting->Net Contact Point을 선택한다.

Name은 임의로 입력하고 Webhook URL에 kwatch 때 사용했던 Webhook URL을 입력한 다음 Test을 실행한다. 문제없이 Slack 채널에 테스트 메시지가 전송 될 경우 Save Contact Point을 클릭해 설정을 저장한다.

Alert 창구는 만들었으니 이제 경고를 만들어 테스트를 진행해보려 한다. 여기서는 Node가 다운되었을 때 경고를 받는 것을 테스트해볼 예정이다.

위와 같은 조건으로 설정을 진행해보았다.

kube_node_status_condition Metric에서 Condition=Ready, Status=Unknown이 0보다 클 경우 경고를 발생하는 조건이다.

Node 1개에서 kubectl service을 stop 했더니 아래와 같이 경고를 받을 수 있었다.

경고를 받았으니 조치를 취해야 한다. Stop했던 node의 서비스를 다시 시작시켜줬다.

위 메시지가 온 것으로 해결됐음을 알 수 있다.

5. 마무리

모니터링은 서비스 운영에 있어 중요한 지점이나 항상 쉽지 않은 것 같아. Metric-server에서 데이터를 수집하고 Prometheus와 Grafana을 이용해 시각화, 경고를 전송하는 것을 테스트해보았다.

Alert 자체가 수집할 수 있는 Metric의 조건을 거의 모두 활용할 수 있기 때문에 내가 원하는 방식의 경고를 생성할 수 있을 것 같다.

꼭 k8s가 아니더라도 인프라 운영에 있어 Prometheus, Grafana 조합을 활용해봐야겠다.

PKOS Study #3 – GitOps System

PKOS Study #3은 GitOps System을 다룰 예정이다. 기존 실습에서는 kOps Instance에서 yaml을 통해 배포를 진행했었는데 GitOps을 사용하여 파이프라인을 통한 배포를 진행하는 것이 이번 실습의 목표이다.

0. 환경 구성

이번 실습도 Study #2와 마찬가지로 One-Click kOps yaml을 통해 진행했다. (maxpods 설정은 제거하고 진행했다.)

1. Harbor을 통해 Image 저장소 구축

Harbor : Docker 이미지를 저장하고 관리할 수 있는 중앙 집중식 이미지 저장소이다. Harbor을 통해 로컬 개발 환경에서 Docker 이미지를 빌드한 뒤 업로드할 수 있고 Docker CLI 및 API와 호환이 가능하다. 또한 이미지의 보안적 취약점 및 인증 문제를 확인할 수 있는 특징이 있다.

Harbor을 HelmChart을 통해 설치한다.

# 사용 리전의 인증서 ARN 확인
aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text
CERT_ARN=`aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text`
echo "alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN"

# 하버 설치<
helm repo add harbor https://helm.goharbor.io
helm fetch harbor/harbor --untar --version 1.11.0
vim ~/harbor/values.yaml
----------------------
expose.tls.certSource=none                        # 19줄
expose.ingress.hosts.core=harbor.<각자자신의도메인>    # 36줄
expose.ingress.hosts.notary=notary.<각자자신의도메인>  # 37줄<
expose.ingress.hosts.core=harbor.bs-yang.com
expose.ingress.hosts.notary=notary.bs-yang.com
expose.ingress.controller=alb                      # 44줄
expose.ingress.className=alb                       # 47줄~
expose.ingress.annotations=alb.ingress.kubernetes.io/scheme: internet-facing
expose.ingress.annotations=alb.ingress.kubernetes.io/target-type: ip
expose.ingress.annotations=alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
expose.ingress.annotations=alb.ingress.kubernetes.io/certificate-arn: ${CERT_ARN}   # 각자 자신의 값으로 수정입력
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
alb.ingress.kubernetes.io/certificate-arn: ${CERT_ARN}   # 각자 자신의 값으로 수정입력
externalURL=https://harbor.<각자자신의도메인>          # 131줄
externalURL=https://harbor.bs-yang.com             
----------------------
kubectl create ns harbor

helm install harbor harbor/harbor -f ~/harbor/values.yaml --namespace harbor --version 1.11.0

Harbor을 설치하고 모니터링 명령어 등을 통해 확인한다.

watch kubectl get pod,pvc,ingress -n harbor

helm list -n harbor
kubectl get-all -n harbor
kubectl get pod,pvc,ingress,deploy,sts -n harbor
kubectl get ingress -n harbor harbor-ingress -o json | jq
kubectl krew install df-pv && kubectl df-pv

<em># 웹 접속 주소 확인 및 접속</em>
echo -e "harbor URL = https://harbor.$KOPS_CLUSTER_NAME"

Harbor URL을 접속해서 제대로 사이트가 호출되는지 확인한다.

위와 같이 Harbor가 잘 설치 된 것을 확인할 수 있다.

로그인 후 New Project로 Project을 생성한다.

프로젝트 생성 후 컨테이너 이미지에 Tag 설정을 한 뒤 Harbor Project에 업로드를 한다.

<em># 컨테이너 이미지 가져오기</em>
docker pull nginx && docker pull busybox && docker images

<em># 태그 설정</em>
docker tag busybox harbor.$KOPS_CLUSTER_NAME/pkos/busybox:0.1
docker image ls

<em># 로그인 - 비밀번호는 미리 Harbor Portal에서 변경을 한다. 아래는 기본값을 바탕으로 한다.</em>
echo 'Harbor12345' > harborpw.txt
cat harborpw.txt | docker login harbor.$KOPS_CLUSTER_NAME -u admin --password-stdin
cat /root/.docker/config.json | jq

<em># 이미지 업로드</em>
docker push harbor.$KOPS_CLUSTER_NAME/pkos/busybox:0.1

이미지가 잘 업로드 된 것을 확인할 수 있다.

업로드 된 이미지로 Deployment을 생성하는 과정을 테스트해본다.

<em># 파드 배포</em>
curl -s -O https://raw.githubusercontent.com/junghoon2/kube-books/main/ch13/busybox-deploy.yml
sed -i "s|harbor.myweb.io/erp|harbor.$KOPS_CLUSTER_NAME/pkos|g" busybox-deploy.yml
kubectl apply -f busybox-deploy.yml

샘플 yaml을 받은 뒤 이미지 위치를 내 Harbor Project 저장소로 선택한다. 이렇게 하면 Pods을 배포할 때 위에서 설정한 이미지 저장소를 사용하게 된다. Pulling/Pulled을 참고하면 내 주소를 사용함을 알 수 있다.

업로드 된 이미지를 스캔하고 앞으로 업로드 될 이미지를 자동으로 스캔하게 하는 설정을 진행한다.

이미지를 선택 후 SCAN을 클릭한다. SCAN이 아직 진행되지 않았을 때는 Vulnerabilities에 Not Scanned로 표시 된다.

Scan이 완료 된 뒤에 문제가 없을 경우 No vulnerability로 표기됨을 알 수 있다.

아래는 앞으로 업로드(Push) 될 이미지들을 자동으로 Scan하는 방법이다.

Project을 선택한 뒤 Configuration을 선택하고 Automatically scan images on push을 체크하고 화면 하단의 SAVE을 클릭한다.

2. GitLab을 통해 Local Git 소스 저장소 구축

GitLab : Git Repo을 내부에서 관리할 수 있는 서비스이다. Private Github이라고 생각하면 편리하다.

이번 실습은 생성한 파일을 GitLab Repo에 업로드하는 것을 목표로 한다.

<em># 모니터링</em>
kubectl create ns gitlab
watch kubectl get pod,pvc,ingress -n gitlab

<em># 설치</em>
echo $CERT_ARN
helm repo add gitlab https://charts.gitlab.io/
helm repo update
helm fetch gitlab/gitlab --untar --version 6.8.1
vim ~/gitlab/values.yaml
----------------------
global:
  hosts:
    domain: <각자자신의도메인>             <em># 52줄</em>
    https: true

  ingress:                             <em># 66줄~</em>
    configureCertmanager: false
    provider: aws
    class: alb
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/certificate-arn: ${CERT_ARN}   <em># 각자 자신의 값으로 수정입력</em>
      alb.ingress.kubernetes.io/success-codes: 200-399
      alb.ingress.kubernetes.io/group.name: "gitlab" <em># 이렇게 할 경우 4개의 Ingress을 하나의 ALB로 생성 가능</em>
    tls:                               <em># 79줄</em>
      enabled: false
----------------------
helm install gitlab gitlab/gitlab -f ~/gitlab/values.yaml --set certmanager.install=false --set nginx-ingress.enabled=false --set prometheus.install=false --set gitlab-runner.install=false --namespace gitlab --version 6.8.4

위 내용을 사용하여 설치를 진행하고 아래와 같이 확인을 진행한다.

<em># 확인 - SubCharts</em>
<em># gitlab-gitaly : 웹서비스 혹은 ssh 방식으로 진행되는 깃 제목, 브랜치, 태그 등의 깃 요청 등에 대한 작업을 담당</em>
<em># gitlab-gitlab-shell : https 가 아닌 ssh 방식으로 깃 명령어 실행 시 해당 요청을 처리</em>
<em># gitlab-kas : gitlab agent server</em>
<em># gitlab-postgresql : 유저, 권한, 이슈 등 깃랩의 메타 데이터 정보가 저장</em>
<em># gitlab-redis-master : 깃랩 작업 정보는 레디스 캐시 서버를 이용하여 처리</em>
<em># gitlab-sidekiq-all-in-1-v2 : 레디스와 연동하여 작업 큐 처리 용도로 사용</em>
<em># gitlab-webservice-default : 깃랩 웹 서비스를 처리</em>
helm list -n gitlab
kubectl get pod,pvc,ingress,deploy,sts -n gitlab
kubectl df-pv -n gitlab
kubectl get-all -n gitlab

pod, pvc. ingress들이 정상적으로 생성 된 것을 확인 할 수 있다. 실습에 사용한 yaml 파일의 내용은 4개의 ingress을 하나의 ALB로 묶는 내용이었기 때문에 gitlab 관련 ALB는 하나만 생성 된 것을 볼 수 있다.

<em># 웹 root 계정 암호 확인</em>
kubectl get secrets -n gitlab gitlab-gitlab-initial-root-password --template={{.data.password}} | base64 -d ;echo

위 명령어로 root passwd을 확인한 뒤 alb와 연결 된 도메인 주소를 통해 gitlab에 접속을 할 수 있었다.

root로 로그인 후 간편하게 로그인 하기 위해 별도의 계정을 생성한 후 Admin권한을 부여한 뒤 해당 계정으로 로그인하였다. 앞으로 Gitlab 관련 된 작업은 해당 계정으로 진행할 예정이다. 해당 계정은 패스워드도 있지만 토큰 값도 생성되어 있는 상태로 터미널에서 계정에 로그인 할 때는 토큰값을 사용한다.

Gitlab에 코드를 업로드/다운로드 테스트하기 위해 프로젝트를 생성하였다.

<em>#</em>
mkdir ~/gitlab-test && cd ~/gitlab-test

<em># git 계정 초기화 : 토큰 및 로그인 실패 시 매번 실행해주자</em>
git config --system --unset credential.helper
git config --global --unset credential.helper

<em># git 계정 정보 확인 및 global 계정 정보 입력</em>
git config --list
git config --global user.name "userid"
git config --global user.email "user email"

<em># git clone/로그인 시 토큰 값을 사용</em>
git clone https://gitlab.$KOPS_CLUSTER_NAME/<각자 자신의 Gitlab 계정>/test-stg.git

터미널에서 gitlab 계정 정보를 입력해주고 git clone까지 시도했을 때 문제없이 정상적으로 성공했다.

이때 로그인에는 일반 패스워드가 아닌 계정 생성 때 같이 만든 Token값을 활용하였다.

로그인까지 정상적으로 됐으니 이제는 Local (kOps EC2)에 파일을 만들고 해당 파일을 push해보는 과정을 진행한다.

<em># 파일 생성 및 깃 업로드(push) : 웹에서 확인</em>
echo "gitlab test memo" >> test.txt
git add . && git commit -m "initial commit - add test.txt"
git push

위와 같이 test.txt가 정상적으로 push가 된 것을 확인 할 수 있다. 아래는 gitlab에 직접 접속해서 push 된 파일을 확인한 화면이다.

터미널에서 push한 파일이 정상적으로 gitlab site에서도 보이는 것을 확인 할 수 있다.

3. ArgoCD를 활용한 깃옵스(GitOps) 시스템 구축

Harbor을 통해 컨테이너 이미지 저장소를 구성했고 Gitlab으로 코드 저장소를 구성했다면 이제 ArgoCD을 통해 GitOps 시스템을 구축할 계획이다.

<em># 모니터링</em>
kubectl create ns argocd
watch kubectl get pod,pvc,svc -n argocd

<em># 설치</em>
cd
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
helm install argocd argo/argo-cd --set server.service.type=LoadBalancer --namespace argocd --version 5.19.14

<em># 확인</em>
<em># argocd-application-controller : 실행 중인 k8s 애플리케이션의 설정과 깃 저장소의 소스 파일에 선언된 상태를 서로 비교하는 컨트롤러. 상태와 다르면 ‘OutOfSync’ 에러를 출력.</em>
<em># argocd-dex-server : 외부 사용자의 LDAP 인증에 Dex 서버를 사용할 수 있음</em>
<em># argocd-repo-server : 원격 깃 저장소의 소스 코드를 아르고시디 내부 캐시 서버에 저장합니다. 디렉토리 경로, 소스, 헬름 차트 등이 저장.</em>
helm list -n argocd
kubectl get pod,pvc,svc,deploy,sts -n argocd
kubectl get-all -n argocd

kubectl get crd | grep argoproj

<em># CLB에 ExternanDNS 로 도메인 연결</em>
kubectl annotate service -n argocd argocd-server "external-dns.alpha.kubernetes.io/hostname=argocd.$KOPS_CLUSTER_NAME"

<em># admin 계정의 암호 확인</em>
ARGOPW=$(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d)
echo $ARGOPW

<em># 웹 접속 로그인 (admin) CLB의 DNS 주소로 접속</em>
echo -e "Argocd Web URL = https://argocd.$KOPS_CLUSTER_NAME"

helm chart를 통해 설치하고 생성 된 CLB을 내 도메인에 연결하는 과정을 거친다.

위에서 확인한 admin 계정의 암호를 통해 로그인까지 성공할 수 있었다. 이상하게 argocd를 등록한 레코드만 실습 환경인 맥북에서 접속되지 않아 dns flush를 강제로 진행하고서야 접속할 수 있었다.

로그인을 진행했으니 앞서 생성한 Gitlab과 k8s cluster 등록을 위해 ArgocdCLI 도구를 설치한다.

<em># 최신버전 설치</em>
curl -sSL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
install -m 555 argocd-linux-amd64 /usr/local/bin/argocd
chmod +x /usr/local/bin/argocd

<em># 버전 확인</em>
argocd version --short

<em># Help</em>
<em># argocd app : 쿠버네티스 애플리케이션 동기화 상태 확인</em>
<em># argocd context : 복수의 쿠버네티스 클러스터 등록 및 선택</em>
<em># argocd login : 아르고시디 서버에 로그인 </em>
<em># argocd repo : 원격 깃 저장소를 등록하고 현황 파악</em>
argocd

<em># argocd 서버 로그인</em>
argocd login argocd.$KOPS_CLUSTER_NAME --username admin --password $ARGOPW

<em># 기 설치한 깃랩의 프로젝트 URL 을 argocd 깃 리포지토리(argocd repo)로 등록. 깃랩은 프로젝트 단위로 소스 코드를 보관.</em>
argocd repo add https://gitlab.$KOPS_CLUSTER_NAME/<깃랩 계정명>/test-stg.git --username <깃랩 계정명> --password <깃랩 계정 암호>

Git repo와 kubernetes cluster 정보가 정상적으로 출력되는 것을 확인할 수 있다.

* 현재 출력되는 Cluster는 Argocd가 설치 된 k8s cluster로 이는 자동으로 등록된다. 나는 별도의 cluster를 나눠서 진행하고 있지 않기 때문에 상관 없으나 Cluster을 나눠서 진행할 경우 별도로 등록을 해줘야 한다.

Git repo가 등록됐으니 해당 repo을 사용해서 RabbitMQ Application을 배포해본다.

<em># test-stg 깃 디렉터리에서 아래 실행</em>
cd ~/gitlab-test/test-stg

<em># 깃 원격 오리진 주소 확인</em>
git config -l | grep remote.origin.url

<em># RabbitMQ 헬름 차트 설치</em>
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm fetch bitnami/rabbitmq --untar --version 11.10.3
cd rabbitmq/
cp values.yaml my-values.yaml

<em># 헬름 차트를 깃랩 저장소에 업로드</em>
git add . && git commit -m "add rabbitmq helm"
git push

<em># 수정</em>
cd ~/
curl -s -O https://raw.githubusercontent.com/wikibook/kubepractice/main/ch15/rabbitmq-helm-argo-application.yml
vim rabbitmq-helm-argo-application.yml
--------------------------------------
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: rabbitmq-helm
  namespace: argocd
  finalizers:
  - resources-finalizer.argocd.argoproj.io
spec:
  destination:
    namespace: rabbitmq
    server: https://kubernetes.default.svc
  project: default
  source:
    repoURL: https://gitlab.myurl.com/path/xxx.git
    path: rabbitmq
    targetRevision: HEAD
    helm:
      valueFiles:
      - my-values.yaml
  syncPolicy:
    syncOptions:
    - CreateNamespace=true
--------------------------------------

<em># 모니터링 : argocd 웹 화면 보고 있기!</em>
echo -e "Argocd Web URL = https://argocd.$KOPS_CLUSTER_NAME"

<em># 배포</em>
kubectl apply -f rabbitmq-helm-argo-application.yml

<em># YAML 파일을 적용(apply)하여 아르고시디 ‘Application’ CRD를 생성</em>
kubectl get applications.argoproj.io -n argocd
NAME            SYNC STATUS   HEALTH STATUS
rabbitmq-helm   OutOfSync     Missing

배포 명령어를 입력하자마자 아래와 같이 ArgoCD 화면이 변경되는 것을 확인할 수 있었다.

 현재는 Missing/OutOfSync 상태이기 때문에 RabbitMQ Application을 들어가서 Sync을 클릭하여 동기화를 진행해준다. 이렇게 할 경우 별도의 helm install 명령어는 진행하지 않아도 된다.

동기화가 완료되면 아래 화면과 같이 Missing은 Healthy로 OutOfSync는 Synced로 변경된 것을 확인할 수 있다.

추가로 svc, ep 등이 확장되었음도 확인할 수 있다.

위 화면의 pod는 현재 1개인데 이를 2개로 확장하는 명령어를 입력해본 뒤 화면이 어떻게 변화하는지 확인해봤다.

명령어를 입력하자마자 ArgoCD 화면에 pod가 1개 추가 늘어나는 것을 볼 수 있었다.

물론 이 내용은 변경 된 부분이기 대문에 Sync는 다시 OutOfSync가 되니 다시 동기화를 진행해주어야 Synced 상태로 바꿀 수 있다.

여기까지 간단한게 ArgoCD을 활용한 GitOps 실습을 진행해보았다.

4. 마무리

이번 실습은 설치해야 하는 것들이 많아 시간이 오래 걸렸다. 지난 시간은 이론적으로 이해해야 하는 부분이 많았지만 실습 자체는 난이도가 높지 않았는데 이번에는 ingress 배포에서 조금 좌절을 하는 시간도 있었다. 제공 된 yaml을 수정하는 중에 놓치고 넘어간 부분이 있거나 잘못 수정한 부분이 있어 ingress가 배포되지 않아 로그를 확인해보는 등 시간을 소모해야 했다. 덕분에 실습을 조금 더 깊이 있게 들여다볼 수 있었고 harbor, gitops, ArgoCD을 사용한 GitOps 환경 구축 실습이 더 흥미로웠던 것 같다.

앞으로 프로젝트를 진행할 때 GitOps을 빼놓을 수 없을 것 같은데 이번에 미리 학습한 덕분에 실무에서도 잘 적용할 수 있을 것 같다는 생각을 하게 됐다.

다음 목표는 이번에 구성한 환경을 바탕으로 Application을 제작/수정/배포하는 내용을 진행해보고 싶다. 언젠가는 간단하지만 나만의 Application을 만들고 싶었는데 그 과정에 오늘 실습한 GitOps을 포함하면 더 수월하고 의미있는 프로젝트가 될 것 같다.

PKOS Study #2-2 – Kubernetes Network&Storage

PKOS Study #2-1에서 Kubernetes Network에 대한 기본적인 내용을 정리했다.

이번 2-2에서는 지난 문서에서 정리하지 못한 Loadbalancer에 대한 내용과 Storage 파트를 정리할 계획이다.

1. Pods 생성 수량 제한

Worker Node의 EC2 Instance Type 별로 Pods 생성 수량은 제한 된다. Pods 생성 수량 기준은 인스턴스에 연결 가능한 ENI 숫자와 할당 가능한 IP 수에 따라 결정되게 된다.

* aws-node, kube-proxy Pods는 Host의 IP을 같이 사용하기 때문에 배포 가능 최대 수량에서 제외 된다. (IP 조건에서 제외되기 때문에)

Pods 최대 생성 수량 계산 : {Instance에 연결 가능한 최대 ENI 수량 x (ENI에 할당 가능한 IP 수량 – 1)} + 2

위 식을 계산하기 위해서는 내가 사용하는 Node Instance Type의 ENI 및 IP 할당 제한을 확인할 필요가 있는데 그럴 때는 아래와 같이 확인 할 수 있다.

Values=t3.* 부분을 확인하고 싶은 Instance Type으로 변경하면 된다.

aws ec2 describe-instance-types --filters Name=instance-type,Values=t3.* \
 --query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
 --output table

내가 사용하는 Instance Type은 t3.medium이고 해당 Instance의 경우 연결 가능한 ENI는 3개이고 ENI당 할당 가능한 IP는 6개이다. 위 계산식에 대입하면 {3 x (6-1)} + 2가 되고 aws-node, kube-proxy 포함하여 총 17개의 Pods를 생성할 수 있게 된다.

Pods 생성 수량(Replicas)을 늘리면서 Worker Node의 ENI 정보와 IP 주소에 어떤 변화가 있는지 확인해보겠다.

우선 어떠한 Pods도 배포하지 않았을 때 Worker Node 1, 2의 ENI 상태 화면이다. (위가 1, 밑이 2)

Replicas 수량이 2개인 Deployment을 생성했을 때 ENI와 IP 모습이다.

Pods가 2개 생성되어 각각 IP을 할당 받은 것을 확인 할 수 있다. 각 Pods는 서로 다른 Node에 배포 되었다는 것을 IP을 통해 확인 할 수 있다.

이번엔 Replicas을 8로 증가하여 보았다.

Pods가 8개로 증가하며 ENI 정보와 IP가 각각 증가한 것을 알 수 있다. 현재 Worker Node는 t3.medium으로 앞서 계산한 공식에 따르면 Pods을 15개까지 생성할 수 있다. Worker Node가 2개이니 30개까지 생성이 가능한지 확인해봤다.

30개로 증가하는 명령어 이후 Running 중인 Pods 수량을 확인하면 21개만 생성 된 것을 알 수 있다. 왜 30개가 아닌 21개인지 궁금하여 현재 Node에서 실행 중인 Pods들을 모두 조회하고 해당 Pods들이 사용 중인 IP들을 확인해보았다.

kubectl get pod --all-namespaces -o wide

현재 Deployment가 생성 된 namespace가 아닌 kube-system namespace에서 사용 중인 Pods들까지 확인할 수 있었다.

Node 1은 이미 5개의 IP을 할당 받아 사용 중이었고 Node 2는 4개의 IP을 할당 받아 사용 중이었다. 총 할당 가능한 15개의 IP 중 이미 4, 5개의 IP을 기존 시스템에서 사용 중인 상태라 30개의 Pods 중 9개가 부족한 21개의 Pods만 정상적으로 배포가 된 것을 확인할 수 있었다.

그럼 EC2 Instance Type에 종속되는 Pods 배포 수량을 극복할 수 있는 방안은 없을까? 아니다 Prefix Delegation 방식을 사용하면 Instance Type에 한정되지 않고 Pods을 배포할 수 있다.

자세한 내용은 아래 링크를 참고하면 된다.Amazon EC2 노드에 사용 가능한 IP 주소의 양 늘리기 – Amazon EKS관리형 노드 그룹은 maxPods의 값에 최대 수를 적용합니다. vCPU가 30개 미만인 인스턴스의 경우 최대 수는 110이고 다른 모든 인스턴스의 경우 최대 수는 250입니다. 이 최대 수는 접두사 위임의 활성docs.aws.amazon.com

나는 t3.medium Type을 Worker Node에 사용했고 해당 Node의 Allocatable을 확인하는 명령어를 통해 가용 가능 Pods을 확인해보았다. 앞서 계산한 바와 같이 17개임을 확인 할 수 있다.

50개의 Pods을 배포하는 명령어를 실행했을 때 21개만 배포가 되어있는 것을 확인할 수 있다. 이는 앞서 배포했을 때 확인할 수 있었던 수치와 동일하다.

설정값을 바꾸기 위해 kops edit cluster을 통해 내용을 수정하고 Rolling Update을 실행했다.

kops edit cluster
<em>#maxPods 수정</em>
  kubelet:
    anonymousAuth: false
    maxPods: 50
<em>#ENABLE_PREFIX_DELEGATION 수정</em>
  networking:
    amazonvpc:
      env:
      - name: ENABLE_PREFIX_DELEGATION
        value: "true"

<em>#Rolling Update 진행, 해당 과정에서 Node들의 IP 변경 발생하고 약 15~20분 정도 소요 된다.</em>
kops update cluster --yes && echo && sleep 5 && kops rolling-update cluster --yes

Rolling Update 이후 배포 가능 Pods 확인을 진행하였다.

그 후 다시 Replicas을 50으로 수정하여 배포를 진행하였다.

50개가 모두 실행 중인 것을 확인할 수 있었다. ENI 정보와 IP 정보를 조회했을 때도 해당 수량이 증가한 것을 알 수 있었다.

나는 실습 전에 CPU Limits을 제거하고 진행했기 때문에 원할하게 Pods들이 증가했으나 Network 제한만 푼다고 해서 Pods들이 많이 생성되지 않을 수 있다. 그럴 때는 아래와 같은 방법으로 CPU Limits을 해제하면 된다.

kubectl describe limitranges <em># LimitRanges 기본 정책 확인 : 컨테이너는 기본적으로 0.1CPU(=100m vcpu)를 최소 보장(개런티)</em>
kubectl delete limitranges limits
kubectl get limitranges

위 방법으로 CPU 제한을 해제하고 배포를 진행해보면 더 많은 수의 Pods가 배포되는 것을 확인할 수 있다.

2. LoadBalancer Controller with NLB

Kubernetes의 Service에는 ClusterIP, NodePort, Loadbalancer Type이 있다.

ClusterIP : Control Plane의 iptables을 이용하여 내부에서만 접근 가능한 방식으로 외부에서의 접근은 불가능하다.

NodePort : 위 ClusterIP는 외부에서 접근이 불가능하기 때문에 외부에서 접근 가능하게 하기 위해서 Worker Node의 Port을 맵핑하여 통신할 수 있도록 한다.

Loadbalancer : NodePort 방식과 마찬가지로 외부에서 접근할 수 있는 방식이다. 다만, Worker Node의 Port을 통해 접근하는 방식이 아닌 앞단에 위치한 LoadBalancer을 통해 접근하게 된다.

Loadbalancer Controller : Public Cloud을 사용할 경우 각 CSP에서 제공하는 Loadbalancer을 사용하기 위해 설치하는 Controller이다. 이를 통해 CSP의 LB을 제어할 수 있게 된다.

나는 one-click kOps yaml을 사용하여 실습을 진행했기 때문에 그 과정에 IAM Policy 생성 및 Role Attach까지 같이 마무리를 하였다. 혹시 그 내용이 누락되어 있다면 해당 내용을 추가로 진행해야 한다. (특히 Rolling Update 등으로 IAM Profile이 빠져있을 수 있다.)

NLB가 같이 포함 된 yaml을 사용하여 Deployment 생성을 진행했다.

그 후 정보를 확인하여 NLB 주소를 확인할 수 있었다.

NLB 주소를 Client에서 접속 시도하여 정상적으로 페이지가 호출되는 것까지 확인하였고 해당 NLB 주소는 내 도메인에 연결 및 테스트까지 완료하였다.

내 도메인 레코드와 NLB을 통해 정상적으로 분산 되는지를 테스트하였고 이상 없이 분산 되는 것을 확인했다.

NLB=$(kubectl get svc svc-nlb-ip-type -o jsonpath={.status.loadBalancer.ingress[0].hostname})
curl -s $NLB
for i in {1..100}; do curl -s $NLB | grep Hostname ; done | sort | uniq -c | sort -nr

3. Kubernetes Storage

Kubernetes Storage : hostPath, emptyDir, PV/PVC 등이 있고 이번 실습에는 PV/PVC을 진행할 예정이다.

hostPath : Pods가 배포 된 Worker Node(Host)의 Directory Path을 Pod에 Mount하여 사용하는 방법으로 Host Directory Path에 Data을 저장하기 때문에 Pods가 삭제되더라도 Data는 유지시킬 수 있다.

emptyDir : Pod 내부에 존재하고 휘발성을 뛰고 있다. hostPath와 다르게 Pod을 삭제하면 Data도 삭제되기 때문에 이 점은 유의해야 한다.

PV/PVC : hostPath와 언뜻 비슷해보이지만 hostPath는 Pods가 배포 된 Worker Node의 공간을 사용한다고 봤을 때 PV/PVC는 공유 공간이라고 보는 게 좋다. 동일한 Worker Node가 아니더라도 공간을 공유할 수 있으며 이를 위해 AWS에서는 EBS, EFS 등을 사용한다.

EBS을 생성하고 추가 된 EBS 볼륨을 확인하는 내용으로 PV/PVC 실습을 진행한다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ebs-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  terminationGracePeriodSeconds: 3
  containers:
  - name: app
    image: centos
    command: ["/bin/sh"]
    args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"]
    volumeMounts:
    - name: persistent-storage
      mountPath: /data
  volumes:
  - name: persistent-storage
    persistentVolumeClaim:
      claimName: ebs-claim

위 내용으로 PVC 및 Pods을 생성한다. 4GB의 용량에 RW Access가 설정 된 EBS을 생성한 후 Pods에 해당 EBS을 Mount하는 내용이다. 생성 된 Pods의 /data 경로를 보면 해당 EBS가 마운트되어 있어야 한다. 아래 방법으로 해당 내용을 확인한다.

/data/out.txt 파일을 호출하고 app Pod의 Filesystem 정보를 조회한 내용이다. 제대로 Mount되었음을 확인할 수 있다.

4. 마무리

지난 실습 때 Pods 생성 제한이나 NLB 등을 제대로 이해하지 못해 추가로 진행하였는데 늦게라도 해보길 잘했다는 생각이 들었다. Storage 부분은 특이한 내용은 없었고 기존 컨테이너를 공부할 때 배웠던 내용들이 같이 들어있어서 이해하는데 도움이 됐다.

Network 부분이 계속 어려웠었는데 이번 기회가 기술 성숙도를 올릴 수 있는 기회가 되길 바란다.

PKOS Study #2-1 – Kubernetes Network

PKOS Study #1 kOps에 이어 이번 #2에는 Kubernetes Network에 대해 정리할 예정이다.

사실 eks나 kOps나 배포 자체는 어렵지 않고 Docker Image을 통해 Container을 배포하는 것도 조금만 찾아보면 쉽게 따라 할 수 있다.

하지만 Network는 이해하는 영역이나 컨트롤 하는 부분이 어렵고 이 부분에 대해 자세히 학습할 수 있는 기회가 많지 않다.

이번 스터디 때는 그 부분에 대해 상세하게 설명을 들을 수 있었고 실습을 통해 이해의 영역이 조금 더 넓어진 것 같다.

0. 환경 구성

기존에 직접 kOps EC2를 배포해서 설정한 뒤에 kOps 배포를 진행했지만 이번 실습부터는 간편하게 설정 된 one-click kOps 배포 yaml을 사용하여 진행할 예정이다.

one-click yaml로 환경 배포 후 IAM 정책을 생성하고 EC2 Instance Profile에 Attach 한다.

IAM Policy은 https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.4.5/docs/install/iam_policy.json 파일을 통해 진행했다.

1. AWS VPC CNI?

CNI : Container Network Interface로 컨테이너 간의 네트워킹 제어 플러그인을 위한 표준이다. k8s에서는 Pod간의 네트워킹을 위해 CNI을 사용한다. kubenet이라는 자체 k8s CNI 플러그인이 있지만 기능이 부족하여 Calico, Weave 등을 사용하기도 한다.

Amazon VPC CNI : Node에 VPC IP Address을 할당하고 각 Node의 Pods에 대한 필수 네트워킹을 구성하는 역할을 한다.

특징으로는 Node와 Pods의 IP Range가 동일하게 Node<->Pods 통신이 가능하고 VPC Flow logs, Routing Table, Security Group 등을 지원한다.

VPC 연계되는 리소스들을 지원하기 때문에 AWS 인프라를 관리하는 것과 비슷한 방식으로 네트워크 환경을 관리할 수 있는 이점이 있다.

Node와 Pods의 IP을 확인하는 명령어를 입력하면 아래와 같이 Node와 Pods의 IP가 동일한 대역(172.30.*.*)을 사용하는 것을 확인할 수 있다.

# 노드 IP 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table

# 파드 IP 확인
kubectl get pod -n kube-system -o=custom-columns=NAME:.metadata.name,IP:.status.podIP,STATUS:.status.phase

2. Pods 간 통신 확인

Master Node와 Worker Node의 보조 IPv4을 확인해보면 아래와 같이 확인 할 수 있다.

Worker Node #1과 #2에 각각 Pod을 생성 후 두 Pods의 통신을 확인해볼 계획이다.

우선 테스트 Pods을 생성한 뒤 생성 된 Pods의 IP을 확인한다.

# 테스트용 파드 netshoot-pod 생성
cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: netshoot-pod
spec:
  replicas: 2
  selector:
    matchLabels:
      app: netshoot-pod
  template:
    metadata:
      labels:
        app: netshoot-pod
    spec:
      containers:
      - name: netshoot-pod
        image: nicolaka/netshoot
        command: ["tail"]
        args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
EOF

# 파드 이름 변수 지정
PODNAME1=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[0].metadata.name})
PODNAME2=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[1].metadata.name})

# 파드 확인
kubectl get pod -o wide
kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP

Pod #1에서 Pod #2로 Ping을 시도했을 때 정상적으로 Ping이 가는 것을 확인 할 수 있다.

만약 Ping이 제대로 가지 않는다면 Routing Table 정보가 업데이트 되었는지 확인해봐야 한다. (기본적으로는 자동으로 업데이트가 된다.)

Worker Node에서 ip route 정보를 확인해보면 Pods 간 통신시에는 Overlay을 사용하지 않고 Pods간 직접 통신을 하는 것을 확인 할 수 있다. 여기서 Overlay을 사용한다는 뜻은 가상의 네트워크를 만들어서 해당 네트워크를 통해 통신하게 한다는 의미이다. Amazon VPC CNI을 사용하면 해당 기술을 사용하지 않고도 직접적으로 Pods간 통신이 가능하게 된다.

3. Pods -> 외부 통신 확인

2번에서 Pods간 통신을 확인했다면 이번에는 Pods -> 외부 통신을 확인하려 한다.

외부 통신을 할 때에는 Pods 간 통신과는 다르게 Node의 IP(eth0)로 변경되어 외부로 통신을 나가게 된다.

위 흐름에 따라 통신이 된다면 Pod에서 외부 통신을 수행할 경우 소속 된 Worker Node의 Public IP을 통해 통신을 하게 된다.

아래 켑쳐 화면을 보면 Worker Node의 Public IP인 3.36.113.87을 통해 Pod가 외부 통신을 하는 것을 확인할 수 있다.

물론 이는 기본적인 Amazon VPC CNI의 SNAT Rule에 따른 것이고 SNAT Rule을 변경하게 되면 이 부분은 변경 될 수 있다.

SNAT Rule을 확인한 화면은 아래와 같다. 

Worker Node에 접속하여 확인한 화면이고 Source NAT IP을 172.30.53.166을 할당한다는 뜻이다. 해당 IP에 Attach된 Public IP는 외부 통신 확인하는 사진에서 볼 수 있듯이 172.30.53.166->3.36.113.87임을 알 수 있다. 내부에서 보기에는 Public IP을 바라보는 것이 아닌 Private IP인 172.30 대역을 바라보고 이후 Attach 된 Public IP(3.36.*.*)으로 외부 통신 됨을 확인할 수 있다.

4. 마무리

Node와 Pods 배포 및 내/외부 통신을 확인하는 과정을 진행하였다.

본 과제에는 정리하지 못하였으나 Loadbalancer와 Ingress 도 실습을 진행해보았다. 다만, 내용을 아직 이해하지 못했고 단순 따라하기 정도에 그친 부분이 있어 이 부분은 조금 더 실습을 진행하고 내가 이해한 부분을 정리할 수 있도록 해야겠다.

CNI에 대해 완벽하게 내용을 이해하지는 못하였지만 Pods간 통신과 Pods->외부 통신에 대한 원리를 이해할 수 있었던 게 이번 과정에서 제일 의미 있던 부분이라고 생각 된다.

PKOS Study #1 – kOps

지난 Terraform Study에 이어 이번엔 Kubernetets Study에 참여하게 됐다.

그간 실무에 k8s을 사용할 일이 없었어서 이번 기회를 통해 k8s와 가까워지면 좋겠다고 생각했다.

Terraform Study 때 배운 내용을 따로 사용해보기도 하면서 많은 도움이 됐었는데 이번에도 스터디로 만족하지 않고 실무에 적용할 수 있는 기회가 오면 좋을 것 같다.

단순 k8s을 설치하고 관리하는 것에 중점을 둔 스터디가 아니라 kOps와 같은 Operation Tool과 네트워크, 스토리지 등에 대한 내용이 담겨있어 부족한 기본기와 더불어 확장성 있는 내용까지 학습할 수 있을 것으로 기대한다.

첫 스터디는 kOps 설치에 대해 다루고 있다.

kOps bastion 역할을 하는 EC2를 배포하고 그 뒤에 kOps 설치 스크립트를 통해 kOps을 설치하는데 그 과정에 있어 route53에 등록 된 DNS Hosted Zone을 사용하게 된다. 마침 최근 등록한 .com 도메인이 있어 해당 도메인을 통해 진행할 예정이다. (네임서버는 스터디를 위해 최근 route53으로 변경 완료하였다.)

0. kOps?

kOps는 AWS와 같은 Cloud Platform에서 k8s을 쉽게 설치하기 위해 도와주는 도구이다.

주요 기능으로는,

고가용성 쿠버네티스 클러스터의 프로비저닝 자동화

테라폼 생성 능력

명령줄 자동 완성

YAML Manifast 기반 API 구성 등이 있다.

이번 스터디를 통해 사용해보니 나같이 k8s가 익숙하지 않은 사람도 쉽고 간편하게 k8s을 설치할 수 있었다.

kOps에 대한 자세한 설명은 하기 링크로 대신한다.

https://kops.sigs.k8s.io/

1. kOps 설치

kOps 역할을 할  EC2를 배포하고 해당 EC2에서 kOps 설치를 진행한다.

사전 준비 내용은 Study 자료 중 제공 되는 yaml 파일을 따로 확인해서 yaml file 없이 별도로 준비를 진행해봤다.

kOps EC2는 Amazon Linux2을 사용하고 kOps가 배포되는 VPC와는 별도의 VPC를 생성했다.

awscli와 jq, git 등을 사전 설치하고 yaml 편집 편의를 위해 Yaml Highlighter도 설치해준다.

그리고 제일 중요한 kubectl, kOps 그리고 helm의 설치도 같이 진행한다.

* 이 부분부터 진행되는 모든 내용은 kOps EC2에서 root 로그인을 진행한 후 진행한다.

kOps의 설치는 아래 코드를 통해 설치한다.

cd /root
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
curl -Lo kops https://github.com/kubernetes/kops/releases/download/$(curl -s https://api.github.com/repos/kubernetes/kops/releases/latest | grep tag_name | cut -d '"' -f 4)/kops-linux-amd64
chmod +x kops
mv kops /usr/local/bin/kops
curl -s https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash

kubectl, KOps 그리고 awsclit 등의 설치 결과를 확인해보았다.

2. kOps Cluster 배포

kOps가 잘 설치 된 것을 확인했으니 kOps을 통해 Cluster 배포를 진행한다.

Cluster 배포 전에 IAM User 생성 및 kOps EC2에 AWS Configure에 IAM User 정보 설정이 필요하다.

IAM User을 생성하고 Administrator Access 권한을 부여한 뒤 Access key을 생성하는 과정을 진행한다.

방법은 하단 코드블럭을 참고해 진행한다.

#iam user 생성
aws iam create-user --user-name "kopsadmin"

#생성 된 iam user에 administrator access 부여
aws iam attach-user-policy --user-name "kopsadmin" --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

#iam user access key, secret access key 생성
aws iam create-access-key --user-name "kopsadmin"

#aws configure 설정
aws configure
AWS Access Key ID [None]: ......
AWS Secret Access Key [None]: ......
Default region name [None]: ap-northeast-2
Default output format [None]: json

#필요 변수 설정
export KOPS_CLUSTER_NAME=bs-yang.com
export KOPS_STATE_STORE=s3://ybs-k8s-s3
export AWS_PAGER=""
export REGION=ap-northeast-2

#S3 생성도 이 단계에서 같이 진행한다.
aws s3 mb s3://ybs-k8s-s3 --region $REGION

Cluster 배포는 아래 코드를 통해 진행한다.

# 변수로 설정 된 값들을 통해 dry run으로 결과값을 yaml에 저장해 이상 유무를 확인한다.
kops create cluster --zones="$REGION"a,"$REGION"c --networking amazonvpc \
--cloud aws --master-size t3.medium --node-size t3.medium --node-count=2 \
--network-cidr 172.30.0.0/16 --ssh-public-key ~/.ssh/id_rsa.pub \
--name=$KOPS_CLUSTER_NAME --kubernetes-version "1.24.10" \
--dry-run -o yaml > mykops.yaml

#위 확인 이후 문제가 없다면 실제 배포를 진행한다.
kops create cluster --zones="$REGION"a,"$REGION"c --networking amazonvpc \
--cloud aws --master-size t3.medium --node-size t3.medium --node-count=2 \
--network-cidr 172.30.0.0/16 --ssh-public-key ~/.ssh/id_rsa.pub \
--name=$KOPS_CLUSTER_NAME --kubernetes-version "1.24.10" -y

일정 시간이 지나면 아래 화면과 같이 배포가 완료된 것을 알 수 있다.

3. kOps Cluster 배포 확인

배포가 모두 완료 되면 검증 명령어를 통해 배포 상황을 확인한다.

배포가 잘 됐는지 Instances 목록 및 Route53을 통해 확인한다.

DNS 정보도 제대로 등록되어 있는지 Route53을 통해 확인한다.

api.과 api.internal. 그리고 kops-controller.internal.이 생성된 것을 확인할 수 있다.

아래와 같이 Cluster 내용을 확인할 수 있다.

위 내용까지 진행하면서 오류가 발생하지 않는다면 kOps을 통해 Cluster 배포가 잘 진행됐다고 볼 수 있다.

추가로 Cluster 배포 때 설정한 S3 버킷을 확인하면 아래와 같은 목록들이 생성 된 것도 확인할 수 있다.

추후 Network, Storage 등을 확인하는 과정이 있어 이 부분이 기대 된다.

이번 회차 내용을 정리하는 것을 과제로 작성하면서 든 생각은 kOps 설치 및 Cluster 배포까지 한 번에 진행할 수 있는 설정이 추가 된 Terraform 코드를 작성해보면 어떨까 하는 생각을 해봤다. 시간은 좀 걸리겠지만 해당 내용을 진행할 수 있도록 도전해봐야겠다.

Terraform Study #Final Exam

길다고 생각했던 Terraform Study가 드디어 끝이 났다.

혼자 공부를 할 때 어려웠던 부분을 잘 쉽게 설명해주신 gasida님 덕분에 즐거운 스터디 기간이 됐던 것 같다.

오늘은 그 동안 배웠던 내용을 토대로 하나의 프로젝트를 생성해보려고 한다.

요새 AWS 환경에 테스트 중인 AWS Backup을 배포하여 사용해보려고 한다.

실습 단계는 아래와 같이 진행할 예정이다.

1. VPC, Subnet 생성

2. 2대의 EC2 생성 – AMI 백업 목적의 EC2와 EFS을 마운트 할 목적의 EC2 생성(EFS도 같이 생성)

3. RDS 생성

4. Backup vault, Plan 생성 – KMS Key도 같이 생성

5. EFS 마운트 확인

위와 같은 순서로 리소스들을 생성할 예정이다.

대부분의 코드는 기존 챕터 과제와 중간 과제 때 사용했던 코드를 가져와서 이용했다.

전체적인 구조는 리소스를 Modules의 정보를 가져와서 생성하게끔 되어있다.

PRD/DEV는 따로 분류하지 않았다.

1. VPC, Subnet 생성

코드는 아래 열기 버튼을 클릭하면 된다.

VPC와 서브넷을 포함한 네트워크 리소스들은 대부분 이전 프로젝트에서 따와서 작성했다.

서울 Region에 VPC을 생성하고 Zone A, C로 나눈 서브넷을 각각 생성했다.

SG의 경우 SSH을 위한 22, HTTP을 위한 80과 외부 통신을 하기 위해 Egress는 Any로 열어줬다. 

EFS 마운트를 위해 2049도 추가로 열어주었다.

Mysql RDS와 EC2간의 통신을 위한 포트도 추가해야 하는데 이는 아직 DB 데이터가 없어 추후 변경 시에 넣을 예정이다.

locals {
  http_port    = 80
  ssh_port     = 22
  any_port     = 0
  efs_port     = 2049
  any_protocol = "-1"
  tcp_protocol = "tcp"
  my_ip        = ["222.111.4.135/32"]
  all_ips      = ["0.0.0.0/0"]
  all_block    = "0.0.0.0/0"
}

resource "aws_vpc" "prj_vpc" {
  cidr_block       = var.vpc_cidr
  enable_dns_support = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.prj_name}-vpc"
  }
}

resource "aws_subnet" "prj_subnet1" {
  vpc_id     = aws_vpc.prj_vpc.id
  cidr_block = var.subnet1_cidr

  availability_zone = var.az_a

  tags = {
    Name = "${var.prj_name}-subnet1"
  }
}

resource "aws_subnet" "prj_subnet2" {
  vpc_id     = aws_vpc.prj_vpc.id
  cidr_block = var.subnet2_cidr

  availability_zone = var.az_c

  tags = {
    Name = "${var.prj_name}-subnet2"
  }
}

resource "aws_internet_gateway" "prj_igw" {
  vpc_id = aws_vpc.prj_vpc.id

  tags = {
    Name = "${var.prj_name}-igw"
  }
}


resource "aws_route_table" "prj_rt" {
  vpc_id = aws_vpc.prj_vpc.id

  tags = {
    Name = "${var.prj_name}-rt"
  }
}

resource "aws_route_table_association" "prj_asso1" {
  subnet_id      = aws_subnet.prj_subnet1.id
  route_table_id = aws_route_table.prj_rt.id
}

resource "aws_route_table_association" "prj_asso2" {
  subnet_id      = aws_subnet.prj_subnet2.id
  route_table_id = aws_route_table.prj_rt.id
}

resource "aws_route" "prj_dftrt" {
  route_table_id         = aws_route_table.prj_rt.id
  destination_cidr_block = local.all_block
  gateway_id             = aws_internet_gateway.prj_igw.id
}


resource "aws_security_group" "prj_sg" {
  name = "${var.prj_name}-sg"
  vpc_id      = aws_vpc.prj_vpc.id

  ingress {
    from_port   = local.http_port
    to_port     = local.http_port
    protocol    = local.tcp_protocol
    cidr_blocks = local.all_ips
  }

  ingress {
    from_port   = local.ssh_port
    to_port     = local.ssh_port
    protocol    = local.tcp_protocol
    cidr_blocks = local.my_ip
  }
  ingress {
description = "EFS mount target"
from_port   = local.efs_port
to_port     = local.efs_port
protocol    = local.tcp_protocol
cidr_blocks = local.all_ips
  }

  egress {
    from_port   = local.any_port
    to_port     = local.any_port
    protocol    = local.any_protocol
    cidr_blocks = local.all_ips
  }
  tags = {
    Name = "${var.prj_name}-sg"
  }
}

2. 2대의 EC2 생성

AMI 백업만을 진행할 목적의 EC2 1대와 EFS을 백업할 목적의 EC2를 1대 각각 배포한다.

Keypair는 스터디 진행 중인 맥북의 ssh key로 생성할 수 있게 하였다.

앞단에는 ALB을 두어 http 통신이 가능하게끔 했다.

이때 EFS도 같이 생성을 하고 자동 백업은 실행되지 않게끔 해둔 상태로 생성을 진행했다.

EFS 백업 목적의 EC2에는 user data에 EFS을 마운트하는 명령어도 추가하였다.

EFS 마운트는 해당 EC2의 서브넷에서만 가능하게 하였다.

코드는 아래에서 확인 가능하다.

<hide/>
locals {
  http_protocol = "HTTP"
  ec2_file_system_local_mount_path = "/mnt/efs"
}

data "aws_ami" "prj_amazonlinux2" {
  most_recent = true
  filter {
    name   = "owner-alias"
    values = ["amazon"]
  }

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-ebs"]
  }

  owners = ["amazon"]
}

resource "aws_lb" "prj_alb" {
  name               = "${var.prj_name}-alb"
  load_balancer_type = "application"
  subnets            = [aws_subnet.prj_subnet1.id, aws_subnet.prj_subnet2.id]
  security_groups = [aws_security_group.prj_sg.id]

  tags = {
    Name = "${var.prj_name}-alb"
  }
}

resource "aws_lb_listener" "prj_http" {
  load_balancer_arn = aws_lb.prj_alb.arn
  port              = local.http_port
  protocol          = local.http_protocol

  <em># By default, return a simple 404 page</em>
  default_action {
    type = "fixed-response"

    fixed_response {
      content_type = "text/plain"
      message_body = "404: page not found - T101 Study"
      status_code  = 404
    }
  }
}

resource "aws_lb_target_group" "prj_albtg" {
  name = "${var.prj_name}-albtg"
  port     = local.http_port
  protocol = local.http_protocol
  vpc_id   = aws_vpc.prj_vpc.id

  health_check {
    path                = "/"
    protocol            = local.http_protocol
    matcher             = "200-299"
    interval            = 5
    timeout             = 3
    healthy_threshold   = 2
    unhealthy_threshold = 2
  }
}

resource "aws_lb_target_group_attachment" "prj_albrg_atch1" {
  target_group_arn = "${aws_lb_target_group.prj_albtg.arn}"
  target_id        = "${aws_instance.ami_backup_ec2.id}"
  port             = 80
}
resource "aws_lb_target_group_attachment" "prj_albrg_atch2" {
  target_group_arn = "${aws_lb_target_group.prj_albtg.arn}"
  target_id        = "${aws_instance.efs_backup_ec2.id}"
  port             = 80
}

resource "aws_key_pair" "prj_keypair" { 
  key_name = "ec2-kp"
  public_key = file("~/.ssh/id_rsa.pub") 
}


resource "aws_instance" "ami_backup_ec2" {

  depends_on = [
    aws_internet_gateway.prj_igw
    
  ]

  ami                         = data.aws_ami.prj_amazonlinux2.id
  associate_public_ip_address = true
  instance_type               = var.instance_type
  vpc_security_group_ids      = ["${aws_security_group.prj_sg.id}"]
  subnet_id                   = aws_subnet.prj_subnet1.id
key_name = aws_key_pair.prj_keypair.key_name
  user_data = <<-EOF
              <em>#!/bin/bash</em>
              wget https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-x86_64
              mv busybox-x86_64 busybox
              chmod +x busybox
              RZAZ=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone-id)
              IID=$(curl 169.254.169.254/latest/meta-data/instance-id)
              LIP=$(curl 169.254.169.254/latest/meta-data/local-ipv4)
              echo "<h1>RegionAz($RZAZ) : Instance ID($IID) : Private IP($LIP) : Web Server</h1>" > index.html
              nohup ./busybox httpd -f -p 80 &
              EOF

  user_data_replace_on_change = true

  tags = {
    Name = "${var.prj_name}-amibackup-ec2"
    backup = "enable"
  }
}

resource "aws_instance" "efs_backup_ec2" {

  depends_on = [
    aws_internet_gateway.prj_igw
    
  ]

  ami                         = data.aws_ami.prj_amazonlinux2.id
  associate_public_ip_address = true
  instance_type               = var.instance_type
  vpc_security_group_ids      = ["${aws_security_group.prj_sg.id}"]
  subnet_id                   = aws_subnet.prj_subnet2.id
key_name = aws_key_pair.prj_keypair.key_name

  user_data = <<-EOF
              <em>#!/bin/bash</em>
              wget https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-x86_64
              mv busybox-x86_64 busybox
              chmod +x busybox
              RZAZ=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone-id)
              IID=$(curl 169.254.169.254/latest/meta-data/instance-id)
              LIP=$(curl 169.254.169.254/latest/meta-data/local-ipv4)
              echo "<h1>RegionAz($RZAZ) : Instance ID($IID) : Private IP($LIP) : Web Server</h1>" > index.html
              nohup ./busybox httpd -f -p 80 &
              mkdir -p ${local.ec2_file_system_local_mount_path}
              yum install -y amazon-efs-utils
              mount -t efs -o iam,tls ${aws_efs_file_system.prj_efs.id} ${local.ec2_file_system_local_mount_path}
              echo "${aws_efs_file_system.prj_efs.id} ${local.ec2_file_system_local_mount_path} efs _netdev,tls,iam 0 0" >> /etc/fstab
              <em># Creating demo content for other services</em>
              mkdir -p ${local.ec2_file_system_local_mount_path}/fargate
              mkdir -p ${local.ec2_file_system_local_mount_path}/lambda
              df -h > ${local.ec2_file_system_local_mount_path}/fargate/demo.txt
              df -h > ${local.ec2_file_system_local_mount_path}/lambda/demo.txt
              chown ec2-user:ec2-user -R ${local.ec2_file_system_local_mount_path}
              EOF

  user_data_replace_on_change = true

  tags = {
    Name = "${var.prj_name}-amibackup-ec2"
    <em>#backup = "enable"</em>
  }
}

resource "aws_lb_listener_rule" "prj_albrule" {
  listener_arn = aws_lb_listener.prj_http.arn
  priority     = 100

  condition {
    path_pattern {
      values = ["*"]
    }
  }

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.prj_albtg.arn
  }
}

resource "aws_efs_file_system" "prj_efs" {
  encrypted = false
  tags = {
    Name = "${var.prj_name}-efs"
    backup = "enable"
  }
}

resource "aws_efs_backup_policy" "prj_efs_backup_policy" {
  file_system_id = aws_efs_file_system.prj_efs.id

  backup_policy {
    status = "DISABLED"
  }
}

resource "aws_efs_mount_target" "prj_efs_backup_mt" {
  file_system_id = aws_efs_file_system.prj_efs.id
  subnet_id      = aws_subnet.prj_subnet2.id
  security_groups = ["${aws_security_group.prj_sg.id}"]
}

3. RDS 생성

RDS는 별다른 설정 없이 우선 기본값으로 생성하였다.

추후 html 코드 등을 수정하여 DB값을 불러올 때 수정할 예정이다.

<hide/>

resource "aws_db_instance" "prj_rds" {
  <em>#name = "${var.prj_name}-prjdb"</em>
  db_name           = "tfstudyfprjdb"
  allocated_storage = 8
  engine = "mysql"
  engine_version = "5.7"
  instance_class = "db.t2.micro"
  username = ""
  password = ""
  db_subnet_group_name = aws_db_subnet_group.prj_dbsubnetgr.name
}

resource "aws_db_subnet_group" "prj_dbsubnetgr" {
    Name = "${var.prj_name}-dbsubnetgr"
  subnet_ids = [aws_subnet.prj_subnet1.id, aws_subnet.prj_subnet2.id]
}

4. Backup 리소스 생성

Backup Vault을 생성하기 전에 KMS Key을 생성하였다.

이후 Vault을 생성하고 Plan과 리소스 종류를 결정 지어줬다.

백업 선택은 Tag에 backup:enable이 입력 된 리소스들에 한해 백업을 진행할 수 있도록 하였다.

백업 스케쥴은 2시간 단위로 백업이 진행 될 수 있도록 Cron 표현식을 사용하였다.


resource "aws_kms_key" "prj_kmskey" {
  description             = "KMS key for backup vault"
}

resource "aws_backup_vault" "prj_kmskey" {
  name = "${var.prj_name}-backupvault"
  kms_key_arn = aws_kms_key.prj_kmskey.arn
}

resource "aws_backup_selection" "prj_backup_sel" {
  iam_role_arn = "arn:aws:iam::536033748497:role/aws-service-role/backup.amazonaws.com/AWSServiceRoleForBackup"
  name = "${var.prj_name}-backupsel"
  plan_id      = aws_backup_plan.prj_backup_plan.id

  selection_tag {
    type  = "STRINGEQUALS"
    key   = "backup"
    value = "enalbe"
  }

}

resource "aws_backup_plan" "prj_backup_plan" {
  name = "${var.prj_name}-backupplan"

  rule {
    rule_name         = "${var.prj_name}-backuprule-ami"
    target_vault_name = aws_backup_vault.prj_kmskey.name
    schedule          = "cron(0 0/2 1/1 * ? *)"
    
    lifecycle {
      delete_after = 2
    }
  }

    rule {
    rule_name         = "${var.prj_name}-backuprule-efs"
    target_vault_name = aws_backup_vault.prj_kmskey.name
    schedule          = "cron(0 0/2 1/1 * ? *)"
    
    lifecycle {
      delete_after = 2
    }
  }
  rule {
    rule_name         = "${var.prj_name}-backuprule-rds"
    target_vault_name = aws_backup_vault.prj_kmskey.name
    schedule          = "cron(0 0/2 1/1 * ? *)"

    lifecycle {
      delete_after = 2
    }
  }

  advanced_backup_setting {
    backup_options = {
      WindowsVSS = "enabled"
    }
    resource_type = "EC2"
  }
}

5. EFS 마운트 확인

배포가 전부 완료 되면 EC2에 접근하여 EFS Mount가 잘 됐는지 확인한다.

마운트는 물론 폴더 생성하는 스크립트까지 잘 작동한 것을 확인할 수 있다.

6. 회고

RDS을 활용하지 못해 아쉬웠다.

백업의 경우에도 시간이 부족해 백업이 잘 작동하고 이걸 바탕으로 복원하는 테스트까진 진행해보지 못했다.

콘솔 상에선 작동해봤지만 코드로 진행해보지 못해서 아쉽다.

테라폼을 쓰다보면 리소스 관리가 수월하다는 것을 알 수 있다. 하지만 단점도 분명 존재하는 것 같다.

예를 들면, 콘솔에서 UI을 바탕으로 생성할 때는 손 쉽게 구조가 보이지만 테라폼으로 하려면 코드 몇 줄만으로는 생성이 어렵다는 것을 알 수 있게 된다.

물론 테라폼 Docs에 어느정도 기술이 되어있지만 이것만 보고는 진행하기가 어려울 것으로 생각 된다.

plan과 apply을 계속 실행해보며 오류를 찾아서 해결해나가는 과정이 필요하지 않을까 싶다.

그런 과정 속에서 RDS나 ASG같이 삭제되고 재생성되는 리소스들의 시간이 오래 걸리는 부분도 단점이 아닐까 싶다.

그럼에도 불구하고 편리하게 리소스를 배포하고 관리할 수 있다는 점에서는 분명 좋은 툴이라고 생각이 든다.

앞으로 더 다양한 리소스를 테라폼을 통해 배포하는 과정을 진행해봐야겠다.

Terraform Study #1

4주 동안 Terraform Study에 참여하였고 이번 주에 중간 과제를 제출해야 한다.

어떤 주제로 진행을 할까 고민하였는데 지금까지 배웠던 내용들을 간략하게 정리하면서 가장 최근에 배운 기능인 모듈을 활용해서 각기 다른 환경에 리소들을 배포한 뒤 테스트하는 내용으로 결정했다.

실습 환경

랩탑 M1 Macbook Pro 10core 32GB RAM, macOS 12.6.1

VSCode, iTerm2, AWS

실습 내용

모듈을 사용하여 환경을 나누어(PRD, DEV) 리소스 배포 후 curl 테스트

Mac OS에서 Terraform을 설치할 때는 단순한 설치보다는 tfenv을 설치해서 버저닝 기능을 사용하면 좋다.

최신 버전인 1.3.4를 설치하여 사용했다.

– IAM User을 생성하여 권한 부여
IAM User로 생성한 계정의 Access 정보를 변수로 입력해놓으면 터미널을 사용할 때 편리하다.
export AWS_ACCESS_KEY_ID=””
export AWS_SECRET_ACCESS_KEY=””

export AWS_DEFAULT_REGION=ap-northeast-2

– 테라폼을 간단하게 구성할 경우 variables.tf, output.tf, main.tf 등을 기본으로 해서 간단하게 배포가 가능하다.

–  기본적인 명령어는 3가지만 우선 알면 된다.

terraform init && terraform plan && terraform apply -auto-approve

-auto-approve는 별다른 확인 절차 없이 바로 배포를 진행하겠다는 의미로 운영계에서는 가급적 삼가는 게 좋을 것 같다.

– variables.tf 을 활용할 때는 아래와 같이 사용한다.

– ubuntu 최신 버전을 사용하려면 아래와 같이 data 소스를 생성

data "aws_ami" "ubuntu" {
    most_recent = true

    filter {
        name   = "name"
        values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
    }

    filter {
        name   = "virtualization-type"
        values = ["hvm"]
    }

    owners = ["099720109477"] # Canonical
}

– output 사용해서 생성 된 EC2의 Public IP 등을 확인할 수 있다.

output "public_ip" {
  value       = aws_instance.example.public_ip
  description = "The public IP of the Instance"
}

– userdata을 통해 배포되는 인스턴스에 웹서비스 설치 및 간단한 html을 배포하여 사용할 수 있다.

스터디에서 사용한 간단한 예제는 다음과 같다.

  user_data = <<-EOF
              #!/bin/bash
              wget https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-x86_64
              mv busybox-x86_64 busybox
              chmod +x busybox
              RZAZ=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone-id)
              IID=$(curl 169.254.169.254/latest/meta-data/instance-id)
              LIP=$(curl 169.254.169.254/latest/meta-data/local-ipv4)
              echo "<h1>RegionAz($RZAZ) : Instance ID($IID) : Private IP($LIP) : Web Server</h1>" > index.html
              nohup ./busybox httpd -f -p 80 &
              EOF

– 아직 상태 관리에 대한 부분은 이해를 다 하지 못해서 이 부분은 천천히 따로 복습해봐야 할 것 같다.

resource "aws_s3_bucket" "prj_s3bucket" {
  bucket = "${var.prj_name}-tfstate"
}

# Enable versioning so you can see the full revision history of your state files
resource "aws_s3_bucket_versioning" "prj_s3bucket_versioning" {
  bucket = aws_s3_bucket.prj_s3bucket.id
  versioning_configuration {
    status = "Enabled"
  }
}

output "s3_bucket_arn" {
  value       = aws_s3_bucket.prj_s3bucket.arn
  description = "The ARN of the S3 bucket"
}

resource "aws_dynamodb_table" "prj_dynamodbtable" {
  name         = "tfme--locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

output "dynamodb_table_name" {
  value       = aws_dynamodb_table.prj_dynamodbtable.name
  description = "The name of the DynamoDB table"
}

– 이번 과제의 구조는 아래와 같이 생성했다.

조금 아쉬운 점은 Modules의 경우 compute와 network로 폴더 구조화해서 하위에 ec2.tf, sg,tf 등 tf을 리소스 종류 별로 구분하여 생성하고 싶었는데 그렇게 진행하지 못한 점이다. 그렇게 진행하지 못한 이유는 Compute 쪽에서 Network 쪽의 리소스 정보를 받아와야 하는데 그러지 못함에 있었다. 아마 별도로 호출하는 방안이 있을 것 같은데 다음에 시도해볼 예정이다.

– 구조는 최대한 단순하게 작성했다.

ASG에 웹서버만 설치한 인스턴스를 배포하였다. DB을 연결하여 특정 데이터를 호출하는 페이지를 생성하고 싶었지만 Module을 적용하는 과정에서 시간이 많이 소요되어 해당 기능들을 적용하지는 못했다. 추후 해당 기능은 추가할 예정이다.

– ALB을 사용하였기 때문에 CURL 테스트 때 적용할 DNS 값을 호출하기 위해 output.tf에 alb dns을 호출할 수 있도록 했다.

– 리소스 이름을 관리하기 위해 prd, dev 환경에 따라 prj_name을 입력 받고 Module에서 “prj_name-리소스명” 방식으로 이름을 입력하게끔 하였다. 이렇게 하면 프로젝트 이름과 Env 값을 받아 tfstudy-prd가 기본값이 되고 그 뒤에 리소스 명(ex:subnet1)이 결합하여 tfstudy-prd-subnet1이라는 서브넷이 생성되게 된다.

  resource~~{
  tags = {
    Name = "${var.prj_name}-subnet1"
  }
  }
  
  module "midexam" {
  source = "../Modules"

  prj_name           = "tfstudy-prd"
  }

– 환경에 따라 max, min 인스턴스 숫자와 인스턴스 크기의 차이점을 두고 vpc, subnet의 cidr 값을 설정했다.

개발계와 운영계는 IP을 다르게 설정하였다. 추후 확장했을 때 VPN을 연결하여 IDC와 연동한다고 한다면 중복되는 IP로 인해 충돌이 날 수 있기 때문에 이 부분을 고려하여 환경 간 IP  차이를 두었다.

#Dev 환경
module "midexam" {
  source = "../Modules"

  prj_name           = "tfstudy-dev"
  bucket_name = "tfstudy-dev-tfstate"
  instance_type = "t2.micro"
  min_size      = 2
  max_size      = 3
  vpc_cidr  = "192.168.0.0/16"
  subnet1_cidr  = "192.168.0.0/24"
  subnet2_cidr  = "192.168.1.0/24"
}

#Prd 환경
module "midexam" {
  source = "../Modules"

  prj_name           = "tfstudy-prd"
  bucket_name = "tfstudy-prd-tfstate"
  instance_type = "m4.large"
  min_size      = 2
  max_size      = 10
  vpc_cidr  = "172.16.0.0/16"
  subnet1_cidr  = "172.16.0.0/24"
  subnet2_cidr  = "172.16.1.0/24"
}

– 처음 dev 환경에서 배포를 시작했을 때 여러 오류가 발생했었다. 대표적으로는 valiables.tf에서 값을 불러오는 것에서 충돌이 많았는데 중복되는 설정값 등이 있어서 이 부분을 정리했다. 대부분의 변수값은 Modules에서 받아올 수 있도록 했다.

– Modules 안에서만 불러오고 밖에서 참조할 필요가 없는 값의 경우 locals 을 통해 Modules 내부에서 처리하였다.

대표적으로, Security Group 생성 시에 사용하는 port, cidr, protocol에 활용하였다. 추후 locals에 넣을 수 있는 변수들이 어떤 게 있을지 조금 더 고민해봐야 할 것 같다.

locals {
  http_port    = 80
  any_port     = 0
  any_protocol = "-1"
  tcp_protocol = "tcp"
  all_ips      = ["0.0.0.0/0"]
  all_block    = "0.0.0.0/0"
}

– 배포 후 리소스 목록은 아래와 같다.

– 배포를 완료하고 curl 테스트를 완료한 화면은 아래와 같다.

위 화면이 운영계에서의 테스트고 아래가 개발계에서의 테스트이다.

둘 다 정상적으로 ALB을 통해 각 인스턴스로 트래픽이 분배되는 것을 확인할 수 있었다.

모듈을 통한 배포를 진행했기 때문에 각 환경에서는 환경에 맞는 변수와 아이템을 추가해주기만 하면 되어서 여러 번을 거친 작업을 하지 않아도 되는 부분은 충분히 장점으로 인식 됐다.

아쉬운 점은, 폴더를 도식화했을 때 그 각 폴더를 참조하기 위해 source 경로를 입력해주는 부분이 조금 불편했다. 이 부분을 쉽게 읽어올 수 있는 방법이 있으면 좋을 것 같다. (이미 있을 수도 있는데 내가 못 찾은 건지도..)

위에 진행을 하는 과정에서도 적었던 아쉬운 점이지만, 아직 테라폼에 대한 이해도가 부족해 Module을 제대로 사용하지 못한 점과 여러 리소스를 활용하지 못한 점이 아쉽다.

이 과제는 지우지 않고 계속 보완해가면서 활용할 수 있으면 좋을 것 같다.

여기까지 직접 테라폼으로 만들어보니 앞으로 업무에 적용하거나 개인 프로젝트를 할 때도 활용할 수 있을 것 같다는 생각이 든 게 가장 큰 수확이라고 생각한다.

남은 스터디 기간 동안 더 열심히 해서 조금 더 나은 결과물을 만들어야겠다.