AEWS Study #7 – EKS Automation

이번 챕터는 EKS 스터디의 마지막 챕터인 EKS Automation이다. 7주라는 짧은 시간 동안 여러 내용들을 학습해왔는데 이번 주는 배웠던 내용들을 토대로 자동화룰 구성하고 GitOps 환경을 구성하는 내용으로 진행하게 된다.

0. 환경 구성

이번 환경 구성은 지난 번과 마찬가지로 특별하게 변경하는 내용은 없고 yaml 파일로 기본 환경 배포 및 External DNS 설치와 prometheus-community, Metrics-server 설치를 진행한다.

1. AWS Controller for Kubernetes (ACK)

AWS Controller for Kubernetes (ACK)는 AWS 리소스와 k8s을 통합하여 Cluster 내에서 AWS 서비스를 관리하고 프로비저닝하는 기능을 제공하는 AWS에서 개발한 오픈소스 프로젝트이다.
ACK는 AWS 리소스를 k8s의 CRD(Custom Resource Definition)으로 표현하고, AWS 리소스와 k8s 리소스 간의 상태를 동기화하는 방식으로 작동한다. 이를 통해 개발자와 운영자는 k8s API을 통해 AWS 서비스 관리가 가능하다.
ACK는 AWS의 다양한 서비스(S3, RDS, DynamoDB, ECR, EKS, SNS, SQS 등)을 지원한다.

https://aws-controllers-k8s.github.io/community/docs/community/how-it-works/

k8s api 와 aws api 의 2개의 RBAC 시스템 확인, 각 서비스 컨트롤러 파드는 AWS 서비스 권한 필요 ← IRSA role for ACK Service Controller

https://aws-controllers-k8s.github.io/community/docs/user-docs/authorization/

ACK를 이용해서 AWS의 각종 서비스들을 배포해보는 실습을 진행한다.
ACK을 이용한 AWS 서비스 배포 과정은 서비스들 전체적으로 비슷하게 이루어진다.
ACK Controller 설치 w/helm (각 서비스에 맞게) -> IRSA 권한 설정 -> AWS 서비스 배포

1-1. ACK for S3

우선 S3을 먼저 배포하고 테스트할 예정이다.
ACK S3 Controller 설치를 진행한다. helm 차트를 다운로드 받고 해당 helm chart을 사용해서 ACK S3 Controller을 설치하고 확인한다. 설치에 사용되는 변수는 SERVICE, ACK_SYSTEM_NAMESPACE, AWS_REGION이 사용된다. EC2나 RDS 등 다른 서비스를 배포할 때도 ACK Controller을 설치해야 하는데 이때 다른 변수는 동일하게 두고 SERVICE만 바꿔서 설치해서 구분지어주면 좋다.

# 서비스명 변수 지정
export SERVICE=s3

# helm 차트 다운로드
export RELEASE_VERSION=$(curl -sL https://api.github.com/repos/aws-controllers-k8s/$SERVICE-controller/releases/latest | grep '"tag_name":' | cut -d'"' -f4 | cut -c 2-)
helm pull oci://public.ecr.aws/aws-controllers-k8s/$SERVICE-chart --version=$RELEASE_VERSION
tar xzvf $SERVICE-chart-$RELEASE_VERSION.tgz

# helm chart 확인
tree ~/$SERVICE-chart

# ACK S3 Controller 설치
export ACK_SYSTEM_NAMESPACE=ack-system
export AWS_REGION=ap-northeast-2
helm install --create-namespace -n $ACK_SYSTEM_NAMESPACE ack-$SERVICE-controller --set aws.region="$AWS_REGION" ~/$SERVICE-chart

# 설치 확인
helm list --namespace $ACK_SYSTEM_NAMESPACE
NAME             	NAMESPACE 	REVISION	UPDATED                                	STATUS  	CHART         	APP VERSION
ack-s3-controller	ack-system	1       	2023-06-04 21:12:06.857876402 +0900 KST	deployed	s3-chart-1.0.4	1.0.4

kubectl -n ack-system get pods
NAME                                          READY   STATUS    RESTARTS   AGE
ack-s3-controller-s3-chart-7c55c6657d-mbrq5   1/1     Running   0          10s

kubectl get crd | grep $SERVICE
buckets.s3.services.k8s.aws                  2023-06-04T12:12:04Z

kubectl get all -n ack-system
NAME                                              READY   STATUS    RESTARTS   AGE
pod/ack-s3-controller-s3-chart-7c55c6657d-mbrq5   1/1     Running   0          30s
NAME                                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ack-s3-controller-s3-chart   1/1     1            1           30s
NAME                                                    DESIRED   CURRENT   READY   AGE
replicaset.apps/ack-s3-controller-s3-chart-7c55c6657d   1         1         1       30s

kubectl get-all -n ack-system
kubectl describe sa -n ack-system ack-s3-controller
Name:                ack-s3-controller
Namespace:           ack-system
Labels:              app.kubernetes.io/instance=ack-s3-controller
                     app.kubernetes.io/managed-by=Helm
....

AWS 서비스 배포 및 관리 권한을 얻기 위해 IRSA 설정을 진행한다. 실습에서는 AmazonS3FullAccess 권한을 사용해서 진행하도록 한다.

# Create an iamserviceaccount - AWS IAM role bound to a Kubernetes service account
eksctl create iamserviceaccount \
  --name ack-$SERVICE-controller \
  --namespace ack-system \
  --cluster $CLUSTER_NAME \
  --attach-policy-arn $(aws iam list-policies --query 'Policies[?PolicyName==`AmazonS3FullAccess`].Arn' --output text) \
  --override-existing-serviceaccounts --approve

# 확인 >> 웹 관리 콘솔에서 CloudFormation Stack >> IAM Role 확인
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
NAMESPACE	NAME				ROLE ARN
ack-system	ack-s3-controller		arn:aws:iam::MyAccount:role/eksctl-myeks-addon-iamserviceaccount-ack-sys-Role1-XFQ0LIYFYJ3Z

# Inspecting the newly created Kubernetes Service Account, we can see the role we want it to assume in our pod.
kubectl get sa -n ack-system
NAME                SECRETS   AGE
ack-s3-controller   0         5m16s

kubectl describe sa ack-$SERVICE-controller -n ack-system
Name:                ack-s3-controller
Namespace:           ack-system
Labels:              app.kubernetes.io/instance=ack-s3-controller
                     app.kubernetes.io/managed-by=eksctl
...

# Restart ACK service controller deployment using the following commands.
kubectl -n ack-system rollout restart deploy ack-$SERVICE-controller-$SERVICE-chart

# IRSA 적용으로 Env, Volume 추가 확인
kubectl describe pod -n ack-system -l k8s-app=$SERVICE-chart
Name:             ack-s3-controller-s3-chart-559866764-42xt6
Namespace:        ack-system
Priority:         0
Service Account:  ack-s3-controller
Node:             ip-192-168-1-10.ap-northeast-2.compute.internal/192.168.1.10
...

ACK S3 Controller와 IRSA가 준비됐다면 S3을 배포하고 수정 및 삭제하는 테스트를 진행해본다.
Bucket Name은 중복을 피하기 위해 Account ID을 참조해서 만들고 생성 확인 후 Tag 값을 추가한 업데이트와 삭제를 진행해봤다.
S3의 경우 문제 없이 간단하게 배포 및 업데이트, 삭제가 진행된 것을 확인할 수 있었다.

# [터미널1] 모니터링
watch -d aws s3 ls

# S3 버킷 생성을 위한 설정 파일 생성
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
export BUCKET_NAME=my-ack-s3-bucket-$AWS_ACCOUNT_ID

read -r -d '' BUCKET_MANIFEST <<EOF
apiVersion: s3.services.k8s.aws/v1alpha1
kind: Bucket
metadata:
  name: $BUCKET_NAME
spec:
  name: $BUCKET_NAME
EOF

echo "${BUCKET_MANIFEST}" > bucket.yaml
cat bucket.yaml | yh
apiVersion: s3.services.k8s.aws/v1alpha1
kind: Bucket
metadata:
  name: my-ack-s3-bucket-MyAccount
spec:
  name: my-ack-s3-bucket-MyAccount

# S3 버킷 생성
aws s3 ls
kubectl create -f bucket.yaml
bucket.s3.services.k8s.aws/my-ack-s3-bucket-MyAccount created

# S3 버킷 확인
aws s3 ls
2023-06-04 21:21:43 my-ack-s3-bucket-MyAccount

kubectl get buckets
NAME                            AGE
my-ack-s3-bucket-MyAccount   17s

kubectl describe bucket/$BUCKET_NAME | head -6
Name:         my-ack-s3-bucket-MyAccount
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  s3.services.k8s.aws/v1alpha1
Kind:         Bucket

# S3 버킷 업데이트 : 태그 정보 입력
read -r -d '' BUCKET_MANIFEST <<EOF
apiVersion: s3.services.k8s.aws/v1alpha1
kind: Bucket
metadata:
  name: $BUCKET_NAME
spec:
  name: $BUCKET_NAME
  tagging:
    tagSet:
    - key: myTagKey
      value: myTagValue
EOF

echo "${BUCKET_MANIFEST}" > bucket.yaml

# 변경 내용 사전 확인
cat bucket.yaml | yh
apiVersion: s3.services.k8s.aws/v1alpha1
kind: Bucket
metadata:
  name: my-ack-s3-bucket-587122150371
spec:
  name: my-ack-s3-bucket-587122150371
  tagging:
    tagSet:
    - key: myTagKey
      value: myTagValue

# S3 버킷 설정 업데이트 실행 : 필요 주석 자동 업뎃 내용이니 무시해도됨!
kubectl apply -f bucket.yaml

# S3 버킷 업데이트 확인 
kubectl describe bucket/$BUCKET_NAME | grep Spec: -A5
Spec:
  Name:  my-ack-s3-bucket-MyAccount
  Tagging:
    Tag Set:
      Key:    myTagKey
      Value:  myTagValue

# S3 버킷 삭제
kubectl delete -f bucket.yaml

# verify the bucket no longer exists
kubectl get bucket/$BUCKET_NAME
aws s3 ls | grep $BUCKET_NAME

S3의 생성, 업데이트와 삭제을 확인 완료했다면 ACK S3 Controller와 IRSA을 삭제하도록 한다. 물론, 다른 서비스의 Controller, IRSA와 겹치지 않게 배포할 수 있기 때문에 꼭 삭제를 사전에 진행할 필요는 없다.

# helm uninstall
export SERVICE=s3
helm uninstall -n $ACK_SYSTEM_NAMESPACE ack-$SERVICE-controller

# ACK S3 Controller 관련 crd 삭제
kubectl delete -f ~/$SERVICE-chart/crds

# IRSA 삭제
eksctl delete iamserviceaccount --cluster myeks --name ack-$SERVICE-controller --namespace ack-system

##### namespace 삭제 >> ACK 모든 실습 후 삭제  #####
kubectl delete namespace $ACK_K8S_NAMESPACE

1-2. ACK for EC2&VPC

S3는 간단하게 Bucket만 배포하고 업데이트, 삭제하는 부분이라 어렵지 않았는데 이번엔 EC2를 배포하려고 한다. EC2의 경우 배포하려면 VPC부터 시작해서 여러 서비스들과 결합이 필요하기 때문에 꽤 상세한 설정이 필요하다.
순서는 ACK EC2 Controller 설치-> EC2 IRSA 설정(AmazonEC2FullAccess) -> VPC, Subnet 생성 -> Public/Private Subnet EC2 배포 -> 테스트 순으로 진행한다.

우선 ACK EC2 Controller 설치를 진행한다. 특이한 내용은 없고 SERVICE 변수를 위에서 실습한 s3에서 ec2로 변경하고 진행하면 된다.

# 서비스명 변수 지정 및 helm 차트 다운로드
export SERVICE=ec2
export RELEASE_VERSION=$(curl -sL https://api.github.com/repos/aws-controllers-k8s/$SERVICE-controller/releases/latest | grep '"tag_name":' | cut -d'"' -f4 | cut -c 2-)
helm pull oci://public.ecr.aws/aws-controllers-k8s/$SERVICE-chart --version=$RELEASE_VERSION
tar xzvf $SERVICE-chart-$RELEASE_VERSION.tgz

# helm chart 확인
tree ~/$SERVICE-chart

# ACK EC2-Controller 설치
export ACK_SYSTEM_NAMESPACE=ack-system
export AWS_REGION=ap-northeast-2
helm install -n $ACK_SYSTEM_NAMESPACE ack-$SERVICE-controller --set aws.region="$AWS_REGION" ~/$SERVICE-chart
NAME: ack-ec2-controller
LAST DEPLOYED: Sun Jun  4 21:46:39 2023
NAMESPACE: ack-system
STATUS: deployed

# 설치 확인
helm list --namespace $ACK_SYSTEM_NAMESPACE
NAME              	NAMESPACE 	REVISION	UPDATED                               	STATUS  	CHART          	APP VERSION
ack-ec2-controller	ack-system	1       	2023-06-04 21:46:39.80730411 +0900 KST	deployed	ec2-chart-1.0.3	1.0.3

kubectl -n $ACK_SYSTEM_NAMESPACE get pods -l "app.kubernetes.io/instance=ack-$SERVICE-controller"
NAME                                            READY   STATUS    RESTARTS   AGE
ack-ec2-controller-ec2-chart-777567ff4c-45s2d   1/1     Running   0          21s

kubectl get crd | grep $SERVICE
dhcpoptions.ec2.services.k8s.aws             2023-06-04T12:46:38Z
elasticipaddresses.ec2.services.k8s.aws      2023-06-04T12:46:38Z
instances.ec2.services.k8s.aws               2023-06-04T12:46:38Z
internetgateways.ec2.services.k8s.aws        2023-06-04T12:46:38Z
natgateways.ec2.services.k8s.aws             2023-06-04T12:46:39Z
routetables.ec2.services.k8s.aws             2023-06-04T12:46:39Z
securitygroups.ec2.services.k8s.aws          2023-06-04T12:46:39Z
subnets.ec2.services.k8s.aws                 2023-06-04T12:46:39Z
transitgateways.ec2.services.k8s.aws         2023-06-04T12:46:39Z
vpcendpoints.ec2.services.k8s.aws            2023-06-04T12:46:39Z
vpcs.ec2.services.k8s.aws                    2023-06-04T12:46:39Z

IRSA 설정 또한 s3 때와 크게 다르지 않다. 권한만 AmazonS3FullAccess에서 AmazonEC2FullAccess로 변경하여 진행한다. IRSA 설정 후 AWS Console에서 IAM Role에서 생성 된 Role을 확인하면 AmazonEC2FullAccess가 잘 들어간 것을 확인할 수 있다.

# Create an iamserviceaccount - AWS IAM role bound to a Kubernetes service account
eksctl create iamserviceaccount \
  --name ack-$SERVICE-controller \
  --namespace $ACK_SYSTEM_NAMESPACE \
  --cluster $CLUSTER_NAME \
  --attach-policy-arn $(aws iam list-policies --query 'Policies[?PolicyName==`AmazonEC2FullAccess`].Arn' --output text) \
  --override-existing-serviceaccounts --approve

# 확인 >> 웹 관리 콘솔에서 CloudFormation Stack >> IAM Role 확인
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
NAMESPACE	NAME				ROLE ARN
ack-system	ack-ec2-controller		arn:aws:iam::MyAccount:role/eksctl-myeks-addon-iamserviceaccount-ack-sys-Role1-VK23PW7Y288N

# Inspecting the newly created Kubernetes Service Account, we can see the role we want it to assume in our pod.
kubectl get sa -n $ACK_SYSTEM_NAMESPACE
NAME                 SECRETS   AGE
ack-ec2-controller   0         3m45s

kubectl describe sa ack-$SERVICE-controller -n $ACK_SYSTEM_NAMESPACE
Name:                ack-ec2-controller
Namespace:           ack-system
Labels:              app.kubernetes.io/instance=ack-ec2-controller
                     app.kubernetes.io/managed-by=eksctl
                     app.kubernetes.io/name=ec2-chart
...

# Restart ACK service controller deployment using the following commands.
kubectl -n $ACK_SYSTEM_NAMESPACE rollout restart deploy ack-$SERVICE-controller-$SERVICE-chart
deployment.apps/ack-ec2-controller-ec2-chart restarted

# IRSA 적용으로 Env, Volume 추가 확인
kubectl describe pod -n $ACK_SYSTEM_NAMESPACE -l k8s-app=$SERVICE-chart
Name:             ack-ec2-controller-ec2-chart-76dd69c88-62kfs
Namespace:        ack-system
Priority:         0
Service Account:  ack-ec2-controller
Node:             ip-192-168-1-10.ap-northeast-2.compute.internal/192.168.1.10
...

EC2 배포에 사용할 VPC, Subnet 그리고 IGW와 SG등을 생성하기 전에 테스트로 VPC, Subnet을 배포하는 실습을 진행해보았다.

# [터미널1] 모니터링
while true; do aws ec2 describe-vpcs --query 'Vpcs[*].{VPCId:VpcId, CidrBlock:CidrBlock}' --output text; echo "-----"; sleep 1; done
-----
192.168.0.0/16	vpc-0ab487a4560d0e009
172.31.0.0/16	vpc-04663e26208f8fb7d
-----

# VPC 생성
cat <<EOF > vpc.yaml
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: VPC
metadata:
  name: vpc-tutorial-test
spec:
  cidrBlocks: 
  - 10.0.0.0/16
  enableDNSSupport: true
  enableDNSHostnames: true
EOF
 
kubectl apply -f vpc.yaml
vpc.ec2.services.k8s.aws/vpc-tutorial-test created

-----
192.168.0.0/16	vpc-0ab487a4560d0e009
172.31.0.0/16	vpc-04663e26208f8fb7d
10.0.0.0/16	vpc-0cc88d9792531d28b
-----

# VPC 생성 확인
kubectl get vpcs
NAME                ID                      STATE
vpc-tutorial-test   vpc-0cc88d9792531d28b   available

kubectl describe vpcs
Name:         vpc-tutorial-test
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  ec2.services.k8s.aws/v1alpha1
...

aws ec2 describe-vpcs --query 'Vpcs[*].{VPCId:VpcId, CidrBlock:CidrBlock}' --output text
192.168.0.0/16	vpc-0ab487a4560d0e009
172.31.0.0/16	vpc-04663e26208f8fb7d
10.0.0.0/16	vpc-0cc88d9792531d28b

# [터미널1] 모니터링
VPCID=$(kubectl get vpcs vpc-tutorial-test -o jsonpath={.status.vpcID})
while true; do aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPCID" --query 'Subnets[*].{SubnetId:SubnetId, CidrBlock:CidrBlock}' --output text; echo "-----"; sleep 1 ; done
-----
-----

# 서브넷 생성
VPCID=$(kubectl get vpcs vpc-tutorial-test -o jsonpath={.status.vpcID})

cat <<EOF > subnet.yaml
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: Subnet
metadata:
  name: subnet-tutorial-test
spec:
  cidrBlock: 10.0.0.0/20
  vpcID: $VPCID
EOF
kubectl apply -f subnet.yaml
-----
10.0.0.0/20	subnet-03d5af788ba04433d
-----

# 서브넷 생성 확인
kubectl get subnets
kubectl describe subnets
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPCID" --query 'Subnets[*].{SubnetId:SubnetId, CidrBlock:CidrBlock}' --output text

# 리소스 삭제
kubectl delete -f subnet.yaml && kubectl delete -f vpc.yaml

테스트로 VPC, Subnet을 생성해봤으니 이제 EC2 배포에 필요할 리소스들을 배포해보도록 한다. AWS Network Resource를 배포할 때는 workflow을 작성하는 게 좋다. VPC CIDR, Subnet CIDR(w/AZ)와 Public/Private Subnet 구분 그리고 IGW와 SG을 생성하고 Subnet에서의 정상적인 트래픽 흐름을 위해 RT 까지 같이 설정해준다. 이번 실습에서는 샘플로 제공되는 Workflow을 사용한다.

https://aws-controllers-k8s.github.io/community/docs/tutorials/ec2-example/#create-a-vpc-workflow

위 그림과 같은 흐름으로 Network Workflow을 배포할 예정이다. 내용을 보면 tutorial-vpc(10.0.0.0/16)을 생성하고 그 안에 Public(10.0.0.0/20)/Private(10.0.128.0/20)을 하나씩 생성한다.
SG는 any IP에 대해 22 port 접근을 허용하고 Internet Gateway, NAT Gateway(w/EIP)을 각각 생성한다. 그리고 RouteTable은 Public Subnet용 RT는 0.0.0.0/0에 대해 IGW로 연결하고 Private Subnet용 RT는 0.0.0.0/0에 대해 NAT Gateway로 연결해준다.
샘플로 제공 된 yaml을 사용하여 배포하도록 한다.

cat <<EOF > vpc-workflow.yaml
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: VPC
metadata:
  name: tutorial-vpc
spec:
  cidrBlocks: 
  - 10.0.0.0/16
  enableDNSSupport: true
  enableDNSHostnames: true
  tags:
    - key: name
      value: vpc-tutorial
---
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: InternetGateway
metadata:
  name: tutorial-igw
spec:
  vpcRef:
    from:
      name: tutorial-vpc
---
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: NATGateway
metadata:
  name: tutorial-natgateway1
spec:
  subnetRef:
    from:
      name: tutorial-public-subnet1
  allocationRef:
    from:
      name: tutorial-eip1
---
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: ElasticIPAddress
metadata:
  name: tutorial-eip1
spec:
  tags:
    - key: name
      value: eip-tutorial
---
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: RouteTable
metadata:
  name: tutorial-public-route-table
spec:
  vpcRef:
    from:
      name: tutorial-vpc
  routes:
  - destinationCIDRBlock: 0.0.0.0/0
    gatewayRef:
      from:
        name: tutorial-igw
---
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: RouteTable
metadata:
  name: tutorial-private-route-table-az1
spec:
  vpcRef:
    from:
      name: tutorial-vpc
  routes:
  - destinationCIDRBlock: 0.0.0.0/0
    natGatewayRef:
      from:
        name: tutorial-natgateway1
---
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: Subnet
metadata:
  name: tutorial-public-subnet1
spec:
  availabilityZone: ap-northeast-2a
  cidrBlock: 10.0.0.0/20
  mapPublicIPOnLaunch: true
  vpcRef:
    from:
      name: tutorial-vpc
  routeTableRefs:
  - from:
      name: tutorial-public-route-table
---
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: Subnet
metadata:
  name: tutorial-private-subnet1
spec:
  availabilityZone: ap-northeast-2a
  cidrBlock: 10.0.128.0/20
  vpcRef:
    from:
      name: tutorial-vpc
  routeTableRefs:
  - from:
      name: tutorial-private-route-table-az1
---
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: SecurityGroup
metadata:
  name: tutorial-security-group
spec:
  description: "ack security group"
  name: tutorial-sg
  vpcRef:
     from:
       name: tutorial-vpc
  ingressRules:
    - ipProtocol: tcp
      fromPort: 22
      toPort: 22
      ipRanges:
        - cidrIP: "0.0.0.0/0"
          description: "ingress"
EOF

yaml을 생성했으면 해당 yaml을 갖고 VPC 환경을 배포하도록 한다. Private Subnet의 경우 AWS Console에서 배포할 때도 그렇지만 NAT GW의 배포가 완료되기 전까지 정보 확인이 완료되지 않는다. AWS Console에서 확인하는 VPC 구성도에서도 subnet이 하나만 표현 된다. 이후 시간이 지나고 NAT GW가 배포되고 Private Subnet의 정보가 반영되면 AWS Console에서도 정상적으로 화면이 나타난다.

# VPC 환경 생성
kubectl apply -f vpc-workflow.yaml

# [터미널1] NATGW 생성 완료 후 tutorial-private-route-table-az1 라우팅 테이블 ID가 확인되고 그후 tutorial-private-subnet1 서브넷ID가 확인됨 > 5분 정도 시간 소요
watch -d kubectl get routetables,subnet


# VPC 환경 생성 확인
kubectl describe vpcs
Name:         tutorial-vpc
Namespace:    default
...
kubectl describe internetgateways
Name:         tutorial-igw
Namespace:    default
...
kubectl describe routetables
Name:         tutorial-private-route-table-az1
Namespace:    default
...
kubectl describe natgateways
Name:         tutorial-natgateway1
Namespace:    default
...
kubectl describe elasticipaddresses
Name:         tutorial-eip1
Namespace:    default
...
kubectl describe securitygroups
Name:         tutorial-security-group
Namespace:    default
...

# 배포 도중 2개의 서브넷 상태 정보 비교 해보자
kubectl describe subnets
...
Status:
  Conditions:
    Last Transition Time:  2023-06-04T13:03:04Z
    Message:               Reference resolution failed
    Reason:                the referenced resource is not synced yet. resource:RouteTable, namespace:default, name:tutorial-private-route-table-az1
    Status:                Unknown
    Type:                  ACK.ReferencesResolved
...
Status:
  Ack Resource Metadata:
    Arn:                       arn:aws:ec2:ap-northeast-2:MyAccount:subnet/subnet-03cd0923f8663f1ee
    Owner Account ID:          MyAccount
    Region:                    ap-northeast-2
  Available IP Address Count:  4091
  Conditions:
    Last Transition Time:           2023-06-04T13:01:45Z
    Status:                         True
    Type:                           ACK.ReferencesResolved
    Last Transition Time:           2023-06-04T13:01:45Z
    Message:                        Resource synced successfully
    Reason:
    Status:                         True
    Type:                           ACK.ResourceSynced
...

VPC workflow 배포가 완료되면 Public Subnet에 EC2를 배포하고 테스트해보도록 한다.
배포에는 SubnetID, SGID, AMI ID 그리고 접속에 필요한 Keypair을 사용하여 배포를 진행한다. 배포 이전에는 bastion ec2와 Node Group에 속한 EC2들만 확인이 되고 배포를 진행하면 추가로 테스트 EC2가 보이는 것을 확인할 수 있다.

# public 서브넷 ID 확인
PUBSUB1=$(kubectl get subnets tutorial-public-subnet1 -o jsonpath={.status.subnetID})
echo $PUBSUB1
subnet-03cd0923f8663f1ee

# 보안그룹 ID 확인
TSG=$(kubectl get securitygroups tutorial-security-group -o jsonpath={.status.id})
echo $TSG
sg-0992f933b69408947

# Amazon Linux 2 최신 AMI ID 확인
AL2AMI=$(aws ec2 describe-images --owners amazon --filters "Name=name,Values=amzn2-ami-hvm-2.0.*-x86_64-gp2" --query 'Images[0].ImageId' --output text)
echo $AL2AMI
ami-0a0453c1a1758acf1

# 각자 자신의 SSH 키페어 이름 변수 지정
MYKEYPAIR=<각자 자신의 SSH 키페어 이름>
MYKEYPAIR=aewspair

# 변수 확인 > 특히 서브넷 ID가 확인되었는지 꼭 확인하자!
echo $PUBSUB1 , $TSG , $AL2AMI , $MYKEYPAIR
subnet-03cd0923f8663f1ee , sg-0992f933b69408947 , ami-0a0453c1a1758acf1 , aewspair

# [터미널1] 모니터링
while true; do 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; date ; sleep 1 ; done
+----------------+-----------------+------------------+----------+
|  InstanceName  |  PrivateIPAdd   |   PublicIPAdd    | Status   |
+----------------+-----------------+------------------+----------+
|  myeks-ng1-Node|  192.168.3.165  |  54.180.xxx.xxx  |  running |
|  myeks-ng1-Node|  192.168.2.5    |  3.35.xxx.xxx    |  running |
|  myeks-ng1-Node|  192.168.1.10   |  43.201.xxx.xxx  |  running |
|  myeks-bastion |  192.168.1.100  |  54.180.xxx.xxx  |  running |
+----------------+-----------------+------------------+----------+
Sun Jun  4 22:09:42 KST 2023

# public 서브넷에 인스턴스 생성
cat <<EOF > tutorial-bastion-host.yaml
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: Instance
metadata:
  name: tutorial-bastion-host
spec:
  imageID: $AL2AMI # AL2 AMI ID - ap-northeast-2
  instanceType: t3.medium
  subnetID: $PUBSUB1
  securityGroupIDs:
  - $TSG
  keyName: $MYKEYPAIR
  tags:
    - key: producer
      value: ack
EOF
kubectl apply -f tutorial-bastion-host.yaml
instance.ec2.services.k8s.aws/tutorial-bastion-host created


# 인스턴스 생성 확인
kubectl get instance
NAME                    ID
tutorial-bastion-host   i-070849e21be1aa100

kubectl describe instance
Name:         tutorial-bastion-host
Namespace:    default

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
+----------------+-----------------+------------------+----------+
|  InstanceName  |  PrivateIPAdd   |   PublicIPAdd    | Status   |
+----------------+-----------------+------------------+----------+
|  myeks-ng1-Node|  192.168.3.165  |  54.180.xxx.xxx  |  running |
|  myeks-ng1-Node|  192.168.2.5    |  3.35.xxx.xxx    |  running |
|  myeks-ng1-Node|  192.168.1.10   |  43.201.xxx.xxx  |  running |
|  myeks-bastion |  192.168.1.100  |  54.180.xxx.xxx  |  running |
|  None          |  10.0.5.137     |  13.209.xxx.xxx  |  running |
+----------------+-----------------+------------------+----------+
Sun Jun  4 22:10:42 KST 2023

Public Subnet에 EC2를 배포했으니 Client에서 해당 EC2에 접속하고 egress(ping 8.8.8.8)을 테스트해보았다.
현재 SG에는 egress 허용이 되어있지 않아 8.8.8.8로 Ping이 나가지 않는 것을 확인할 수 있다.

ssh -i <자신의 키페어파일> ec2-user@<public 서브넷에 인스턴스 퍼블릭IP>
------
# public 서브넷에 인스턴스 접속 후 외부 인터넷 통신 여부 확인 
ping -c 2 8.8.8.8
exit
------

SG에 대한 egress 설정도 할 겸 ACK Controller을 활용해서 생성만 진행했으니 업데이트를 진행해보도록 한다. 설정은 기존 SG에 0.0.0.0/0에 대해 Egress 허용 정책을 추가하였다.

# SG egress 설정
cat <<EOF > modify-sg.yaml
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: SecurityGroup
metadata:
  name: tutorial-security-group
spec:
  description: "ack security group"
  name: tutorial-sg
  vpcRef:
     from:
       name: tutorial-vpc
  ingressRules:
    - ipProtocol: tcp
      fromPort: 22
      toPort: 22
      ipRanges:
        - cidrIP: "0.0.0.0/0"
          description: "ingress"
  egressRules:
    - ipProtocol: '-1'
      ipRanges:
        - cidrIP: "0.0.0.0/0"
          description: "egress"
EOF
kubectl apply -f modify-sg.yaml
securitygroup.ec2.services.k8s.aws/tutorial-security-group configured

# 변경 확인 >> 보안그룹에 아웃바운드 규칙 확인
kubectl logs -n $ACK_SYSTEM_NAMESPACE -l k8s-app=ec2-chart -f
2023-06-04T13:16:36.970Z	INFO	ackrt	desired resource state has changed	{"account": "MyAccount", "role": "", "region": "ap-northeast-2", "kind": "SecurityGroup", "namespace": "default", "name": "tutorial-security-group", "is_adopted": false, "generation": 2, "diff": [{"Path":{"Parts":["Spec","EgressRules"]},"A":[{"ipProtocol":"-1","ipRanges":[{"cidrIP":"0.0.0.0/0","description":"egress"}]}],"B":null}]}
2023-06-04T13:16:37.217Z	INFO	ackrt	updated resource	{"account": "MyAccount", "role": "", "region": "ap-northeast-2", "kind": "SecurityGroup", "namespace": "default", "name": "tutorial-security-group", "is_adopted": false, "generation": 2}

SG Egress 설정 업데이트를 했으니 다시 8.8.8.8에 대한 ping 확인도 진행해보았다. 정상적으로 ping이 나가는 것을 보아 Egress 정책이 잘 적용되었음을 확인할 수 있었다. 그리고 EC2의 IP 정보를 확인하였고 이때 Public IP가 정상적으로 호출되는 것을 확인할 수 있었다.

ssh -i <자신의 키페어파일> ec2-user@<public 서브넷에 인스턴스 퍼블릭IP>
------
# public 서브넷에 인스턴스 접속 후 외부 인터넷 통신 여부 확인 
ping -c 10 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=46 time=17.0 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=46 time=17.1 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=46 time=17.1 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=46 time=17.1 ms
...
--- 8.8.8.8 ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 9011ms
rtt min/avg/max/mdev = 17.089/17.135/17.164/0.156 ms

curl ipinfo.io/ip ; echo
13.209.xxx.xxx
exit
------

Public Subnet에 EC2을 배포하고 테스트해봤으니 이번에는 Private Subnet에 인스턴스를 배포하고 테스트해보도록 한다.
Private Subnet에 배포하는 것도 Public Subnet에 배포하는 것과 동일하게 SubnetID, SGID, AMIID, Keypir가 필요하다.
배포하고 내용을 조회해보면 Public IP 없이 Private IP만 갖은 채로 생성된 것을 확인할 수 있다.

# private 서브넷 ID 확인
PRISUB1=$(kubectl get subnets tutorial-private-subnet1 -o jsonpath={.status.subnetID})
echo $PRISUB1
subnet-0997be6d046a7f8c7

# 변수 확인 > 특히 private 서브넷 ID가 확인되었는지 꼭 확인하자!
echo $PRISUB1 , $TSG , $AL2AMI , $MYKEYPAIR
subnet-0997be6d046a7f8c7 , sg-0992f933b69408947 , ami-0a0453c1a1758acf1 , aewspair

# private 서브넷에 인스턴스 생성
cat <<EOF > tutorial-instance-private.yaml
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: Instance
metadata:
  name: tutorial-instance-private
spec:
  imageID: $AL2AMI # AL2 AMI ID - ap-northeast-2
  instanceType: t3.medium
  subnetID: $PRISUB1
  securityGroupIDs:
  - $TSG
  keyName: $MYKEYPAIR
  tags:
    - key: producer
      value: ack
EOF
kubectl apply -f tutorial-instance-private.yaml
instance.ec2.services.k8s.aws/tutorial-instance-private created

# 인스턴스 생성 확인 (위에서 만든 Public Subnet EC2는 bastion, 방금 만든 EC2는 instance-private)
kubectl get instance
NAME                        ID
tutorial-bastion-host       i-070849e21be1aa100
tutorial-instance-private   i-088273052920e222b

kubectl describe instance
Name:         tutorial-instance-private
Namespace:    default

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
+----------------+-----------------+------------------+----------+
|  InstanceName  |  PrivateIPAdd   |   PublicIPAdd    | Status   |
+----------------+-----------------+------------------+----------+
|  myeks-ng1-Node|  192.168.3.165  |  54.180.xxx.xxx  |  running |
|  myeks-ng1-Node|  192.168.2.5    |  3.35.xxx.xxx    |  running |
|  myeks-ng1-Node|  192.168.1.10   |  43.201.xxx.xxx  |  running |
|  myeks-bastion |  192.168.1.100  |  54.180.xxx.xxx  |  running |
|  None          |  10.0.5.137     |  13.209.xxx.xxx  |  running |
|  None          |  10.0.128.130   |  None            |  running |
+----------------+-----------------+------------------+----------+

Private Subnet에 배포 된 EC2에는 현재 환경에선 곧바로 접속할 수 있는 방법이 없다. VPN 등을 연결하면 가능하나 현재 VPC Workflow에는 그런 구성이 되어있지 않으니 기존에 배포한 Public Subnet의 EC2에 SSH 터널링을 설정해서 해당 EC2을 거쳐 Private Subnet EC2에 접속하도록 한다.
이때 2개의 터미널 창을 사용할 예정인데 EKS Bastion에 접속한 터미널1, 모니터용 터미널2을 제외한 SSH 터널링용 터미널3와 Private Subnet EC2에 SSH 터널링을 타고 접속할 터미널4을 사용한다.

SSH 터널링을 통해 Private EC2에 접속을 하게 되면 8.8.8.8에 대한 Egress도 제대로 작동하고 ss -tnp을 통해 연결 된 포트를 확인해보면 Public EC2와 22번 Port을 통해 연결 된 것을 확인할 수 있다.
그리고 IP을 확인하면 Public IP을 확인할 수 있는데 해당 IP는 Public EC2의 IP가 아닌 Private EC2가 외부로 나갈 때 사용하는 NAT GW의 Public IP이니 참고하자.

# [터미널3] SSH 터널링 설정
ssh -i <자신의 키페어파일> -L <자신의 임의 로컬 포트>:<private 서브넷의 인스턴스의 private ip 주소>:22 ec2-user@<public 서브넷에 인스턴스 퍼블릭IP> -v
ssh -i aewspair.pem -L 9999:10.0.128.130:22 ec2-user@13.209.xxx.xxx -v
---

# [터미널4] SSH 터널링 통해 Private Subnet EC2 접속
ssh -i <자신의 키페어파일> -p <자신의 임의 로컬 포트> ec2-user@localhost
ssh -i aewspair.pem -p 9999 ec2-user@localhost
---
# IP 및 네트워크 정보 확인
ip -c addr
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP group default qlen 1000
    link/ether 02:93:25:a2:64:64 brd ff:ff:ff:ff:ff:ff
    inet 10.0.128.130/20 brd 10.0.143.255 scope global dynamic eth0

sudo ss -tnp
State       Recv-Q       Send-Q              Local Address:Port                Peer Address:Port        Process
ESTAB       0            0                    10.0.128.130:22                    10.0.5.137:38416        users:(("sshd",pid=2516,fd=3),("sshd",pid=2499,fd=3))

ping -c 2 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=103 time=28.7 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=103 time=27.9 ms
--- 8.8.8.8 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 27.994/28.396/28.799/0.436 ms

curl ipinfo.io/ip ; echo 
43.201.xxx.xxx
exit
---

실습이 완료됐으니 리소스 삭제를 진행한다.

# 리소스 삭제
kubectl delete -f tutorial-bastion-host.yaml && kubectl delete -f tutorial-instance-private.yaml
kubectl delete -f vpc-workflow.yaml  # vpc 관련 모든 리소스들 삭제에는 다소 시간이 소요됨

# 리소스 삭제 확인
kubectl get instance
No resources found in default namespace.

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
+----------------+-----------------+------------------+----------+
|  InstanceName  |  PrivateIPAdd   |   PublicIPAdd    | Status   |
+----------------+-----------------+------------------+----------+
|  myeks-ng1-Node|  192.168.3.165  |  54.180.xxx.xxx  |  running |
|  myeks-ng1-Node|  192.168.2.5    |  3.35.xxx.xxx    |  running |
|  myeks-ng1-Node|  192.168.1.10   |  43.201.xxx.xxx  |  running |
|  myeks-bastion |  192.168.1.100  |  54.180.xxx.xxx  |  running |
+----------------+-----------------+------------------+----------+

1-3. ACK for RDS

S3와 EC2과 같이 AWS에서 많이 쓰이는 서비스인 RDS에 대해서도 실습을 진행해보려고 한다. RDS의 엔진 대부분을 ACK에서 지원하는데 이번 실습에서는 RDS for MariaDB으로 실습을 진행한다.
다른 서비스들과 마찬가지로 ACK Controller 설치 및 IRSA 구성과 서비스 배포, 업데이트 그리고 삭제까지 진행할 예정이다.

# 서비스명 변수 지정 및 helm 차트 다운로드
export SERVICE=rds
export RELEASE_VERSION=$(curl -sL https://api.github.com/repos/aws-controllers-k8s/$SERVICE-controller/releases/latest | grep '"tag_name":' | cut -d'"' -f4 | cut -c 2-)
helm pull oci://public.ecr.aws/aws-controllers-k8s/$SERVICE-chart --version=$RELEASE_VERSION
tar xzvf $SERVICE-chart-$RELEASE_VERSION.tgz

# helm chart 확인
tree ~/$SERVICE-chart

# ACK RDS-Controller 설치
export ACK_SYSTEM_NAMESPACE=ack-system
export AWS_REGION=ap-northeast-2
helm install -n $ACK_SYSTEM_NAMESPACE ack-$SERVICE-controller --set aws.region="$AWS_REGION" ~/$SERVICE-chart

# 설치 확인
helm list --namespace $ACK_SYSTEM_NAMESPACE
NAME              	NAMESPACE 	REVISION	UPDATED                                	STATUS  	CHART          	APP VERSION
ack-ec2-controller	ack-system	1       	2023-06-04 21:46:39.80730411 +0900 KST 	deployed	ec2-chart-1.0.3	1.0.3
ack-rds-controller	ack-system	1       	2023-06-05 08:24:20.821985798 +0900 KST	deployed	rds-chart-1.1.4	1.1.4

kubectl -n $ACK_SYSTEM_NAMESPACE get pods -l "app.kubernetes.io/instance=ack-$SERVICE-controller"
NAME                                            READY   STATUS              RESTARTS   AGE
ack-rds-controller-rds-chart-6d59dfdfd7-k2mnl   0/1     ContainerCreating   0          2s

kubectl get crd | grep $SERVICE
dbclusterparametergroups.rds.services.k8s.aws   2023-06-04T23:24:19Z
dbclusters.rds.services.k8s.aws                 2023-06-04T23:24:19Z
dbinstances.rds.services.k8s.aws                2023-06-04T23:24:20Z
dbparametergroups.rds.services.k8s.aws          2023-06-04T23:24:20Z
dbproxies.rds.services.k8s.aws                  2023-06-04T23:24:20Z
dbsubnetgroups.rds.services.k8s.aws             2023-06-04T23:24:20Z
globalclusters.rds.services.k8s.aws             2023-06-04T23:24:20Z

ACK Controller 설치를 완료했다면 IRSA 설정을 진행한다. 권한은 AmazonRDSFullAccess을 부여했다.

# Create an iamserviceaccount - AWS IAM role bound to a Kubernetes service account
eksctl create iamserviceaccount \
  --name ack-$SERVICE-controller \
  --namespace $ACK_SYSTEM_NAMESPACE \
  --cluster $CLUSTER_NAME \
  --attach-policy-arn $(aws iam list-policies --query 'Policies[?PolicyName==`AmazonRDSFullAccess`].Arn' --output text) \
  --override-existing-serviceaccounts --approve

# 확인 >> 웹 관리 콘솔에서 CloudFormation Stack >> IAM Role 확인
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
NAMESPACE	NAME				ROLE ARN
ack-system	ack-ec2-controller		arn:aws:iam::MyAccount:role/eksctl-myeks-addon-iamserviceaccount-ack-sys-Role1-VK23PW7Y288N
ack-system	ack-rds-controller		arn:aws:iam::MyAccount:role/eksctl-myeks-addon-iamserviceaccount-ack-sys-Role1-AUTL0REK98XM
ack-system	ack-s3-controller		arn:aws:iam::MyAccount:role/eksctl-myeks-addon-iamserviceaccount-ack-sys-Role1-XFQ0LIYFYJ3Z

# Inspecting the newly created Kubernetes Service Account, we can see the role we want it to assume in our pod.
kubectl get sa -n $ACK_SYSTEM_NAMESPACE
NAME                 SECRETS   AGE
ack-ec2-controller   0         10h
ack-rds-controller   0         5m2s

kubectl describe sa ack-$SERVICE-controller -n $ACK_SYSTEM_NAMESPACE
Name:                ack-rds-controller
Namespace:           ack-system
Labels:              app.kubernetes.io/instance=ack-rds-controller
                     app.kubernetes.io/managed-by=eksctl
                     app.kubernetes.io/name=rds-chart

# Restart ACK service controller deployment using the following commands.
kubectl -n $ACK_SYSTEM_NAMESPACE rollout restart deploy ack-$SERVICE-controller-$SERVICE-chart
deployment.apps/ack-rds-controller-rds-chart restarted

# IRSA 적용으로 Env, Volume 추가 확인
kubectl describe pod -n $ACK_SYSTEM_NAMESPACE -l k8s-app=$SERVICE-chart
Name:             ack-rds-controller-rds-chart-5dfbf7dccb-dlwdv
Namespace:        ack-system
Priority:         0
Service Account:  ack-rds-controller
Node:             ip-192-168-2-5.ap-northeast-2.compute.internal/192.168.2.5
...

RDS for MariaDB 생성테스트를 진행해본다. DB 생성에 필요한 secret 생성을 먼저 생성하고 RDS을 배포했다.

# DB 암호를 위한 secret 생성
RDS_INSTANCE_NAME="<your instance name>"
RDS_INSTANCE_PASSWORD="<your instance password>"
RDS_INSTANCE_NAME=myrds
RDS_INSTANCE_PASSWORD=qwe12345
kubectl create secret generic "${RDS_INSTANCE_NAME}-password" --from-literal=password="${RDS_INSTANCE_PASSWORD}"
secret/myrds-password created

# 확인
kubectl get secret $RDS_INSTANCE_NAME-password
NAME             TYPE     DATA   AGE
myrds-password   Opaque   1      10s

# [터미널1] 모니터링
RDS_INSTANCE_NAME=myrds
watch -d "kubectl describe dbinstance "${RDS_INSTANCE_NAME}" | grep 'Db Instance Status'"

# RDS 배포 생성 : 15분 이내 시간 소요 >> 보안그룹, 서브넷 등 필요한 옵션들은 추가해서 설정해보자!
cat <<EOF > rds-mariadb.yaml
apiVersion: rds.services.k8s.aws/v1alpha1
kind: DBInstance
metadata:
  name: "${RDS_INSTANCE_NAME}"
spec:
  allocatedStorage: 20
  dbInstanceClass: db.t4g.micro
  dbInstanceIdentifier: "${RDS_INSTANCE_NAME}"
  engine: mariadb
  engineVersion: "10.6"
  masterUsername: "admin"
  masterUserPassword:
    namespace: default
    name: "${RDS_INSTANCE_NAME}-password"
    key: password
EOF
kubectl apply -f rds-mariadb.yaml

# 생성 확인
kubectl get dbinstances  ${RDS_INSTANCE_NAME}
NAME    STATUS
myrds   creating

kubectl describe dbinstance "${RDS_INSTANCE_NAME}"
Name:         myrds
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  rds.services.k8s.aws/v1alpha1
...

aws rds describe-db-instances --db-instance-identifier $RDS_INSTANCE_NAME | jq
  Db Instance Status:         available

# 생성 완료 대기 : for 지정 상태가 완료되면 정상 종료됨
kubectl wait dbinstances ${RDS_INSTANCE_NAME} --for=condition=ACK.ResourceSynced --timeout=15m
dbinstance.rds.services.k8s.aws/myrds condition met

MariaDB 배포가 완료됐다면 RDS 연결하는 Pod를 배포하고 해당 Pod에서 RDS의 정보를 제대로 받아오는지를 테스트할 예정이다.
이때 FieldExport는 ACK의 컨트롤러 구성 파일인 awscdk.FieldExport을 사용할 예정이다. FieldExport는 CDK(Cloud Development Kit)를 사용하여 AWS 리소스를 프로비저닝하고 관리할 때 사용한다.
FieldExport를 사용하면 AWS 리소스의 특정 속성을 다른 스택이나 리소스에서 참조할 수 있고 일반적으로 리소스의 출력값을 다른 리소스의 입력값으로 전달하거나 스택 간에 데이터를 공유하는 데 사용한다. 이 특성을 활용하여 k8s Pod에서 좀 전에 배포한 RDS의 변수를 받아올 수 있도록 할 예정이다.
아래 내용이 fieldexport을 사용해서 위에서 배포한 RDS에 대한 configmap을 설정하는 내용이다.

# Configmap 구성 전 Configmap 확인
kubectl get configmaps
NAME               DATA   AGE
kube-root-ca.crt   1      12h

# Configmap 구성
RDS_INSTANCE_CONN_CM="${RDS_INSTANCE_NAME}-conn-cm"

cat <<EOF > rds-field-exports.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: ${RDS_INSTANCE_CONN_CM}
data: {}
---
apiVersion: services.k8s.aws/v1alpha1
kind: FieldExport
metadata:
  name: ${RDS_INSTANCE_NAME}-host
spec:
  to:
    name: ${RDS_INSTANCE_CONN_CM}
    kind: configmap
  from:
    path: ".status.endpoint.address"
    resource:
      group: rds.services.k8s.aws
      kind: DBInstance
      name: ${RDS_INSTANCE_NAME}
---
apiVersion: services.k8s.aws/v1alpha1
kind: FieldExport
metadata:
  name: ${RDS_INSTANCE_NAME}-port
spec:
  to:
    name: ${RDS_INSTANCE_CONN_CM}
    kind: configmap
  from:
    path: ".status.endpoint.port"
    resource:
      group: rds.services.k8s.aws
      kind: DBInstance
      name: ${RDS_INSTANCE_NAME}
---
apiVersion: services.k8s.aws/v1alpha1
kind: FieldExport
metadata:
  name: ${RDS_INSTANCE_NAME}-user
spec:
  to:
    name: ${RDS_INSTANCE_CONN_CM}
    kind: configmap
  from:
    path: ".spec.masterUsername"
    resource:
      group: rds.services.k8s.aws
      kind: DBInstance
      name: ${RDS_INSTANCE_NAME}
EOF

kubectl apply -f rds-field-exports.yaml

# Configmap 구성 확인
NAME               DATA   AGE
kube-root-ca.crt   1      12h
myrds-conn-cm      3      2m35s

fieldexport로 Configmap 구성이 끝났다면 해당 구성 내용을 확인해본다. 위에서 설정한대로 잘 구성된 것을 확인할 수 있다.

# 상태 정보 확인 : address 와 port 정보 
kubectl get dbinstances myrds -o jsonpath={.status.endpoint} | jq
{
  "address": "myrds.cnlc7l....ap-northeast-2.rds.amazonaws.com",
  "hostedZoneID": "ZLA2NUC...",
  "port": 3306
}

# 상태 정보 확인 : masterUsername 확인
kubectl get dbinstances myrds -o jsonpath={.spec.masterUsername} ; echo
admin

# 컨피그맵 확인
kubectl get cm myrds-conn-cm -o yaml | kubectl neat | yh
apiVersion: v1
data:
  default.myrds-host: myrds.cnlc7....ap-northeast-2.rds.amazonaws.com
  default.myrds-port: "3306"
  default.myrds-user: admin
kind: ConfigMap
metadata:
  name: myrds-conn-cm
  namespace: default

# fieldexport 정보 확인
kubectl get crd | grep fieldexport
fieldexports.services.k8s.aws                   2023-06-04T12:46:39Z

kubectl get fieldexport
NAME         AGE
myrds-host   3m8s
myrds-port   3m8s
myrds-user   3m8s

kubectl get fieldexport myrds-host -o yaml | k neat | yh

RDS의 정보를 읽어올 Pod을 생성한다. Pod의 Image는 가볍게 실행할 busybox을 서낵하고 환경변수에 위에 Configmap을 구성한 내용들(HOST, PORT, USER, PASSWORD)을 참조해서 배포한다.

APP_NAMESPACE=default
cat <<EOF > rds-pods.yaml
apiVersion: v1
kind: Pod
metadata:
  name: app
  namespace: ${APP_NAMESPACE}
spec:
  containers:
   - image: busybox
     name: myapp
     command:
        - sleep
        - "3600"
     imagePullPolicy: IfNotPresent
     env:
      - name: DBHOST
        valueFrom:
         configMapKeyRef:
          name: ${RDS_INSTANCE_CONN_CM}
          key: "${APP_NAMESPACE}.${RDS_INSTANCE_NAME}-host"
      - name: DBPORT
        valueFrom:
         configMapKeyRef:
          name: ${RDS_INSTANCE_CONN_CM}
          key: "${APP_NAMESPACE}.${RDS_INSTANCE_NAME}-port"
      - name: DBUSER
        valueFrom:
         configMapKeyRef:
          name: ${RDS_INSTANCE_CONN_CM}
          key: "${APP_NAMESPACE}.${RDS_INSTANCE_NAME}-user"
      - name: DBPASSWORD
        valueFrom:
          secretKeyRef:
           name: "${RDS_INSTANCE_NAME}-password"
           key: password
EOF
kubectl apply -f rds-pods.yaml

# 생성 확인
kubectl get pod app
NAME   READY   STATUS    RESTARTS   AGE
app    1/1     Running   0          5s

# 파드의 환경 변수 확인
kubectl exec -it app -- env | grep DB
DBUSER=admin
DBPASSWORD=qwe12345
DBHOST=myrds.cnlc7l....ap-northeast-2.rds.amazonaws.com
DBPORT=3306

앞서 배포한 RDS for MariaDB의 DB 식별자를 업데이트한다. 해당 업데이트 내용을 위에서 배포한 Pod에서는 제대로 인식하는지 확인해도록 한다.
DB식별자 업데이트 명령을 ACK을 통해 진행했고 AWS Console과 터미널 화면에서 모두 변경되는 내용을 확인할 수 있었다.

# [터미널]
watch -d "kubectl get dbinstance; echo; kubectl get cm myrds-conn-cm -o yaml | kubectl neat"

apiVersion: v1
data:
  default.myrds-host: myrds.cnlc7....ap-northeast-2.rds.amazonaws.com
  default.myrds-port: "3306"
  default.myrds-user: admin

# DB 식별자를 업데이트 >> 어떤 현상이 발생하는가?
kubectl patch dbinstance myrds --type=merge -p '{"spec":{"dbInstanceIdentifier":"studyend"}}'
# 상태가 creating -> backing-up -> available로 변경되면서 DB식별자도 myrds에서 studyend로 업데이트 됨.
NAME    STATUS
myrds   creating
NAME    STATUS
myrds   backing-up
NAME    STATUS
myrds   available
apiVersion: v1
data:
  default.myrds-host: studyend.cnlc7...ap-northeast-2.rds.amazonaws.com

# 확인
kubectl get dbinstance myrds
NAME    STATUS
myrds   available

kubectl describe dbinstance myrds
Name:         myrds
Namespace:    default
...
  Endpoint:
    Address:                            studyend.cnlc7lcs9gjt.ap-northeast-2.rds.amazonaws.com
...

식별자 업데이트를 완료했으니 해당 내용을 Pod에서도 인식하는지 확인해보았다.
Pod에는 환경 변수로 해당 정보를 주입했기 때문에 내용이 반영되지 않았음을 확인할 수 있다.
이를 반영시키기 위해선 rollout으로 env 변경을 적용시키거나 삭제 후 재생성하는 방법을 사용해야 함을 알 수 있다.

# 상태 정보 확인 : address 변경 확인!
kubectl get dbinstances myrds -o jsonpath={.status.endpoint} | jq
{
  "address": "studyend.cnlc7lc....ap-northeast-2.rds.amazonaws.com",
  "hostedZoneID": "ZLA2NU...",
  "port": 3306
}

# 파드의 환경 변수 확인 >> 파드의 경우 환경 변수 env로 정보를 주입했기 때문에 변경된 정보를 확인 할 수 없다
kubectl exec -it app -- env | grep DB
DBHOST=myrds.cnlc7lc....ap-northeast-2.rds.amazonaws.com
DBPORT=3306
DBUSER=admin
DBPASSWORD=qwe12345

# 파드 삭제 후 재생성 후 확인
kubectl delete pod app && kubectl apply -f rds-pods.yaml

# 파드의 환경 변수 확인 >> 변경 정보 확인!
# 즉 deployments, daemonsets, statefulsets 의 경우 rollout 으로 env 변경 적용을 할 수 는 있겠다!
kubectl exec -it app -- env | grep DB
DBHOST=studyend.cnlc....ap-northeast-2.rds.amazonaws.com
DBPORT=3306
DBUSER=admin
DBPASSWORD=qwe12345

Pod 재배포로 해당 내용 제대로 받아오는 것을 확인했으니 실습 리소스를 삭제하도록 한다.
처음 만들어졌던 DB식별자 myrds는 AWS Console에서 직접 삭제하거나 AWS CLI로 삭제하도록 한다.

# 파드 삭제
kubectl delete pod app

# RDS 삭제 
kubectl delete -f rds-mariadb.yaml

# db식별자 myrds 삭제는 AWS CLI나 AWS Console에서 진행
aws rds delete-db-instance --db-instance-identifier myrds --skip-final-snapshot

1-4. ACK for DynamoDB

기본 실습으로 제공 된 S3, EC2, RDS 제외한 리소스를 배포해보는 것을 도전해보려고 한다.
간단하게 배포 및 테스트할 수 있는 DynamoDB를 테스트해볼 예정이다.
테스트 순서는 다른 리소스들과 동일하다.

SERVICE 변수는 dynamodb로 변경한 후 helm chart 다운로드 및 ACK Controller 설치를 진행한다.

# 서비스명 변수 지정 및 helm 차트 다운로드
export SERVICE=dynamodb
export RELEASE_VERSION=$(curl -sL https://api.github.com/repos/aws-controllers-k8s/$SERVICE-controller/releases/latest | grep '"tag_name":' | cut -d'"' -f4 | cut -c 2-)
helm pull oci://public.ecr.aws/aws-controllers-k8s/$SERVICE-chart --version=$RELEASE_VERSION
tar xzvf $SERVICE-chart-$RELEASE_VERSION.tgz

# helm chart 확인
tree ~/$SERVICE-chart

# ACK dynamodb-Controller 설치
export ACK_SYSTEM_NAMESPACE=ack-system
export AWS_REGION=ap-northeast-2
helm install -n $ACK_SYSTEM_NAMESPACE ack-$SERVICE-controller --set aws.region="$AWS_REGION" ~/$SERVICE-chart

# 설치 확인
helm list --namespace $ACK_SYSTEM_NAMESPACE
NAME                 	NAMESPACE 	REVISION	UPDATED                                	STATUS  	CHART             	APP VERSION
ack-ec2-controller   	ack-system	1       	2023-06-04 21:46:39.80730411 +0900 KST 	deployed	ec2-chart-1.0.3   	1.0.3
ack-dynamodb-controller	ack-system	1       	2023-06-05 11:22:44.98087282 +0900 KST 	deployed	dynamodb-chart-1.1.1	1.1.1
ack-rds-controller   	ack-system	1       	2023-06-05 08:24:20.821985798 +0900 KST	deployed	rds-chart-1.1.4   	1.1.4

kubectl -n $ACK_SYSTEM_NAMESPACE get pods -l "app.kubernetes.io/instance=ack-$SERVICE-controller"
NAME                                                      READY   STATUS    RESTARTS   AGE
ack-dynamodb-controller-dynamodb-chart-779c6458d8-wq7tc   1/1     Running   0          6m31s

kubectl get crd | grep $SERVICE
backups.dynamodb.services.k8s.aws               2023-06-05T02:22:44Z
globaltables.dynamodb.services.k8s.aws          2023-06-05T02:22:44Z
tables.dynamodb.services.k8s.aws                2023-06-05T02:22:44Z

IRSA도 기존 다른 서비스들과 동일하게 진행하면서 권한은 AmazonDynamoDBFullAccess로 정의한다.

# Create an iamserviceaccount - AWS IAM role bound to a Kubernetes service account
eksctl create iamserviceaccount \
  --name ack-$SERVICE-controller \
  --namespace $ACK_SYSTEM_NAMESPACE \
  --cluster $CLUSTER_NAME \
  --attach-policy-arn $(aws iam list-policies --query 'Policies[?PolicyName==`AmazonDynamoDBFullAccess`].Arn' --output text) \
  --override-existing-serviceaccounts --approve

# 확인 >> 웹 관리 콘솔에서 CloudFormation Stack >> IAM Role 확인
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
NAMESPACE	NAME				ROLE ARN
ack-system	ack-dynamodb-controller		arn:aws:iam::MyAccount:role/eksctl-myeks-addon-iamserviceaccount-ack-sys-Role1-199XY74UI3PSY
ack-system	ack-ec2-controller		arn:aws:iam::MyAccount:role/eksctl-myeks-addon-iamserviceaccount-ack-sys-Role1-VK23PW7Y288N
ack-system	ack-rds-controller		arn:aws:iam::MyAccount:role/eksctl-myeks-addon-iamserviceaccount-ack-sys-Role1-AUTL0REK98XM
ack-system	ack-s3-controller		arn:aws:iam::MyAccount:role/eksctl-myeks-addon-iamserviceaccount-ack-sys-Role1-XFQ0LIYFYJ3Z

# Inspecting the newly created Kubernetes Service Account, we can see the role we want it to assume in our pod.
kubectl get sa -n $ACK_SYSTEM_NAMESPACE
NAME                    SECRETS   AGE
ack-dynamodb-controller   0         116s
ack-ec2-controller      0         12h
ack-rds-controller      0         124m

kubectl describe sa ack-$SERVICE-controller -n $ACK_SYSTEM_NAMESPACE
Name:                ack-dynamodb-controller
Namespace:           ack-system
Labels:              app.kubernetes.io/managed-by=eksctl
Annotations:         eks.amazonaws.com/role-arn: arn:aws:iam::587122150371:role/eksctl-myeks-addon-iamserviceaccount-ack-sys-Role1-199XY74UI3PSY

# Restart ACK service controller deployment using the following commands.
kubectl -n $ACK_SYSTEM_NAMESPACE rollout restart deploy ack-$SERVICE-controller-$SERVICE-chart
deployment.apps/ack-dynamodb-controller-dynamodb-chart restarted

# IRSA 적용으로 Env, Volume 추가 확인
kubectl describe pod -n $ACK_SYSTEM_NAMESPACE -l k8s-app=$SERVICE-chart
Name:                      ack-dynamodb-controller-dynamodb-chart-779c6458d8-wq7tc
Namespace:                 ack-system
Priority:                  0
Service Account:           ack-dynamodb-controller
Node:                      ip-192-168-1-10.ap-northeast-2.compute.internal/192.168.1.10
...

ACK Controller 설치 및 IRSA 설정이 완료됐다면 DynamoDB을 배포하도록 한다.
DynamoDB을 배포하고 제대로 배포가 됐는지 테스트해봤다.

# DynamoDB 배포
cat <<EOF > dynamodb.yaml
apiVersion: dynamodb.services.k8s.aws/v1alpha1
kind: Table
metadata:
  name: my-dynamodb-table
  namespace: ack-system
spec:
  tableName: my-dynamodb-table
  billingMode: PAY_PER_REQUEST
  attributeDefinitions:
    - attributeName: id
      attributeType: S
  keySchema:
    - attributeName: id
      keyType: HASH
EOF
kubectl apply -f dynamodb.yaml
table.dynamodb.services.k8s.aws/my-dynamodb-table created

# AWS CLI 통해 배포 확인
aws dynamodb list-tables
{
    "TableNames": [
        "my-dynamodb-table"
    ]
}

# kubectl ACK 통해 확인
kubectl get table -n ack-system
NAME                CLASS   STATUS   SYNCED   AGE
my-dynamodb-table           ACTIVE   True     2m36s

배포와 확인까지 ACK을 통해 잘 진행했다면 이제 dynamodb 테이블에 업데이트도 진행해본다. 간단하게 S3 때와 마찬가지로 Tag 생성을 해보았다.
배포할 때 사용한 내용과 유사하게 만들고 태그값만 추가해줬다. 문제없이 Tag가 추가된 것을 확인할 수 있었다.

# Tag을 입력하기 위한 Yaml 작성 및 배포
cat <<EOF > dynamodb-update.yaml
apiVersion: dynamodb.services.k8s.aws/v1alpha1
kind: Table
metadata:
  name: my-dynamodb-table
  namespace: ack-system
spec:
  tableName: my-dynamodb-table
  billingMode: PAY_PER_REQUEST
  attributeDefinitions:
    - attributeName: id
      attributeType: S
  keySchema:
    - attributeName: id
      keyType: HASH
  tags:
    - key: myTagKey
      value: myNewTagValue
EOF
kubectl apply -f dynamodb-update.yaml

# dynamodb Tag 확인
ddbARN=$(aws dynamodb describe-table --table-name my-dynamodb-table --query 'Table.TableArn' --output text)
echo $ddbARN

aws dynamodb list-tags-of-resource --resource-arn $ddbARN
{
    "Tags": [
        {
            "Key": "myTagKey",
            "Value": "myNewTagValue"
        },
        {
            "Key": "services.k8s.aws/controller-version",
            "Value": "dynamodb-v1.1.1"
        },
        {
            "Key": "services.k8s.aws/namespace",
            "Value": "ack-system"
        }
    ]
}

 

간단하게 dynamodb에 대해 테스트를 진행해보았다.
다른 서비스들과 같이 dynamodb을 삭제하는 것까지해서 테스트를 마무리하려 한다.

# dynamodb table 삭제
kubectl delete table my-dynamodb-table -n ack-system
table.dynamodb.services.k8s.aws "my-dynamodb-table" deleted

kubectl get table -n ack-system
No resources found in ack-system namespace.

aws dynamodb list-tables
{
    "TableNames": []
}

# ACK ddb controller 및 IRSA 삭제
helm uninstall -n $ACK_SYSTEM_NAMESPACE ack-$SERVICE-controller
kubectl delete -f ~/$SERVICE-chart/crds
eksctl delete iamserviceaccount --cluster myeks --name ack-$SERVICE-controller --namespace ack-system

dynamodb 까지 포함해서 ACK을 통해 AWS 리소스를 배포하고 업데이트/삭제까지 진행을 해보았다. 아직은 조금 부족한 Controller라고 느껴진 게 동기화도 조금 느렸고 ACK로 배포하고 나서 AWS Console에서 수정하면 해당 내용은 반영되지 않는 등 아직은 정합성이 조금 아쉬운 느낌이다.

2. Flux

Flux CLI을 설치해서 GitOps 관리를 해보려고 한다.
Flux는 k8s Cluster에서 GitOps 방식으로 애플리케이션 배포 및 관리를 자동화하기 위한 도구이다. GitOps는 애플리케이션의 상태 및 구성을 Git 저장소에 기록하고, 이를 통해 모든 변경 사항을 추적하고 배포하는 DevOps 방법론인데 Flux는 이러한 GitOps 워크플로우를 간편하게 구현하도록 지원한다.
Flux는 Kubernetes 클러스터에서 실행되며, Helm과 같은 패키지 관리자와 함께 사용할 수 있다. Flux는 Kubernetes의 Custom Resource Definition을 사용하여 Flux의 구성 및 동작을 설명하는 YAML 파일을 정의한다.
GitOps를 통해 애플리케이션 배포와 관리를 자동화하고, 구성 관리를 통해 신뢰성과 일관성을 확보할 수 있다. Flux는 이러한 GitOps 워크플로우를 구현하는 데 도움을 주는 강력한 도구 중 하나이다
Flux의 특징은 다음과 같다.

  1. GitOps Workflow: Flux는 애플리케이션의 배포 및 구성 정보를 Git 저장소에 저장하고 이를 통해 변경 이력을 관리하고, 모든 변경은 Git 저장소를 통해 추적한다.
  2. 자동 배포 및 롤백: Flux는 Git 저장소의 변경 사항을 감지하고, 변경된 내용을 기반으로 자동으로 애플리케이션을 배포한다. 롤백도 Git 저장소의 이전 상태로 간단히 수행할 수 있다.
  3. Declarative Configuration: Flux는 Kubernetes의 Custom Resource Definition(CRD)을 사용하여 애플리케이션 배포에 대한 선언적인 구성을 제공한다. 이를 통해 애플리케이션 및 인프라스트럭처의 상태를 코드로 관리할 수 있다.
  4. 다중 환경 및 브랜치 관리: Flux는 여러 개발 환경(예: 개발, 스테이징, 프로덕션) 및 Git 브랜치에 대한 배포를 지원한다. 이를 통해 개발자는 각 환경 및 브랜치에 맞는 애플리케이션 구성을 유지할 수 있다.
  5. Synchronization: Flux는 Kubernetes 클러스터와 Git 저장소 간에 지속적인 동기화를 유지한다. 즉, Git 저장소의 변경 사항을 즉시 반영하고, 클러스터와 저장소 간의 일관성을 유지한다.
  6. Hooks 및 Automation: Flux는 이벤트 트리거(Hooks)를 통해 배포 이벤트를 자동화할 수 있다. 예를 들어, 애플리케이션 배포 후에 특정 작업을 수행하거나, 외부 도구와의 통합을 위한 작업을 자동으로 실행할 수 있다.

Flux을 통해 GitOps Workflow을 하기 위해 Flux CLI설치를 먼저 진행한다.
설치 시에 github Token 정보를 등록하는데 나는 모든 권한을 부여한 Token을 생성했다.

# Flux CLI 설치
curl -s https://fluxcd.io/install.sh | sudo bash
[INFO]  Downloading metadata https://api.github.com/repos/fluxcd/flux2/releases/latest
[INFO]  Using 2.0.0-rc.5 as release
[INFO]  Downloading hash https://github.com/fluxcd/flux2/releases/download/v2.0.0-rc.5/flux_2.0.0-rc.5_checksums.txt
[INFO]  Downloading binary https://github.com/fluxcd/flux2/releases/download/v2.0.0-rc.5/flux_2.0.0-rc.5_linux_amd64.tar.gz
[INFO]  Verifying binary download
which: no shasum in (/sbin:/bin:/usr/sbin:/usr/bin)
[INFO]  Installing flux to /usr/local/bin/flux
. <(flux completion bash)

# 버전 확인
flux --version
flux version 2.0.0-rc.5

# 자신의 Github 토큰과 유저이름 변수 지정
export GITHUB_TOKEN=<your-token>
export GITHUB_USER=<your-username>
export GITHUB_TOKEN=ghp_###
export GITHUB_USER=myname

# Bootstrap
## Creates a git repository fleet-infra on your GitHub account.
## Adds Flux component manifests to the repository.
## Deploys Flux Components to your Kubernetes Cluster.
## Configures Flux components to track the path /clusters/my-cluster/ in the repository.
flux bootstrap github \
  --owner=$GITHUB_USER \
  --repository=fleet-infra \
  --branch=main \
  --path=./clusters/my-cluster \
  --personal
✔ Kustomization reconciled successfully
► confirming components are healthy
✔ helm-controller: deployment ready
✔ kustomize-controller: deployment ready
✔ notification-controller: deployment ready
✔ source-controller: deployment ready
✔ all components are healthy

# 설치 확인
kubectl get pods -n flux-system
NAME                                       READY   STATUS    RESTARTS   AGE
helm-controller-fbdd59577-chxns            1/1     Running   0          48s
kustomize-controller-6b67b54cf8-mbb8z      1/1     Running   0          48s
notification-controller-78f4869c94-tftpm   1/1     Running   0          48s
source-controller-75db64d9f7-rdjw8         1/1     Running   0          48s

kubectl get-all -n flux-system
kubectl get crd | grep fluxc
alerts.notification.toolkit.fluxcd.io           2023-06-05T10:19:45Z
buckets.source.toolkit.fluxcd.io                2023-06-05T10:19:45Z
gitrepositories.source.toolkit.fluxcd.io        2023-06-05T10:19:45Z
helmcharts.source.toolkit.fluxcd.io             2023-06-05T10:19:45Z
helmreleases.helm.toolkit.fluxcd.io             2023-06-05T10:19:46Z
helmrepositories.source.toolkit.fluxcd.io       2023-06-05T10:19:46Z
kustomizations.kustomize.toolkit.fluxcd.io      2023-06-05T10:19:46Z
ocirepositories.source.toolkit.fluxcd.io        2023-06-05T10:19:46Z
providers.notification.toolkit.fluxcd.io        2023-06-05T10:19:46Z
receivers.notification.toolkit.fluxcd.io        2023-06-05T10:19:46Z

kubectl get gitrepository -n flux-system
NAME          URL                                       AGE   READY   STATUS
flux-system   ssh://git@github.com/myname/fleet-infra   74s   True    stored artifact for revision 'main@sha1:ede8721252d83c8f4....'

FluxCLI 설치가 완료됐으니 gitops 도구와 대시보드 설치를 진행한다.

# gitops 도구 설치
curl --silent --location "https://github.com/weaveworks/weave-gitops/releases/download/v0.24.0/gitops-$(uname)-$(uname -m).tar.gz" | tar xz -C /tmp
sudo mv /tmp/gitops /usr/local/bin
gitops version
Current Version: 0.24.0
GitCommit: cc1d0e680c55e0aaf5bfa0592a0a454fb2064bc1
BuildTime: 2023-05-24T16:29:14Z
Branch: releases/v0.24.0

# flux 대시보드 설치
PASSWORD="password"
gitops create dashboard ww-gitops --password=$PASSWORD
✔ Flux &{v2.0.0-rc.5  flux-system} is already installed
► Applying GitOps Dashboard manifests
► Installing the GitOps Dashboard ...
✔ GitOps Dashboard has been installed
► Request reconciliation of dashboard (timeout 3m0s) ...
◎ Waiting for GitOps Dashboard reconciliation
✔ GitOps Dashboard ww-gitops is ready
✔ Installed GitOps Dashboard

# 확인
flux -n flux-system get helmrelease
NAME     	REVISION	SUSPENDED	READY	MESSAGE
ww-gitops	4.0.22  	False    	True 	Release reconciliation succeeded

kubectl -n flux-system get pod,svc

gitops 대쉬보드에 접근하기 위해 Ingress 설정을 해준다. 이후 도메인 주소에 연결하면 대쉬보드 설치 및 Ingress 설정이 제대로 된 것을 확인할 수 있다.

CERT_ARN=`aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text`
echo $CERT_ARN

# Ingress 설정

cat <<EOT > gitops-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: gitops-ingress
  annotations:
    alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
    alb.ingress.kubernetes.io/group.name: study
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
    alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/ssl-redirect: "443"
    alb.ingress.kubernetes.io/success-codes: 200-399
    alb.ingress.kubernetes.io/target-type: ip
spec:
  ingressClassName: alb
  rules:
  - host: gitops.$MyDomain
    http:
      paths:
      - backend:
          service:
            name: ww-gitops-weave-gitops
            port:
              number: 9001
        path: /
        pathType: Prefix
EOT
kubectl apply -f gitops-ingress.yaml -n flux-system

# 배포 확인
kubectl get ingress -n flux-system

# GitOps 접속 정보 확인 >> 웹 접속 후 정보 확인
echo -e "GitOps Web https://gitops.$MyDomain"

github에 있는 nginx manifest를 k8s에 배포한다. 이때 샘플 소스는 악분님의 git repo을 사용했다.

# 소스 생성 : 유형 - git, helm, oci, bucket
# flux create source {소스 유형}
# 악분(최성욱)님이 준비한 repo로 git 소스 생성
GITURL="https://github.com/sungwook-practice/fluxcd-test.git"
flux create source git nginx-example1 --url=$GITURL --branch=main --interval=30s
✚ generating GitRepository source
► applying GitRepository source
✔ GitRepository source created
◎ waiting for GitRepository source reconciliation
✔ GitRepository source reconciliation completed
✔ fetched revision: main@sha1:4478b54cb...

# 소스 확인
flux get sources git
NAME          	REVISION          	SUSPENDED	READY	MESSAGE
flux-system   	main@sha1:ede87212	False    	True 	stored artifact for revision 'main@sha1:ede87212'
nginx-example1	main@sha1:4478b54c	False    	True 	stored artifact for revision 'main@sha1:4478b54c'

kubectl -n flux-system get gitrepositories
NAME             URL                                                    AGE    READY   STATUS
flux-system      ssh://git@github.com/myname/fleet-infra                137m   True    stored artifact for revision 'main@sha1:ede8721252d...'
nginx-example1   https://github.com/sungwook-practice/fluxcd-test.git   102s   True    stored artifact for revision 'main@sha1:4478b54cb7a...'

flux 애플리케이션 생성한다. 유형(kustomization) , 깃 소스 경로( —path ./nginx) → gitops 웹 대시보드에서 확인을 진행했다.
gitops 대시보드에 nginx-example1이 생성된 것을 확인할 수 있었다.

# [터미널] 모니터링
watch -d kubectl get pod,svc nginx-example1

# flux 애플리케이션 생성 : nginx-example1
flux create kustomization nginx-example1 --target-namespace=default --interval=1m --source=nginx-example1 --path="./nginx" --health-check-timeout=2m

# 확인
kubectl get pod,svc nginx-example1
kubectl get kustomizations -n flux-system
flux get kustomizations

이렇게 간단하게 Flux 테스트를 진행했으니 Flux 실습 리소스를 삭제한다.

# [터미널] 모니터링
watch -d kubectl get pod,svc nginx-example1

# flux 애플리케이션 삭제 >> 파드와 서비스는? flux 애플리케이션 생성 시 --prune 옵션 false(default 값)
flux delete kustomization nginx-example1
# Pod와 서비스는 삭제되지 않고 Application만 삭제 된다. --prune 옵션 false이기 때문에! 다만, gitops 대시보드에는 삭제 된 것으로 나타난다.
flux get kustomizations
NAME       	REVISION          	SUSPENDED	READY	MESSAGE
flux-system	main@sha1:ede87212	False    	True 	Applied revision: main@sha1:ede87212

kubectl get pod,svc nginx-example1

# flux 애플리케이션 다시 생성 :  --prune 옵션 true
flux create kustomization nginx-example1 \
  --target-namespace=default \
  --prune=true \
  --interval=1m \
  --source=nginx-example1 \
  --path="./nginx" \
  --health-check-timeout=2m

# 확인
flux get kustomizations
NAME          	REVISION          	SUSPENDED	READY	MESSAGE
flux-system   	main@sha1:ede87212	False    	True 	Applied revision: main@sha1:ede87212
nginx-example1	main@sha1:4478b54c	False    	True 	Applied revision: main@sha1:4478b54c

kubectl get pod,svc nginx-example1

# flux 애플리케이션 삭제 >> 파드와 서비스는? 
flux delete kustomization nginx-example1
# Pod와 서비스 모두 삭제 된다. --prune 옵션이 true이기 때문에!
flux get kustomizations
kubectl get pod,svc nginx-example1

# flux 소스 삭제
flux delete source git nginx-example1

# 소스 확인
flux get sources git
kubectl -n flux-system get gitrepositories

# flux 삭제

Flux에 대해 간단하게 다뤄봤는데 간편하다는 생각이 들었다. 가장 큰 장점은 k8s 클러스터와 Git 저장소 간의 동기화를 진행해주는 게 제일 큰 장점 같다.

3. GitOps with ArgoCD

ArgoCD는 지난 PKOS 스터디 때 다뤘던 내용이 있어 해당 내용을 참고해서 진행했다.

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

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

Harbor을 HelmChart을 통해 설치한다.
이때 values.yaml 파일의 일부분을 수정해야 하는데 아래 내용을 참고한다.

# 사용 리전의 인증서 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

values.yaml 파일에 넣은 도메인 주소로 접속해서 로그인을 진행한다.

로그인이 잘 됐다면 새 프로젝트를 만들어준다.

컨테이너 이미지에 Tag 설정을 한 뒤 Harbor Project에 업로드를 한다.

# 컨테이너 이미지 가져오기
docker pull nginx && docker pull busybox && docker images
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
nginx        latest    f9c14fe76d50   11 days ago   143MB
busybox      latest    8135583d97fe   2 weeks ago   4.86MB

# 태그 설정
docker tag busybox harbor.$MyDomain/aews/busybox:0.1
docker image ls
REPOSITORY                        TAG       IMAGE ID       CREATED       SIZE
nginx                             latest    f9c14fe76d50   11 days ago   143MB
busybox                           latest    8135583d97fe   2 weeks ago   4.86MB
harbor.bs-yang.com/aews/busybox   0.1       8135583d97fe   2 weeks ago   4.86MB

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

# 이미지 업로드
docker push harbor.$MyDomain/aews/busybox:0.1
The push refers to repository [harbor.bs-yang.com/aews/busybox]
9547b4c33213: Pushed
0.1: digest: sha256:5cd3db04b8be5773388576a83177aff4f40a03457a63855f4b9cbe30542b9a43 size: 528

프로젝트에 이미지가 잘 업로드 된 것을 확인했으니 업로드 된 이미지로 Deployment을 생성하는 과정을 테스트해본다.
샘플 yaml을 받은 뒤 이미지 위치를 내 Harbor Project 장소로 선택한다. 이렇게 하면 Pods을 배포할 때 위에서 설정한 이미지 저장소를 사용하게 된다. Pulling/Pulled을 참고하면 내 주소를 사용함을 알 수 있다.

# 파드 배포
curl -s -O https://raw.githubusercontent.com/junghoon2/kube-books/main/ch13/busybox-deploy.yml
sed -i "s|harbor.myweb.io/erp|harbor.$MyDomain/aews|g" busybox-deploy.yml
kubectl apply -f busybox-deploy.yml
NAME                      READY   STATUS    RESTARTS   AGE
busybox-7494977b8-bpgs7   1/1     Running   0          3s

업로드 된 이미지를 스캔하고 앞으로 업로드 될 이미지를 자동으로 스캔하게 하는 설정을 진행한다.
Harbor 대시보드에서 이미지를 선택 후 SCAN을 클릭한다. SCAN이 아직 진행되지 않았을 때는 Vulnerabilities에 Not Scanned로 표시 된다.

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

아래는 앞으로 업로드(Push) 될 이미지들을 자동으로 Scan하는 방법이다.
Project을 선택한 뒤 Configuration을 선택하고 Automatically scan images on push을 체크하고 화면 하단의 SAVE을 클릭한다.

테스트를 위해 새로운 이미지를 push해본다.
nginx에 태그 설정을 하고 push을 했더니 바로 SCAN이 실행된 것을 확인할 수 있었다.

# 태그 설정
docker tag nginx harbor.$MyDomain/aews/nginx:0.1
docker image ls
REPOSITORY                        TAG       IMAGE ID       CREATED       SIZE
nginx                             latest    f9c14fe76d50   12 days ago   143MB
harbor.bs-yang.com/aews/nginx     0.1       f9c14fe76d50   12 days ago   143MB
busybox                           latest    8135583d97fe   2 weeks ago   4.86MB
harbor.bs-yang.com/aews/busybox   0.1       8135583d97fe   2 weeks ago   4.86MB

# 이미지 업로드
docker push harbor.$MyDomain/aews/nginx:0.1


이렇게 간단하게 Harbor 테스트를 마쳤다.

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

GitLab : Git Repo을 내부에서 관리할 수 있는 서비스이다. 앞서 위 실습에서 사용한 github의 Private 버전이라고 생각하면 편리하다.
이번 실습은 생성한 파일을 GitLab Repo에 업로드하는 것을 목표로 한다.

우선 gitlab 설치를 진행한다. 설치는 특별한 내용은 없고 values.yml을 내 도메인 환경에 맞춰 변경해준다. inress annotations 부분에 group.name을 설정해주면 생성되는 서비스들을 하나의 ALB에 연결할 수 있다.

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

# 설치
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: <각자자신의도메인>             # 52줄
    https: true

  ingress:                             # 66줄~
    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}   # 각자 자신의 값으로 수정입력
      alb.ingress.kubernetes.io/success-codes: 200-399
      alb.ingress.kubernetes.io/group.name: "gitlab" # 이렇게 할 경우 4개의 Ingress을 하나의 ALB로 생성 가능<
    tls:                               # 79줄
      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

배포를 완료했으면 배포 상황을 확인해본다.
추가로, gitlab은 설치해서 사용하는 거기 때문에 root 계정의 비밀번호를 확인해서 로그인해야 한다.

# 확인 - SubCharts
# gitlab-gitaly : 웹서비스 혹은 ssh 방식으로 진행되는 깃 제목, 브랜치, 태그 등의 깃 요청 등에 대한 작업을 담당
# gitlab-gitlab-shell : https 가 아닌 ssh 방식으로 깃 명령어 실행 시 해당 요청을 처리
# gitlab-kas : gitlab agent server
# gitlab-postgresql : 유저, 권한, 이슈 등 깃랩의 메타 데이터 정보가 저장
# gitlab-redis-master : 깃랩 작업 정보는 레디스 캐시 서버를 이용하여 처리
# gitlab-sidekiq-all-in-1-v2 : 레디스와 연동하여 작업 큐 처리 용도로 사용
# gitlab-webservice-default : 깃랩 웹 서비스를 처리
helm list -n gitlab
NAME  	NAMESPACE	REVISION	UPDATED                                	STATUS  	CHART       	APP VERSION
gitlab	gitlab   	1       	2023-06-06 21:08:33.473720611 +0900 KST	deployed	gitlab-6.8.4	15.8.4

kubectl get pod,pvc,ingress,deploy,sts -n gitlab
kubectl df-pv -n gitlab
kubectl get-all -n gitlab

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

root 로그인은 가급적 사용하지 않는 게 좋기 때문에 별도의 admin 계정을 생성하고 해당 계정에 토큰값도 생성하여 터미널에서 로그인도 할 수 있도록 한다.
상단 메뉴 버튼을 클릭하고 “Admin”을 클릭하여 관리자 화면으로 이동한다.

좌측 메뉴에서 Users을 클릭하고 나오는 화면에서 “New User”을 클릭한다.

이름과 username은 사용자 임의로 입력하고 메일 주소도 입력해준다.
그리고 권한을 Regular이 아닌 Administraotr로 설정하고 하단의 “Create User”을 클릭한다.

사용자의 암호를 설정하기 위해 생성된 사용자의 화면에서 Edit을 클릭한다.

Password를 원하는 암호로 입력하고 하단의 Save Changes을 클릭한다. 이때 암호는 임시암호로 해당 사용자로 로그인하면 암호를 변경하라고 나오니 임의로 입력하도록 하자.

사용자가 생성되고 권한까지 부여가 완료됐으니 root 사용자는 로그아웃하고 해당 사용자(여기서는 bsyang)로 다시 로그인을 진행한다. 로그인 하게되면 암호를 변경하라고 나오니 암호를 변경한다.

사용자의 Token을 생성하기 위해 Admin->Users->생성 된 사용자(bsyang) 화면에서 “Impersonation Tokens”을 클릭한다.

로그인을 하고 나면 동일하게 Admin->Users->만든 사용자까지 들어간 뒤Token 이름을 입력하고 Expiration Date는 본인이 원하는 날짜까지로 선택한다. 권한은 우선 전체를 다 부여하고 하단의 Create Impersonation Token을 클릭한다.
그럼 바로 상단에 Token값이 나오는데 눈 모양 아이콘을 클릭해서 Token 값을 확인하고 옆에 복사 버튼을 클릭해서 안전한 곳에 붙여넣기 해둔다. (ex : glpat-95By……)

이제 프로젝트에 push을 하기 위해 프로젝트를 만든다. gitlab 메인화면에서 “Create a project”을 클릭하고 다음 화면에서 “Create blank project”을 클릭해서 빈 프로젝트를 생성하면 된다.
프로젝트 이름과 네임스페이스(gitlab user name)을 선택하고 Internal을 선택한 뒤 생성해준다.

생성한 Gitlab 프로젝트에 파일을 업로드(push)하기 위해 작업을 진행해준다.
git config로 계정 정보 설정을 해주고 git clone, push을 진행해준다. 이때 password에는 Gitlab 대시보드에 로그인할 때 쓰는 암호가 아닌 위에서 생성한 Token값을 입력해주면 된다.
그리고 로컬에 test.txt를 만들고 gitlab에 Push을 하면 대시보드에서 해당 파일을 확인할 수 있다. 파일까지 들어가서 내가 입력한 내용(gitlab test memo)이 잘 입력되었는지까지 확인을 해보자.

#
mkdir ~/gitlab-test && cd ~/gitlab-test

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

# git 계정 정보 확인 및 global 계정 정보 입력
git config --list
git config --global user.name "<각자 자신의 Gialba 계정>"
git config --global user.email "<각자 자신의 Gialba 계정의 이메일>"
git config --global user.name "myname"
git config --global user.email "mymail@mail.net"

# git clone
git clone https://gitlab.$MyDomain/<각자 자신의 Gitlab 계정>/test-stg.git
git clone https://gitlab.$MyDomain/myname/test-stg.git
Cloning into 'test-stg'...
Username for 'https://gitlab.bs-yang.com': bsyang
Password for 'https://bsyang@gitlab.bs-yang.com': (토근입력)
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.

# 이동
ls -al test-stg && cd test-stg && pwd
total 8
drwxr-xr-x 3 root root   35 Jun  7 13:20 .
drwxr-xr-x 3 root root   22 Jun  7 13:18 ..
drwxr-xr-x 8 root root  163 Jun  7 13:20 .git
-rw-r--r-- 1 root root 6207 Jun  7 13:20 README.md
/root/gitlab-test/test-stg

# 파일 생성 및 깃 업로드(push) : 웹에서 확인
echo "gitlab test memo" >> test.txt
git add . && git commit -m "initial commit - add test.txt"
git push
Username for 'https://gitlab.bs-yang.com': bsyang
Password for 'https://bsyang@gitlab.bs-yang.com': (토근입력)
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 2 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 299 bytes | 299.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To https://gitlab.bs-yang.com/bsyang/test-stg.git
   33958db..5075bf6  main -> main

이렇게 간단하게 gitlab 테스트를 진행해봤다. 이어서 ArgoCD을 진행해보도록 한다.

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

Harbor을 통해 컨테이너 이미지 저장소를 구성했고 Gitlab으로 코드 저장소를 구성했다면 이제 ArgoCD을 통해 GitOps 시스템을 구축할 계획이다.
ArgoCD는 Kubernetes 클러스터 내에서 CI/CD (지속적인 통합 및 지속적인 배포)를 위한 도구이다. Argo CD는 GitOps 원칙에 기반을 둔 애플리케이션 전달 및 배포를 자동화하는 데 사용되고 애플리케이션 배포를 Git 리포지토리의 상태와 동기화한다. 따라서 애플리케이션의 배포 상태를 Git 저장소에 정의하고, Git 저장소의 변경 사항에 따라 배포를 업데이트할 수 있있고. 이를 통해 애플리케이션의 인프라 및 설정을 관리하고, 배포 프로세스를 자동화하며, 롤백 및 복구 기능을 제공한다.

ArgoCD을 통해 Application을 배포하는 로직은 아래와 같다.
ArgoCD CLI을 통해 ArgoCD에 명령을 내리면 GitLab에 Push 된 yaml 등을 활용해서 Application을 EKS에 배포한다.

ArgoCD 실습을 진행하기 위해 기존 PKOS 스터디 때는 helm chart로 설치하였는데 이번에는 argocd 측에서 제공하는 yaml을 사용하여 설치하고 Ingress 설정을 진행하였다.
이후 Login PW을 확인하고 웹 대시보드에 접속해서 로그인까지 완료했다.

# 모니터링 [터미널2]
kubectl create ns argocd
watch kubectl get pod,pvc,svc -n argocd

# 설치
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

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

kubectl get crd | grep argoproj

# 서비스 노출을 위해 서비스 타임 NodePort로 변경
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "NodePort"}}'

# Ingress 설정 및 설치
CERT_ARN=`aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text`
echo $CERT_ARN

cat <<EOF > argocd-ingress-set.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd-ingress
  namespace: argocd
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
    alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
    alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
    alb.ingress.kubernetes.io/backend-protocol: HTTPS
    alb.ingress.kubernetes.io/healthcheck-path: /login
spec:
  rules:
    - host: argocd.$MyDomain
      http:
        paths:
          - pathType: Prefix
            path: /
            backend:
              service:
                name: argocd-server
                port:
                  number: 443
EOF
kubectl apply -f argocd-ingress-set.yaml -n argocd

# ingress 설정 확인
kubectl get pod,pvc,svc,deploy,sts,ingress -n argocd

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

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

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

# 최신버전 설치
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

# 버전 확인
argocd version --short
argocd: v2.7.4+a33baa3
FATA[0000] Argo CD server address unspecified

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

# argocd 서버 로그인
argocd login argocd.$MyDomain --username admin --password $ARGOPW
'admin:login' logged in successfully
Context 'argocd.bs-yang.com' updated

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

# gitlab 등록 확인
argocd repo list
TYPE  NAME  REPO                                            INSECURE  OCI    LFS    CREDS  STATUS      MESSAGE  PROJECT
git         https://gitlab.bs-yang.com/myname/test-stg.git  false     false  false  true   Successful

Git repo가 등록됐으니 해당 repo을 사용해서 RabbitMQ Application을 배포해본다.
yaml 배포를 입력하고 argocd UI을 확인하면 바로 새로운 RabbitMQ Application이 뜨는 것을 확인할 수 있다.
배포되면 처음엔 OutOfSync/Missing 상태인데 이때 Sync을 진행해주면 상태가 변경 된다.

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

# 깃 원격 오리진 주소 확인
git config -l | grep remote.origin.url
remote.origin.url=https://gitlab.bs-yang.com/myname/test-stg.git

# RabbitMQ 헬름 차트 설치
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

# 헬름 차트를 깃랩 저장소에 업로드
git add . && git commit -m "add rabbitmq helm"
git push
Username for 'https://gitlab.bs-yang.com': myname
Password for 'https://bsyang@gitlab.bs-yang.com':
Enumerating objects: 57, done.
Counting objects: 100% (57/57), done.
Delta compression using up to 16 threads
Compressing objects: 100% (54/54), done.
Writing objects: 100% (56/56), 65.14 KiB | 6.51 MiB/s, done.
Total 56 (delta 13), reused 0 (delta 0), pack-reused 0
To https://gitlab.bs-yang.com/bsyang/test-stg.git
   856ff34..c96d6cb  main -> main

# 수정
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
--------------------------------------

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

# 배포
kubectl apply -f rabbitmq-helm-argo-application.yml

# yaml 파일 배포 후 상태 확인 (OutOfSync 상태)
kubectl get applications.argoproj.io -n argocd
NAME            SYNC STATUS   HEALTH STATUS
rabbitmq-helm   OutOfSync     Missing

# sync 후 상태 확인
NAME            SYNC STATUS   HEALTH STATUS
rabbitmq-helm   Synced        Healthy

해당 Application을 클릭해서 들어가면 추가로 svc, ep 등이 확장되었음도 확인할 수 있다.

위 화면의 pod는 현재 1개인데 이를 2개로 확장하는 명령어를 입력해본 뒤 화면이 어떻게 변화하는지 확인해봤다.
Replicas를 2개로 변경하는 명령어를 내리면 Pod가 하나 더 생기면서 sts와 Application의 상태가 OutOfSync 상태로 변경되는 것을 알 수 있다.
이는 Pod가 들어있는 sts와 상위의 Application의 정보가 변경되었음을 의미한다.

# sts 파드 1개에서 2개로 증가 설정 후 argocd 웹 화면 모니터링
kubectl scale statefulset -n rabbitmq rabbitmq-helm --replicas 2

OutOfSync 문제를 해결하기 위해 다시 Sync을 진행해준다.
모두 Synced 상태로 변경되는 것을 확인할 수 있다.

ArgoCD을 활용해서 Gitlab에 있는 yaml 파일을 통해 Application을 배포해보았다. 공동작업을 할 경우 yaml을 gitlab에 올려놓고 작업을 하게되는데 그럴 때 저장소를 통해 배포를 진행할 수 있어서 좋은 방안이라고 생각한다.

4. Crossplane

Crossplane은 k8s Native Infrastructure Cross Cloud Control System이다. 즉, Crossplane을 사용하여 k8s 클러스터에서 멀티 클라우드 및 온프레미스 환경에서 인프라 소스를 프로비저닝하고 관리할 수 있게 된다.
k8s CRD을 사용하여 인프라 리소스를 정의하고 제어할 수 있는데 앞서 다룬 ACK와 유사하다고 볼 수 있다. 오히려 다양한 Provider을 제공하기 때문에 ACK보다 조금 더 확장 된 서비스라고 할 수 있다.

https://docs.crossplane.io/v1.12/getting-started/introduction/

Crossplane을 테스트하기 위해 설치를 우선 진행한다.

# helm chart 통한 설치
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
kubectl create namespace  crossplane-system
helm install crossplane --namespace crossplane-system crossplane-stable/crossplane

# 설치 확인
kubectl get all -n crossplane-system
NAME                                           READY   STATUS    RESTARTS   AGE
pod/crossplane-9f6d5cd7b-5x9np                 1/1     Running   0          64s
pod/crossplane-rbac-manager-699dc89cf4-vck8n   1/1     Running   0          64s

NAME                          TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/crossplane-webhooks   ClusterIP   10.100.175.230   <none>        9443/TCP   64s

NAME                                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/crossplane                1/1     1            1           64s
deployment.apps/crossplane-rbac-manager   1/1     1            1           64s

NAME                                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/crossplane-9f6d5cd7b                 1         1         1       64s
replicaset.apps/crossplane-rbac-manager-699dc89cf4   1         1         1       64s

# crossplane CLI 설치
curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh
sudo mv kubectl-crossplane /usr/local/bin
kubectl crossplane --help

# AWS Provider 설치
cat <<EOF | kubectl apply -f -
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: upbound-provider-aws
spec:
  package: xpkg.upbound.io/upbound/provider-aws:v0.27.0
EOF

# provider 확인. HEALTHY가 true가 되는데까지 최대 약 5분 정도 소요
kubectl get providers
NAME                   INSTALLED   HEALTHY   PACKAGE                                        AGE
upbound-provider-aws   True        True      xpkg.upbound.io/upbound/provider-aws:v0.27.0   98s

AWS Provider에서 특정 사용자 권한을 가져다가 사용하기 위해 Provider 구성을 진행한다.
기존에 생성했던 administratoraccess 권한이 있는 사용자의 Access Profile를 별도의 txt 파일에 저장하고 해당 Profile을 기반으로 Secret을 만들고 Secret을 갖고 Provider에 구성을 진행한다.

# AWS Configure 정보를 토대로 aws-credentials.txt 파일 생성
[default]
aws_access_key_id = AKIAYRMZ...
aws_secret_access_key = DcWBPj3t...

# secret 만들기
kubectl create secret generic aws-secret -n crossplane-system --from-file=creds=./aws-credentials.txt
secret/aws-secret created

# Secret 정보 확인
kubectl describe secret -n crossplane-system
Name:         aws-secret
Namespace:    crossplane-system
...
Data
====
creds:  116 bytes
...

# Provider Config 파일 생성 수 업데이트 진행
cat <<EOF | kubectl apply -f -
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: Secret
    secretRef:
      namespace: crossplane-system
      name: aws-secret
      key: creds
EOF
providerconfig.aws.upbound.io/default created

# Provider Config 확인
kubectl describe providerconfig.aws.upbound.io/default -n crossplane-system
Name:         default
...
Spec:
  Credentials:
    Secret Ref:
      Key:        creds
      Name:       aws-secret
      Namespace:  crossplane-system
    Source:       Secret
...

Provider 구성까지 끝났으니 간단한 테스트로 S3을 배포해보도록 한다.
bucket 이름을 랜덤으로 생성한 후 crossplane API을 사용해서 S3 Bucket을 만드는 과정이다.
Bucket을 배포하기 전에 get buckets 명령어를 입력하면 리소스가 없다고 나온다. aws cli을 통해 확인하면 3개의 S3 Bucket이 나오는 것과는 대조적이다. 이는 k8s에서 동기화되지 않았기 때문이다. ACK에서도 동일하게 해당 내용처럼 진행되는 것을 알 수 있다.
배포 후 READY/SYNCED,가 모두 True가 될 때까지 기다린다.

# S3 목록 확인
aws s3 ls
2023-06-07 15:26:53 cf-templates-2awcm82lq9tn-ap-northeast-2
2023-04-27 13:20:41 cloudtrail-awslogs-....-not-delete
2023-04-29 14:28:11 do-not-delete-....

kubectl get buckets
No resources found

# S3 Bucket 생성
bucket=$(echo "crossplane-bucket-"$(head -n 4096 /dev/urandom | openssl sha1 | tail -c 10))
cat <<EOF | kubectl apply -f -
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
  name: $bucket
spec:
  forProvider:
    region: $AWS_REGION
  providerConfigRef:
    name: default
EOF
bucket.s3.aws.upbound.io/crossplane-bucket-9582936ef created

# S3 Bucket 생성 확인
kubectl get buckets
NAME                          READY   SYNCED   EXTERNAL-NAME                 AGE
crossplane-bucket-9582936ef   True    True     crossplane-bucket-9582936ef   14s

aws s3 ls
2023-06-07 15:26:53 cf-templates-2awc...
2023-04-27 13:20:41 cloudtrail-awslogs-...
2023-06-08 22:39:56 crossplane-bucket-9582936ef
2023-04-29 14:28:11 do-not-delete-...

S3 Bucket을 배포해봤으니 기존 AWS에 배포한 S3 Bucket을 crossplane에 Import 하는 내용을 진행해본다.
AWS Console에서 S3 Bucket을 먼저 생성하고 해당 Bucket을 Crossplane을 통해 Import하는 과정으로 진행한다.
bucket import 후 확인하니 문제 없이 S3 Bucket이 Import 된 것을 볼 수 있었다.

# 앞서 crossplane에서 만든 bucket과 AWS Console에서 만든 Bucket(test 붙은 bucket) 목록 확인
aws s3 ls
...
2023-06-08 22:48:45 crossplane-bucket-64d76c5fa-test
2023-06-08 22:39:56 crossplane-bucket-9582936ef

# crossplane bucket 목록 확인. 앞서 Crossplane 통해 만든 Bucket만 보임
kubectl get bucket
NAME                          READY   SYNCED   EXTERNAL-NAME                 AGE
crossplane-bucket-9582936ef   True    True     crossplane-bucket-9582936ef   10m

# bucket import 진행
cat <<EOF | kubectl apply -f -
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
  name: bucket-import
  annotations:
    crossplane.io/external-name: crossplane-bucket-64d76c5fa-test
spec:
  forProvider:
    region: $AWS_REGION
  providerConfigRef:
    name: default
EOF
bucket.s3.aws.upbound.io/bucket-import created

# bucket import 확인
kubectl get bucket
NAME                          READY   SYNCED   EXTERNAL-NAME                      AGE
bucket-import                 True    True     crossplane-bucket-64d76c5fa-test   76s
crossplane-bucket-9582936ef   True    True     crossplane-bucket-9582936ef        18m

S3 Bucket Create&Import을 했으니 이제 Delete을 해보도록 한다.
AWS Console에서 만들고 Import한 Bucket과 Crossplane에서 생성한 Bucket 모두 각각 삭제를 진행해보았다.
삭제는 문제없이 진행되었고 AWS CLI와 Crossplane CLI에서 모두 Bucket이 지워진 것으로 보인다. 물론 AWS Console에서도 동일하게 삭제 된 것으로 나타난다.

# 삭제 전 Bucket 목록 확인
aws s3 ls
2023-06-08 22:57:22 crossplane-bucket-64d76c5fa-test
2023-06-08 22:39:56 crossplane-bucket-9582936ef

kubectl get bucket
NAME                          READY   SYNCED   EXTERNAL-NAME                      AGE
bucket-import                 True    True     crossplane-bucket-64d76c5fa-test   5m16s
crossplane-bucket-9582936ef   True    True     crossplane-bucket-9582936ef        22m

# crossplane으로 만든 Bucket 삭제
kubectl delete bucket crossplane-bucket-9582936ef
bucket.s3.aws.upbound.io "crossplane-bucket-9582936ef" deleted

kubectl get bucket
NAME            READY   SYNCED   EXTERNAL-NAME                      AGE
bucket-import   True    True     crossplane-bucket-64d76c5fa-test   6m4s

aws s3 ls
2023-06-08 22:57:22 crossplane-bucket-64d76c5fa-test

# AWS Console에서 만들고 crossplane으로 Import한 Bucket 삭제
kubectl delete bucket bucket-import
bucket.s3.aws.upbound.io "bucket-import" deleted

kubectl get bucket
No resources found

aws s3 ls

Crossplane을 간단하게 사용해봤는데 ACK보다 동기화 속도도 빠르고 더 간편하게 작동하는 것 같다. 추후에 k8s에서 AWS 서비스들을 관리해야 하는 순간이 온다면 나는 ACK보다는 crossplane을 사용할 것 같다.

5. eksdemo

eksdemo는 EKS을 사용해서 k8s 클러스터를 배포하고 관리하는데 도움을 주는 예제 및 데모 Application으로 k8s 기반의 Application 배포 및 관리를 위한 다양한 기능과 리소스를 제공한다. Eksdemo는 다음과 같은 주요 기능과 컴포넌트를 갖고 있다.

  1. 애플리케이션 샘플: Eksdemo는 Kubernetes 클러스터에서 실행되는 예제 애플리케이션을 제공한다. 이 애플리케이션은 다양한 마이크로서비스로 구성되어 있으며, 컨테이너화된 애플리케이션 배포와 관리에 대한 실제 시나리오를 보여준다.
  2. 클러스터 구성: Eksdemo는 EKS 클러스터를 배포하기 위한 구성 파일과 스크립트를 제공한다. 이를 통해 클러스터의 크기, 노드 인스턴스 유형, 스토리지 옵션 등을 구성할 수 있다.
  3. CI/CD 지원: Eksdemo는 CI/CD (Continuous Integration/Continuous Deployment) 워크플로를 구축하기 위한 기능과 도구를 포함한다. 예를 들어, GitHub Actions, AWS CodePipeline 등을 사용하여 애플리케이션 배포를 자동화할 수 있다.
  4. 서비스 디스커버리: Eksdemo는 Kubernetes 내부에서 서비스 디스커버리를 구성하는 방법과 관련된 리소스를 제공한다. 이를 통해 서비스 간의 통신과 로드 밸런싱을 구현할 수 있다.
  5. 로깅 및 모니터링: Eksdemo는 Amazon CloudWatch, Prometheus, Grafana 등과 같은 로깅 및 모니터링 도구를 사용하여 클러스터의 상태와 애플리케이션 성능을 모니터링하는 방법을 안내한다.

eksdemo을 사용해보기 위해 eksdemo을 먼저 설치해봤다.
설치는 github에서 압축파일을 당누로드 받고 압축을 풀어서 /usr/local/bin으로 파일을 이동하면서 간단하게 마무리 됐다.

# 압축파일 다운로드
curl -sSL -o eksdemo_Linux_x86_64.tar.gz https://github.com/awslabs/eksdemo/releases/download/v0.8.0/eksdemo_Linux_x86_64.tar.gz

# 압축해제 및 파일 이동
tar xzvf eksdemo_Linux_x86_64.tar.gz
mv eksdemo /usr/local/bin/

ls /usr/local/bin
argocd  aws  aws_completer  eksctl  eksdemo  helm  kubectl  yh

# 설치 확인
eksdemo version
eksdemo version info: cmd.Version{Version:"0.8.0", Date:"2023-06-03T17:45:05Z", Commit:"bac7ddb"}

eksdemo을 설치했으니 간단하게 내 k8s cluster을 잘 불러오는지 조회를 해보았다.
기본적으로 eksctl이 설치되어 있어야 하는 전제조건이 있기 때문에 따로 리전이 다르지 않다면 aws configure에 저장 된 IAM User 정보와 eksctl 정보를 통해 cluster 정보를 받아올 수 있다.

# cluster 정보 조회
eksdemo get cluster
+-------+--------+---------+---------+----------+----------+
|  Age  | Status | Cluster | Version | Platform | Endpoint |
+-------+--------+---------+---------+----------+----------+
| 1 day | ACTIVE | *myeks  |    1.24 | eks.7    | Public   |
+-------+--------+---------+---------+----------+----------+
* Indicates current context in local kubeconfig

# node 정보 조회
eksdemo get node -c $CLUSTER_NAME
+-------+--------------------+---------------------+------------+-----------------+-----------+
|  Age  |        Name        |     Instance Id     |    Type    |      Zone       | Nodegroup |
+-------+--------------------+---------------------+------------+-----------------+-----------+
| 1 day | ip-192-168-1-43.*  | i-0d9e7f20ce15ed4e6 | c5.4xlarge | ap-northeast-2a | ng1       |
| 1 day | ip-192-168-2-192.* | i-023ba2a4ccacca991 | c5.4xlarge | ap-northeast-2b | ng1       |
| 1 day | ip-192-168-3-73.*  | i-0a85a3af6d6bf863d | c5.4xlarge | ap-northeast-2c | ng1       |
+-------+--------------------+---------------------+------------+-----------------+-----------+
* Names end with "ap-northeast-2.compute.internal"

간단하게 조회를 했으니 eksdemo을 통해 Application을 배포해보도록 한다.
ACM에 있는 인증서 정보를 가져와서 해당 인증서를 사용하는 TLS 연결하는 Game-2048 Application을 배포할 계획이다.

클러스터 이름과 Ingress 정보를 입력하고 dry-run을 진행하면 아주 빠른 속도로 manifest 파일을 생성해준다.
파일을 잘 확인해본다. 직접 yaml을 작성하는 것과 유사하게 깔끔하게 만들어주는 것을 확인할 수 있다.

# dry run 통해 manifest 정보 확인
eksdemo install example-game-2048 -c $CLUSTER_NAME -I game2048.$MyDomain --dry-run
Manifest Installer Dry Run:
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: game-2048
  name: deployment-2048
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: app-2048
  replicas: 1
  template:
    metadata:
      labels:
        app.kubernetes.io/name: app-2048
    spec:
      containers:
      - image: public.ecr.aws/l6m2t8p7/docker-2048:latest
        imagePullPolicy: Always
        name: app-2048
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  namespace: game-2048
  name: service-2048
  annotations:
    {}
spec:
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  type: ClusterIP
  selector:
    app.kubernetes.io/name: app-2048
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: game-2048
  name: ingress-2048
  annotations:
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/ssl-redirect: '443'
    alb.ingress.kubernetes.io/target-type: ip
spec:
  ingressClassName: alb
  rules:
    - host: game2048.bs-yang.com
      http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: service-2048
              port:
                number: 80
  tls:
  - hosts:
    - game2048.bs-yang.com

manifest을 확인했다면 설치를 진행하고 설치되면서 생성되는 Application, ALB 정보를 확인해보았다.
eksdemo에서 뿐만 아니라 kubectl 에서 조회할 때도 제대로 나오는 것을 확인할 수 있었다.

# game2048 설치
eksdemo install example-game-2048 -c $CLUSTER_NAME -I game2048.$MyDomain
Helm installing...
2023/06/08 16:31:40 creating 1 resource(s)
2023/06/08 16:31:40 creating 3 resource(s)
Using chart version "n/a", installed "example-game-2048" version "latest" in namespace "game-2048"

# Application 확인
eksdemo get application -c $CLUSTER_NAME
+------------------------------+-------------+---------+----------+--------+
|             Name             |  Namespace  | Version |  Status  | Chart  |
+------------------------------+-------------+---------+----------+--------+
| aws-load-balancer-controller | kube-system | v2.5.2  | deployed | 1.5.3  |
| example-game-2048            | game-2048   | latest  | deployed | n/a    |
| gitlab                       | gitlab      | 15.8.4  | deployed | 6.8.4  |
| harbor                       | harbor      | 2.7.0   | deployed | 1.11.0 |
+------------------------------+-------------+---------+----------+--------+

# ALB 확인 Provisioning -> active가 될 때까지 대기
eksdemo get load-balancer -c $CLUSTER_NAME
+------------+--------------+----------------------------------+------+-------+-----+-----+
|    Age     |    State     |               Name               | Type | Stack | AZs | SGs |
+------------+--------------+----------------------------------+------+-------+-----+-----+
| 43 seconds | provisioning | k8s-game2048-ingress2-70d50ce3fd | ALB  | ipv4  |   3 |   2 |
| 23 hours   | active       | k8s-harbor-harborin-2352dee8a2   | ALB  | ipv4  |   3 |   2 |
| 23 hours   | active       | k8s-gitlab-536957cc0a            | ALB  | ipv4  |   3 |   2 |
| 7 hours    | active       | k8s-argocd-argocdin-cc87c24740   | ALB  | ipv4  |   3 |   2 |
| 23 hours   | active       | k8s-harbor-harborin-b768b16202   | ALB  | ipv4  |   3 |   2 |
+------------+--------------+----------------------------------+------+-------+-----+-----+
* Indicates internal load balancer

# kubectl 통해 확인
kubectl get pod,pvc,svc,deploy,sts,ingress -n game-2048
NAME                                   READY   STATUS    RESTARTS   AGE
pod/deployment-2048-6bc9fd6bf5-nqg7k   1/1     Running   0          11m

NAME                   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/service-2048   ClusterIP   10.100.55.172   <none>        80/TCP    11m

NAME                              READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/deployment-2048   1/1     1            1           11m

NAME                                     CLASS   HOSTS                  ADDRESS                                                                       PORTS     AGE
ingress.networking.k8s.io/ingress-2048   alb     game2048.bs-yang.com   k8s-game2048-ingress2-70d50ce3fd-740064416.ap-northeast-2.elb.amazonaws.com   80, 443   11m


# 사이트 주소 출력
echo "Game2048 URL : https://game2048.$MyDomain"
Game2048 URL : https://game2048.bs-yang.com

출력 된 URL로 접속하면 HTTPS 통한 game2048에 접속이 된 것을 확인할 수 있다.

eksdemo로 AWS 리소스 정보를 확인하고 간단하게 application 배포도 할 수 있는 것을 확인하였다.

6. 정리

이번 실습은 PKOS 스터디의 gitops 때처럼 설치해야 하는 서비스들이 많아서 애를 먹었다. 설치가 잘 안 되거나 기존에 CLB로 설치했던 부분이 있어 그걸 ALB로 변경하려던 과정에서 막히는 부분들이 있어 시간을 꽤 잡아먹은 것 같다.
harbor, gitlab, ArgoCD와 Flux 등을 테스트해보면서 간단하게 GitOps 환경을 구성할 수 있다는 것을 알게 됐다. 물론 Advanced 하게 사용하는 것은 아직 어렵겠지만 간단한 환경 구성은 할 수 있게 된 것 같다.

마지막 스터디 시간이었는데 여러가지를 해볼 수 있어서 좋았고 스터디에서 제공 된 내용 뿐 아니라 내가 직접 찾아서 해보는 내용들을 통해 EKS에 대해서 조금은 더 익숙해진 것 같다.
7주라는 시간 동안 스터디 준비와 진행에 힘써주신 가시다님께 감사 인사를 드리며 AWS EKS 스터디 내용 정리를 마친다.

AEWS Study #6 – EKS Security

이번엔 k8s와 EKS를 떠나서 인프라를 설계하고 구축하는데 있어 가장 중요하다고 볼 수 있는 보안에 대해 학습할 예정이다.
다른 사람들한테 설명할 때도 아리까리하지만 내가 이해하기에도 아리까리한 인증/인가에 대한 내용부터 k8s와 EKS에서 인증/인가를 어떻게 작동시키는지에 대해서 다룰 예정이다.

0. 환경 구성

이번 환경 구성은 지난 번과 마찬가지로 특별하게 변경하는 내용은 없다.
제공되는 yaml 파일로 배포를 진행하며 External DNS만 Cross-Account External DNS 환경으로 별도 수정 구성하였다.
이후 프로메테우스, 그라파나와 metric-server 설치를 진행하였다. 그라파나 대쉬보드는 15757, 17900, 15172 3개를 사용하였다.

# 사용 리전의 인증서 ARN 확인
CERT_ARN=`aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text`
echo $CERT_ARN

# repo 추가
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts

# 파라미터 파일 생성
cat <<EOT > monitor-values.yaml
prometheus:
  prometheusSpec:
    podMonitorSelectorNilUsesHelmValues: false
    serviceMonitorSelectorNilUsesHelmValues: false
    retention: 5d
    retentionSize: "10GiB"

  ingress:
    enabled: true
    ingressClassName: alb
    hosts: 
      - prometheus.$MyDomain
    paths: 
      - /*
    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/load-balancer-name: myeks-ingress-alb
      alb.ingress.kubernetes.io/group.name: study
      alb.ingress.kubernetes.io/ssl-redirect: '443'

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

  ingress:
    enabled: true
    ingressClassName: alb
    hosts: 
      - grafana.$MyDomain
    paths: 
      - /*
    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/load-balancer-name: myeks-ingress-alb
      alb.ingress.kubernetes.io/group.name: study
      alb.ingress.kubernetes.io/ssl-redirect: '443'

defaultRules:
  create: false
kubeControllerManager:
  enabled: false
kubeEtcd:
  enabled: false
kubeScheduler:
  enabled: false
alertmanager:
  enabled: false
EOT

# 배포
kubectl create ns monitoring
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 45.27.2 \
--set prometheus.prometheusSpec.scrapeInterval='15s' --set prometheus.prometheusSpec.evaluationInterval='15s' \
-f monitor-values.yaml --namespace monitoring

# Metrics-server 배포
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

1. K8S 인증/인가

k8s에서는 인증/인가를 위해 다양한 메커니즘과 도구를 제공하는데 예를 들어 인증 정보를 저장하고 관리하는 kubeconfig가 있다. kubeconfig는 클러스터와 상호작용하는 사용자나 애플리케이션에게 필요한 인증 정보를 제공한다. 또한, 인가를 보면 k8s는 RBAC을 구성하고 관리하기 위한 자체적인 API을 제공하며, 관리자는 이를 사용하여 인가 규칙을 설정하고 조정할 수 있다.
k8s에서의 API 서버 접근 과정은 인증->인가->Admission Control과 같은 과정으로 이루어지는데 아래 그림을 참고하면 된다.

https://kubetm.github.io/k8s/07-intermediate-basic-resource/authentication/


k8s에서의 인증은 사용자나 애플리케이션이 자신의 신원을 증명하는 과정으로 k8s에서 인증은 클러스터에 접근하려는 개체가 실제로 그들이 주장하는 사용자 또는 시스템이 맞는지 확인하는 프로세스를 의미한다.
인증은 주로 사용자 이름과 비밀번호, 클라이언트 인증서, 토큰 등을 사용하여 이루어진다. k8s는 다양한 인증 메커니즘을 지원하며, 각각의 메커니즘에 따라 다른 인증 프로세스를 수행한다.

https://kubetm.github.io/k8s/07-intermediate-basic-resource/authentication/

인가는 인증된 개체가 특정 작업 또는 자원에 접근할 수 있는지 여부를 결정하는 프로세스이다. 인가는 인증된 개체의 권한과 역할을 기반으로 이루어진다. k8s에서는 인가 규칙을 정의하여 사용자 또는 그룹에 대한 접근 권한을 제어한다. 인가 규칙은 일반적으로 Role-based Control(RBAC)모델을 사용하여 정의된다.
RBAC을 통해 개발아, 운영자 그리고 시스템 관리자 등 다양한 역할을 정의하고 이 역할에 기반하여 특정 작업을 수행할 수 있는 권한을 부여할 수 있습니다.

https://kubetm.github.io/k8s/07-intermediate-basic-resource/authorization/

위의 내용에 대해 실습을 진행해본다. 실습은 Namespace, ServiceAccount을 생성하고 확인하는 내용으로 진행된다.
ServiceAccount는 각기 다른 권한을 갖는데 이는 각기 다른 Namespace에서 동작하는 것으로 표현된다.
Pod을 기동하게 될 경우 Pod에 ServiceAccount가 할당되며 해당 ServiceAccount 기반 인증/인가를 진행하게 된다.
* 과거 1.23 이전 버전의 경우에는 Service Account에 자동 생성 된 Secret에 저장 된 Token으로 k8s API에 대한 인증 정보를 사용할 수 있었다.

# 네임스페이스(Namespace, NS) 생성 및 확인
kubectl create namespace dev-team
kubectl create ns infra-team

# 네임스페이스 확인
kubectl get ns
NAME              STATUS   AGE
default           Active   25h
dev-team          Active   6s
infra-team        Active   4s
kube-node-lease   Active   25h
kube-public       Active   25h
kube-system       Active   25h
monitoring        Active   11h

# 네임스페이스에 각각 서비스 어카운트 생성 : serviceaccounts 약자(=sa)
kubectl create sa dev-k8s -n dev-team
kubectl create sa infra-k8s -n infra-team

# 서비스 어카운트 정보 확인
kubectl get sa -n dev-team
kubectl get sa dev-k8s -n dev-team -o yaml | yh
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: "2023-06-01T13:01:52Z"
  name: dev-k8s
  namespace: dev-team
  resourceVersion: "336481"
  uid: 3004fea5-b

kubectl get sa -n infra-team
kubectl get sa infra-k8s -n infra-team -o yaml | yh
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: "2023-06-01T13:01:52Z"
  name: infra-k8s
  namespace: infra-team
  resourceVersion: "336483"
  uid: 1bb57fc

Pod을 생성할 때 ServiceAccount을 지정하여 생성하고 권한이 제대로 부여됐는지 테스트를 진행해봤다.
dev-team, infra-team Namespace에 각각 dev-k8s, infra-k8s ServiceAccount을 지정한 Pod을 생성하고 권한 테스트를 진행하였는데 권한을 부여하지 않았기 때문에 권한 오류가 발생하였다.

# 각각 네임스피이스에 kubectl 파드 생성 - 컨테이너이미지
# docker run --rm --name kubectl -v /path/to/your/kube/config:/.kube/config bitnami/kubectl:latest
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: dev-kubectl
  namespace: dev-team
spec:
  serviceAccountName: dev-k8s
  containers:
  - name: kubectl-pod
    image: bitnami/kubectl:1.24.10
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: infra-kubectl
  namespace: infra-team
spec:
  serviceAccountName: infra-k8s
  containers:
  - name: kubectl-pod
    image: bitnami/kubectl:1.24.10
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

# 확인
kubectl get pod -A
kubectl get pod -o dev-kubectl -n dev-team -o yaml
 serviceAccount: dev-k8s
 ...
kubectl get pod -o infra-kubectl -n infra-team -o yaml
 serviceAccount: infra-k8s
...

# 파드에 기본 적용되는 서비스 어카운트(토큰) 정보 확인
kubectl exec -it dev-kubectl -n dev-team -- ls /run/secrets/kubernetes.io/serviceaccount
ca.crt	namespace  token
kubectl exec -it dev-kubectl -n dev-team -- cat /run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIsImtpZCI6IjlmNGNmYWVjMTA...
kubectl exec -it dev-kubectl -n dev-team -- cat /run/secrets/kubernetes.io/serviceaccount/namespace
dev-team
kubectl exec -it dev-kubectl -n dev-team -- cat /run/secrets/kubernetes.io/serviceaccount/ca.crt
-----BEGIN CERTIFICATE-----
MIIC/j...

# 각각 파드로 Shell 접속하여 정보 확인 : 단축 명령어(alias) 사용
alias k1='kubectl exec -it dev-kubectl -n dev-team -- kubectl'
alias k2='kubectl exec -it infra-kubectl -n infra-team -- kubectl'

# 권한 테스트
k1 get pods # kubectl exec -it dev-kubectl -n dev-team -- kubectl get pods 와 동일한 실행 명령이다!
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:dev-team:dev-k8s" cannot list resource "pods" in API group "" in the namespace "dev-team"
command terminated with exit code 1

k1 run nginx --image nginx:1.20-alpine
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:dev-team:dev-k8s" cannot create resource "pods" in API group "" in the namespace "dev-team"
command terminated with exit code 1

k1 get pods -n kube-system
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:dev-team:dev-k8s" cannot list resource "pods" in API group "" in the namespace "kube-system"
command terminated with exit code 1

k2 get pods # kubectl exec -it infra-kubectl -n infra-team -- kubectl get pods 와 동일한 실행 명령이다!
k2 run nginx --image nginx:1.20-alpine
k2 get pods -n kube-system

# (옵션) kubectl auth can-i 로 kubectl 실행 사용자가 특정 권한을 가졌는지 확인
k1 auth can-i get pods
no
command terminated with exit code 1

위에서 진행한 내용에서 k1 get pods 등의 명령을 수행하기 위해서 Role Binding을 진행해보려고 한다.
모든 리소스에 대해 모든 권한을 포함한 Role을 생성하고 해당 Role을 ServiceAccount와 연동(Binding)한다.

https://kubetm.github.io/practice/intermediate/object-authorization/
# 각각 네임스페이스내의 모든 권한에 대한 롤 생성
cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: role-dev-team
  namespace: dev-team
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]
EOF

cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: role-infra-team
  namespace: infra-team
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]
EOF

# 롤 확인 
kubectl get roles -n dev-team
NAME            CREATED AT
role-dev-team   2023-06-01T13:38:54Z

kubectl get roles -n infra-team
NAME              CREATED AT
role-infra-team   2023-06-01T13:38:56Z

kubectl get roles -n dev-team -o yaml
  rules:
  - apiGroups:
    - '*'
    resources:
    - '*'
    verbs:
    - '*'

kubectl describe roles role-dev-team -n dev-team
...
PolicyRule:
  Resources  Non-Resource URLs  Resource Names  Verbs
  ---------  -----------------  --------------  -----
  *.*        []                 []              [*]

# 롤바인딩 생성 : '서비스어카운트 <-> 롤' 간 서로 연동
cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: roleB-dev-team
  namespace: dev-team
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: role-dev-team
subjects:
- kind: ServiceAccount
  name: dev-k8s
  namespace: dev-team
EOF

cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: roleB-infra-team
  namespace: infra-team
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: role-infra-team
subjects:
- kind: ServiceAccount
  name: infra-k8s
  namespace: infra-team
EOF

# 롤바인딩 확인
kubectl get rolebindings -n dev-team
NAME             ROLE                 AGE
roleB-dev-team   Role/role-dev-team   9s

kubectl get rolebindings -n infra-team
NAME               ROLE                   AGE
roleB-infra-team   Role/role-infra-team   8s

kubectl get rolebindings -n dev-team -o yaml
  roleRef:
    apiGroup: rbac.authorization.k8s.io
    kind: Role
    name: role-dev-team

kubectl describe rolebindings roleB-dev-team -n dev-team
...
Role:
  Kind:  Role
  Name:  role-dev-team
Subjects:
  Kind            Name     Namespace
  ----            ----     ---------
  ServiceAccount  dev-k8s  dev-team

Role 생성 및 RoleBinding을 마쳤다면 위에서 실패한 권한 테스트를 마저 다시 진행해본다.
해당 Namespace에서 작동하는 get, create, delete 등은 정상적으로 작동했다. 하지만 kube-system에 대한 get 명령어나 get node 등과 같이 Namespace을 벗어난 다른 Namespace 혹은 클러스터 정보 등에 대한 내용은 조회할 수 없었다. 이는 해당 ServiceAccount는 해당 Namespace에 대한 Role만 보유하기 때문이다.

# 각각 파드로 Shell 접속하여 정보 확인 : 단축 명령어(alias) 사용
alias k1='kubectl exec -it dev-kubectl -n dev-team -- kubectl'
alias k2='kubectl exec -it infra-kubectl -n infra-team -- kubectl'

# 권한 테스트
k1 get pods 
NAME          READY   STATUS    RESTARTS   AGE
dev-kubectl   1/1     Running   0          15m

k1 run nginx --image nginx:1.20-alpine
pod/nginx created

k1 get pods
NAME          READY   STATUS    RESTARTS   AGE
dev-kubectl   1/1     Running   0          15m
nginx         1/1     Running   0          12s

k1 delete pods nginx
pod "nginx" deleted

k1 get pods -n kube-system
rror from server (Forbidden): pods is forbidden: User "system:serviceaccount:dev-team:dev-k8s" cannot list resource "pods" in API group "" in the namespace "kube-system"
command terminated with exit code 1

k1 get nodes
Error from server (Forbidden): nodes is forbidden: User "system:serviceaccount:dev-team:dev-k8s" cannot list resource "nodes" in API group "" at the cluster scope
command terminated with exit code 1


k2 get pods 
NAME            READY   STATUS    RESTARTS   AGE
infra-kubectl   1/1     Running   0          16m

k2 run nginx --image nginx:1.20-alpine
pod/nginx created

k2 get pods
NAME            READY   STATUS    RESTARTS   AGE
infra-kubectl   1/1     Running   0          16m
nginx           1/1     Running   0          5s

k2 delete pods nginx
pod "nginx" deleted

k2 get pods -n kube-system
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:infra-team:infra-k8s" cannot list resource "pods" in API group "" in the namespace "kube-system"
command terminated with exit code 1

k2 get nodes
Error from server (Forbidden): nodes is forbidden: User "system:serviceaccount:infra-team:infra-k8s" cannot list resource "nodes" in API group "" at the cluster scope
command terminated with exit code 1

# (옵션) kubectl auth can-i 로 kubectl 실행 사용자가 특정 권한을 가졌는지 확인
k1 auth can-i get pods
yes

2. EKS 인증/인가

앞서 k8s에서의 인증/인가에 다뤘다면 이번에는 EKS에서의 인증/인가를 다룬다. EKS에서의 인증은 AWS IAM을 사용하고 인가는 k8s RBAC을 통해 진행한다.

https://docs.aws.amazon.com/eks/latest/userguide/cluster-auth.html


RBAC 관련 KREW 플러그인을 설치하고 테스트해보았다.
rbac-view을 통해 Role을 확인할 수 있었다.

# 설치
kubectl krew install access-matrix rbac-tool rbac-view rolesum

# Show an RBAC access matrix for server resources
kubectl access-matrix # Review access to cluster-scoped resources
kubectl access-matrix --namespace default # Review access to namespaced resources in 'default'

# RBAC Lookup by subject (user/group/serviceaccount) name
kubectl rbac-tool lookup
kubectl rbac-tool lookup system:masters
  SUBJECT        | SUBJECT TYPE | SCOPE       | NAMESPACE | ROLE
+----------------+--------------+-------------+-----------+---------------+
  system:masters | Group        | ClusterRole |           | cluster-admin

kubectl rbac-tool lookup system:nodes # eks:node-bootstrapper
kubectl rbac-tool lookup system:bootstrappers # eks:node-bootstrapper
kubectl describe ClusterRole eks:node-bootstrapper

# RBAC List Policy Rules For subject (user/group/serviceaccount) name
kubectl rbac-tool policy-rules
kubectl rbac-tool policy-rules -e '^system:.*'

# Generate ClusterRole with all available permissions from the target cluster
kubectl rbac-tool show

# Shows the subject for the current context with which one authenticates with the cluster
kubectl rbac-tool whoami
{Username: "kubernetes-admin",
 UID:      "aws-iam-authenticator:911283.....:AIDA5ILF2FJ......",
 Groups:   ["system:masters",
            "system:authenticated"],
 Extra:    {accessKeyId:  ["AKIA5ILF2FJ....."],
            arn:          ["arn:aws:iam::911283....:user/admin"],
            canonicalArn: ["arn:aws:iam::911283....:user/admin"],
            principalId:  ["AIDA5ILF2FJ....."],
            sessionName:  [""]}}

# Summarize RBAC roles for subjects : ServiceAccount(default), User, Group
kubectl rolesum -h
kubectl rolesum aws-node -n kube-system
kubectl rolesum -k User system:kube-proxy
kubectl rolesum -k Group system:masters

# [터미널1] A tool to visualize your RBAC permissions
kubectl rbac-view
INFO[0000] Getting K8s client
INFO[0000] serving RBAC View and http://localhost:8800

## 이후 해당 작업용PC 공인 IP:8800 웹 접속
echo -e "RBAC View Web http://$(curl -s ipinfo.io/ip):8800"


kubectl 명령 → aws eks get-token → EKS Service endpoint(STS)에 토큰 요청 ⇒ 응답값 디코드 과정은 아래와 같이 실습을 진행할 수 있다.

# sts caller id의 ARN 확인
aws sts get-caller-identity --query Arn
"arn:aws:iam::MyAccount:user/k8sadmin"

# kubeconfig 정보 확인
cat ~/.kube/config | yh
...
- name: k8sadmin@myeks.ap-northeast-2.eksctl.io
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - eks
      - get-token
      - --output
      - json
      - --cluster-name
      - myeks
      - --region
      - ap-northeast-2
      command: aws
      env:
      - name: AWS_STS_REGIONAL_ENDPOINTS
        value: regional
      interactiveMode: IfAvailable
      provideClusterInfo: false

# Get  a token for authentication with an Amazon EKS cluster.
# This can be used as an alternative to the aws-iam-authenticator.
aws eks get-token help

# 임시 보안 자격 증명(토큰)을 요청 : expirationTimestamp 시간경과 시 토큰 재발급됨
aws eks get-token --cluster-name $CLUSTER_NAME | jq
aws eks get-token --cluster-name $CLUSTER_NAME | jq -r '.status.token'
k8s-aws-v1.aHR0cHM6Ly9zdHMuYXAtbm9ydGhl...

위에서 추출한 Token 값을 JWT 사이트에 조회하면 디코드 정보를 확인할 수 있다.

위에서 뽑아낸 PAYLOAD 값을 URL Decode Online에서 Decode로 확인할 수 있다.
아래 데이터는 내 실습 환경의 Token 값에서 추출한 PAYLOAD 값을 Decode 한 내용에 대한 값이다.

https://sts.ap-northeast-2.amazonaws.com/?

Action=GetCallerIdentity&

Version=2011-06-15&

X-Amz-Algorithm=AWS4-HMAC-SHA256&

X-Amz-Credential=AKIAYRM.../20230602/ap-northeast-2/sts/aws4_request&

X-Amz-Date=20230602T055815Z&

X-Amz-Expires=60&

X-Amz-SignedHeaders=host;x-k8s-aws-id&

X-Amz-Signature=3047f3a91d9780659c.....

EKS API는 Token Review을 Webhook Token Authenticator에 요청하고 AWS IAM에 해당 호출 인증 완료 후 User/Role에 대한 ARN을 반환하게 된다. 해당 과정은 아래와 같이 테스트해볼 수 있다.

# tokenreviews api 리소스 확인 
kubectl api-resources | grep authentication
tokenreviews                                   authentication.k8s.io/v1               false        TokenReview# List the fields for supported resources.

# List the fields for supported resources.
kubectl explain tokenreviews
...
DESCRIPTION:
     TokenReview attempts to authenticate a token to a known user. Note:
     TokenReview requests may be cached by the webhook token authenticator
     plugin in the kube-apiserver.

그 다음 단계는 k8s RBAC 인가를 처리하는 단계이다. 해당 IAM User/Role이 확인 되면 k8s aws-auth-configmap에서 mapping 정보를 확인하게 되고 권한 확인 후 k8s 인가 허가가 되면 동작 실행을 하게 된다.

# Webhook api 리소스 확인 
kubectl api-resources | grep Webhook
mutatingwebhookconfigurations                  admissionregistration.k8s.io/v1        false        MutatingWebhookConfiguration
validatingwebhookconfigurations                admissionregistration.k8s.io/v1        false        ValidatingWebhookConfiguration

# validatingwebhookconfigurations 리소스 확인
kubectl get validatingwebhookconfigurations
NAME                                        WEBHOOKS   AGE
aws-load-balancer-webhook                   3          29h
eks-aws-auth-configmap-validation-webhook   1          43h
kube-prometheus-stack-admission             1          29h
vpc-resource-validating-webhook             2          43h

kubectl get validatingwebhookconfigurations eks-aws-auth-configmap-validation-webhook -o yaml | kubectl neat | yh
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: eks-aws-auth-configmap-validation-webhook
...

# aws-auth 컨피그맵 확인
kubectl get cm -n kube-system aws-auth -o yaml | kubectl neat | yh
apiVersion: v1
kind: ConfigMap
metadata: 
  name: aws-auth
  namespace: kube-system
data: 
  mapRoles: |
    - groups:
      - system:bootstrappers
      - system:nodes
      rolearn: arn:aws:iam::MyAccount:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-1US96WGO4SJJE
      username: system:node:{{EC2PrivateDNSName}}

# EKS 설치한 IAM User 정보 >> system:authenticated는 어떤 방식으로 추가가 되었는지 궁금???
kubectl rbac-tool whoami
{Username: "kubernetes-admin",
 UID:      "aws-iam-authenticator:MyAccount:AIDAYR...",
 Groups:   ["system:masters",
            "system:authenticated"],
...

# system:masters , system:authenticated 그룹의 정보 확인
kubectl rbac-tool lookup system:masters
W0602 15:44:41.617127    6662 warnings.go:67] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
  SUBJECT        | SUBJECT TYPE | SCOPE       | NAMESPACE | ROLE
+----------------+--------------+-------------+-----------+---------------+
  system:masters | Group        | ClusterRole |           | cluster-admin

kubectl rbac-tool lookup system:authenticated
W0602 15:44:58.968522    6716 warnings.go:67] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
  SUBJECT              | SUBJECT TYPE | SCOPE       | NAMESPACE | ROLE
+----------------------+--------------+-------------+-----------+----------------------------------+
  system:authenticated | Group        | ClusterRole |           | system:discovery
  system:authenticated | Group        | ClusterRole |           | eks:podsecuritypolicy:privileged
  system:authenticated | Group        | ClusterRole |           | system:basic-user
  system:authenticated | Group        | ClusterRole |           | system:public-info-viewer

kubectl rolesum -k Group system:masters
Group: system:masters
Policies:
• [CRB] */cluster-admin ⟶  [CR] */cluster-admin
  Resource  Name  Exclude  Verbs  G L W C U P D DC
  *.*       [*]     [-]     [-]   ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✔

kubectl rolesum -k Group system:authenticated
W0602 15:45:24.506385    6825 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
Group: system:authenticated
Policies:
• [CRB] */eks:podsecuritypolicy:authenticated ⟶  [CR] */eks:podsecuritypolicy:privileged
  Name            PRIV  RO-RootFS  Volumes  Caps  SELinux   RunAsUser  FSgroup   SUPgroup
  eks.privileged  True    False      [*]    [*]   RunAsAny  RunAsAny   RunAsAny  RunAsAny
• [CRB] */system:basic-user ⟶  [CR] */system:basic-user
  Resource                                       Name  Exclude  Verbs  G L W C U P D DC
  selfsubjectaccessreviews.authorization.k8s.io  [*]     [-]     [-]   ✖ ✖ ✖ ✔ ✖ ✖ ✖ ✖
  selfsubjectrulesreviews.authorization.k8s.io   [*]     [-]     [-]   ✖ ✖ ✖ ✔ ✖ ✖ ✖ ✖
• [CRB] */system:discovery ⟶  [CR] */system:discovery
• [CRB] */system:public-info-viewer ⟶  [CR] */system:public-info-viewer

# system:masters 그룹이 사용 가능한 클러스터 롤 확인 : cluster-admin
kubectl describe clusterrolebindings.rbac.authorization.k8s.io cluster-admin
Name:         cluster-admin
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
Role:
  Kind:  ClusterRole
  Name:  cluster-admin
Subjects:
  Kind   Name            Namespace
  ----   ----            ---------
  Group  system:masters

# cluster-admin 의 PolicyRule 확인 : 모든 리소스  사용 가능!
kubectl describe clusterrole cluster-admin
Name:         cluster-admin
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
  Resources  Non-Resource URLs  Resource Names  Verbs
  ---------  -----------------  --------------  -----
  *.*        []                 []              [*]
             [*]                []              [*]

# system:authenticated 그룹이 사용 가능한 클러스터 롤 확인
kubectl describe ClusterRole system:discovery
Name:         system:discovery
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
...

kubectl describe ClusterRole system:public-info-viewer
Name:         system:public-info-viewer
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
...

kubectl describe ClusterRole system:basic-user
Name:         system:basic-user
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
...

kubectl describe ClusterRole eks:podsecuritypolicy:privileged
Name:         eks:podsecuritypolicy:privileged
Labels:       eks.amazonaws.com/component=pod-security-policy
              kubernetes.io/cluster-service=true
...

위의 내용을 기반으로 하나의 시나리오를 만들어서 테스트를 해보려고 한다. 신입 사원을 위한 myeks-bastion-2 EC2에 설정을 진행해본다.
기존 이용 중인 bastion인 myeks-bastion에서 testuser라는 IAM User을 생성하고 Accesskey을 생성하고 AdminstratorAccess 정책 권한을 부여한다. 그리고 접속을 위해 myeks-bastion-2의 IP을 확인한다.

# testuser 사용자 생성
aws iam create-user --user-name testuser
{
    "User": {
        "Path": "/",
        "UserName": "testuser",
        "UserId": "AIDAY...",
        "Arn": "arn:aws:iam::MyAccount:user/testuser",
        "CreateDate": "2023-06-02T07:36:50+00:00"
    }
}

# 사용자에게 프로그래밍 방식 액세스 권한 부여
aws iam create-access-key --user-name testuser
{
    "AccessKey": {
        "UserName": "testuser",
        "AccessKeyId": "AKIAY...",
        "Status": "Active",
        "SecretAccessKey": "+U5aviQ.....",
        "CreateDate": "2023-06-02T07:37:11+00:00"
    }
}

# testuser 사용자에 정책을 추가
aws iam attach-user-policy --policy-arn arn:aws:iam::aws:policy/AdministratorAccess --user-name testuser

# get-caller-identity 확인
aws sts get-caller-identity --query Arn
"arn:aws:iam::MyAccount:user/k8sadmin"

# EC2 IP 확인 : myeks-bastion-EC2-2 PublicIPAdd 확인
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
-----------------------------------------------------------------------
|                          DescribeInstances                          |
+----------------------+----------------+------------------+----------+
|     InstanceName     | PrivateIPAdd   |   PublicIPAdd    | Status   |
+----------------------+----------------+------------------+----------+
|  myeks-ng1-Node      |  192.168.3.248 |  54.180.xxx.xx   |  running |
|  myeks-ng1-Node      |  192.168.2.161 |  3.38.xxx.xx     |  running |
|  myeks-bastion-EC2-2 |  192.168.1.200 |  54.180.xxx.xxx  |  running |
|  myeks-bastion-EC2   |  192.168.1.100 |  43.201.xxx.xxx  |  running |
|  myeks-ng1-Node      |  192.168.1.41  |  52.78.xxx.xxx   |  running |
+----------------------+----------------+------------------+----------+

myeks-bastion-2에서 testuser의 자격증명 설정하고 확인한다.
testuser로 get node와 ~/.kube에 대한 내용을 조회할 경우 조회가 되지 않는다. AdministratorAccess을 갖고있음에도 불구하고 조회를 할 수 없는데 그 이유는 EKS의 경우 해당 클러스터에 대한 권한을 갖고 있어야 조회가 가능하다.

# testuser 자격증명 설정
aws configure
AWS Access Key ID [None]: AKIAY...
AWS Secret Access Key [None]: +U5aviQ.....
Default region name [None]: ap-northeast-2
Default output format [None]: json

# get-caller-identity 확인
aws sts get-caller-identity --query Arn
"arn:aws:iam::MyAccount:user/testuser"

# kubectl 시도
kubectl get node -v6
I0602 16:43:03.667055    6352 round_trippers.go:553] GET http://localhost:8080/api?timeout=32s  in 1 milliseconds
E0602 16:43:03.667226    6352 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused

ls ~/.kube
ls: cannot access /root/.kube: No such file or directory

testuser에 system:masters 그룹 부여로 EKS 관리자 수준 권한을 부여하기 위해 myeks-bastion에서 작업을 진행한다. eksctl을 사용해서 aws-auth configmap을 작성하는 방식으로 진행한다.

# eksctl 사용 >> iamidentitymapping 실행 시 aws-auth 컨피그맵 작성해줌
# Creates a mapping from IAM role or user to Kubernetes user and groups
eksctl create iamidentitymapping --cluster $CLUSTER_NAME --username testuser --group system:masters --arn arn:aws:iam::$ACCOUNT_ID:user/testuser
2023-06-02 16:48:02 [ℹ]  checking arn arn:aws:iam::MyAccount:user/testuser against entries in the auth ConfigMap
2023-06-02 16:48:02 [ℹ]  adding identity "arn:aws:iam::MyAccount:user/testuser" to auth ConfigMap

# 확인
kubectl get cm -n kube-system aws-auth -o yaml | kubectl neat | yh
...
data:
  mapRoles: |
    - groups:
      - system:bootstrappers
      - system:nodes
      rolearn: arn:aws:iam::MyAccount:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-1US96WGO4SJJE
      username: system:node:{{EC2PrivateDNSName}}
  mapUsers: |
    - groups:
      - system:masters
      userarn: arn:aws:iam::MyAccount:user/testuser
      username: testuser
...

# 확인 : 기존에 있는 role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-YYYYY 는 어떤 역할/동작을 하는 걸까요?
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
ARN												USERNAME				GROUPS			ACCOUNT
arn:aws:iam::MyAccount:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-1US96WGO4SJJE	system:node:{{EC2PrivateDNSName}}	system:bootstrappers,system:nodes
arn:aws:iam::MyAccount:user/testuser								testuser				system:masters

testuser에서 kubeconfig 생성 및 kubectl 사용을 myeks-bastion-2에서 확인 진행해봤다.

# testuser kubeconfig 생성
aws eks update-kubeconfig --name $CLUSTER_NAME --user-alias testuser
Added new context testuser to /root/.kube/config

# kubectl 사용 확인
kubectl ns default
Context "testuser" modified.
Active namespace is "default".

kubectl get node -v6
I0602 17:28:46.036886    6734 loader.go:373] Config loaded from file:  /root/.kube/config
I0602 17:28:46.867677    6734 round_trippers.go:553] GET https://D5534....yl4.ap-northeast-2.eks.amazonaws.com/api/v1/nodes?limit=500 200 OK in 819 milliseconds
NAME                                               STATUS   ROLES    AGE   VERSION
ip-192-168-1-41.ap-northeast-2.compute.internal    Ready    <none>   44h   v1.24.13-eks-0a21954
ip-192-168-2-161.ap-northeast-2.compute.internal   Ready    <none>   44h   v1.24.13-eks-0a21954
ip-192-168-3-248.ap-northeast-2.compute.internal   Ready    <none>   44h   v1.24.13-eks-0a21954

# rbac-tool 후 확인
kubectl krew install rbac-tool && kubectl rbac-tool whoami
{Username: "testuser",
 UID:      "aws-iam-authenticator:MyAccount:AIDAYR...",
 Groups:   ["system:masters",
            "system:authenticated"],
 Extra:    {accessKeyId:  ["AKIAYRMZ..."],
            arn:          ["arn:aws:iam::MyAccount:user/testuser"],
            canonicalArn: ["arn:aws:iam::MyAccount:user/testuser"],
            principalId:  ["AIDAYRM..."],
            sessionName:  [""]}}
...

testuser의 Group을 system:masters에서 system:authenticated로 RBAC 동작 확인을 진행해본다. 제대로 변경 된 것을 확인할 수 있다.

# 아래 edit로 mapUsers 내용 직접 수정 system:authenticated
kubectl edit cm -n kube-system aws-auth
...

# 확인
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
ARN												USERNAME				GROUPS			ACCOUNT
arn:aws:iam::MyAccount:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-1US96WGO4SJJE	system:node:{{EC2PrivateDNSName}}	system:bootstrappers,system:nodes
arn:aws:iam::MyAccount:user/testuser								testuser				system:authenticated

testuser의 kubectl 사용을 확인해보았다. masters에서 authenticated로 변경되어서 get node -v6 명령이 Forbidden 처리되는 것을 확인할 수 있다.

# 시도
kubectl get node -v6
I0602 17:41:46.148222    7108 loader.go:373] Config loaded from file:  /root/.kube/config
I0602 17:41:47.185378    7108 round_trippers.go:553] GET https://D55341D506A04AA2DE918CAD37BF2459.yl4.ap-northeast-2.eks.amazonaws.com/api/v1/nodes?limit=500 403 Forbidden in 1014 milliseconds
...
  "message": "nodes is forbidden: User \"testuser\" cannot list resource \"nodes\" in API group \"\" at the cluster scope",
  "reason": "Forbidden",
...

kubectl api-resources -v5

testuser의 IAM Mapping 삭제를 첫번째 bastion EC2에서 k8sadmin user로 진행한다.
IAM Mapping 삭제 후 확인하면 k8sadmin에 대한 IAM Mapping만 확인할 수 있다.

# testuser IAM 맵핑 삭제
eksctl delete iamidentitymapping --cluster $CLUSTER_NAME --arn  arn:aws:iam::$ACCOUNT_ID:user/testuser
2023-06-02 17:47:39 [ℹ]  removing identity "arn:aws:iam::MyAccount:user/testuser" from auth ConfigMap (username = "testuser", groups = ["system:authenticated"])

# Get IAM identity mapping(s)
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
ARN												USERNAME				GROUPS			ACCOUNT
arn:aws:iam::MyAccount:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-1US96WGO4SJJE	system:node:{{EC2PrivateDNSName}}	system:bootstrappers,system:nodes

kubectl get cm -n kube-system aws-auth -o yaml | yh

myeks-bastion-2 EC2에서 testuser 권한으로 get node을 실행하면 서버에 접속하지 못하는 것을 확인할 수 있다. 이는 authenticated 그룹에 할당된 것과는 다르게 아예 IAM Mapping이 되지 않았기 때문에 서버에 접속하지 못하는 상황이라고 볼 수 있다.

# 시도
kubectl get node -v6
error: You must be logged in to the server (the server has asked for the client to provide credentials)

kubectl api-resources -v5
error: You must be logged in to the server (the server has asked for the client to provide credentials)

이렇게 간단한 테스트를 진행해보았다. IAM User에게 기존 생성 된 group Mapping을 해주면 언제든 권한을 사용할 수 있음을 확인하였다.

3. IRSA

EKS을 사용 중에 EC2에 Instance Profile을 사용하여 권한을 부여 받을 경우 사용하기에는 편리하지만 보안에는 취약해지게 된다. 그렇기 때문에 IRSA 사용을 권장한다.

IRSA(IAM Role for Service Accounts)는 AWS EKS에서 제공하는 기능으로, 쿠버네티스의 서비스 계정에 AWS IAM 역할을 할당하여 AWS 리소스에 대한 액세스 권한을 제어하는 메커니즘이다.
일반적으로, 쿠버네티스 클러스터에서 AWS 리소스에 액세스하기 위해 AWS API를 호출하는 애플리케이션은 IAM 역할을 사용하여 권한을 부여받는다. 그러나 이러한 방식은 모든 애플리케이션에 대해 개별적으로 IAM 역할을 생성하고 관리해야 한다는 번거로움이 있기 때문에 IRSA를 사용하여 쿠버네티스의 서비스 계정에 IAM 역할을 연결하여 AWS API 액세스를 간편하게 제어한다.

IRSA의 동작 방식은 k8s파드 → AWS 서비스 사용 시 ⇒ AWS STS/IAM ↔ IAM OIDC Identity Provider(EKS IdP) 인증/인가로 이루어진다.

https://awskoreamarketingasset.s3.amazonaws.com/2022 Summit/pdf/T10S1_EKS 환경을 더 효율적으로 더 안전하게.pdf

IRSA에 대한 내용을 실습할 겸 도전과제인 awscli pod에서 IRSA을 사용해서 AWS 서비스의 내용들을 조회해보는 테스트를 진행해볼 예정이다.
과정은 SA 생성 및 IRSA 설정과 Pod 배포 후 테스트로 이어진다.
IRSA에 사용하는 Policy는 AWS에서 ManagedPolicy로 제공하는 ReadOnlyAccess을 사용하였다. 테스트 결과 내용을 조회하는데는 문제없었으나 그 이외의 명령은 권한 오류가 발생하는 것으로 정상적으로 IRSA 테스트가 완료되었음을 알 수 있다.

# service account 생성
kubectl create serviceaccount awscli-sa --namespace default
serviceaccount/awscli-sa created

# IRSA 설정
eksctl create iamserviceaccount --cluster $CLUSTER_NAME --namespace default --name awscli-sa --attach-policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess --approve --override-existing-serviceaccounts
2023-06-03 09:46:51 [ℹ]  1 existing iamserviceaccount(s) (kube-system/aws-load-balancer-controller) will be excluded
2023-06-03 09:46:51 [ℹ]  1 iamserviceaccount (default/awscli-sa) was included (based on the include/exclude rules)
2023-06-03 09:46:51 [!]  metadata of serviceaccounts that exist in Kubernetes will be updated, as --override-existing-serviceaccounts was set
2023-06-03 09:46:51 [ℹ]  1 task: {
    2 sequential sub-tasks: {
        create IAM role for serviceaccount "default/awscli-sa",
        create serviceaccount "default/awscli-sa",
    } }2023-06-03 09:46:51 [ℹ]  building iamserviceaccount stack "eksctl-myeks-addon-iamserviceaccount-default-awscli-sa"
2023-06-03 09:46:51 [ℹ]  deploying stack "eksctl-myeks-addon-iamserviceaccount-default-awscli-sa"
2023-06-03 09:46:51 [ℹ]  waiting for CloudFormation stack "eksctl-myeks-addon-iamserviceaccount-default-awscli-sa"
2023-06-03 09:47:21 [ℹ]  waiting for CloudFormation stack "eksctl-myeks-addon-iamserviceaccount-default-awscli-sa"
2023-06-03 09:48:04 [ℹ]  waiting for CloudFormation stack "eksctl-myeks-addon-iamserviceaccount-default-awscli-sa"
2023-06-03 09:48:04 [ℹ]  serviceaccount "default/awscli-sa" already exists
2023-06-03 09:48:04 [ℹ]  updated serviceaccount "default/awscli-sa"

# pod 배포
## aws-cli-pod.yaml 작성
apiVersion: v1
kind: Pod
metadata:
  name: awscli-pod
spec:
  containers:
    - name: awscli-container
      image: amazon/aws-cli
      command: ["sleep", "infinity"]
  serviceAccountName: awscli-sa

## yaml 사용 pod 배포
kubectl create -f aws-cli-pod.yaml
pod/awscli-pod created

## 배포 확인
kubectl get pod
NAME         READY   STATUS    RESTARTS   AGE
awscli-pod   1/1     Running   0          22s

# aws cli pod 접속 후 테스트 진행
kubectl exec -it awscli-pod -- sh

aws s3 ls
2023-04-27 04:20:41 cloudtrail-awslogs-MyAccount-vlhszz1b-do-not-delete
2023-04-29 05:28:11 do-not-delete-gatedgarden-audit-MyAccount

aws ec2 describe-instances --query "Reservations[].Instances[].Tags[?Key=='Name'].Value" --output text
myeks-ng1-Node
myeks-ng1-Node
test
myeks-bastion-EC2-2
myeks-bastion-EC2
myeks-ng1-Node

# ec2 stop 명령 -> 실행되지 않음
aws ec2 stop-instances --instance-id i-08d649dc....
An error occurred (UnauthorizedOperation) when calling the StopInstances operation: You are not authorized to perform this operation. Encoded authorization failure message: 0wcTjz3wqszNCw...

5. 정리

인증/인가는 특히 다른 파트들에 비해 조금 어려웠다.
내용 자체가 이해가 가지 않는 건 아니었으나 이를 실습에 적용하는 과정에서 이 부분이 지금 내가 이해한 부분대로 작동하는 게 맞는지에 대해 계속 고민하게 만든 것 같다.
IAM User에게 Readonly 권한만 주고 테스트를 해보려고 했으나 계속 작동하지 않아 이 부분은 차근차근 다시 도전해볼 계획이다.

AEWS Study #5 – EKS Autoscaling

그동안은 EKS 실습을 하면서 pod의 replica 수량을 수동으로 설정하고 경우에 따라 늘리거나 줄이거나 했었다.
그 부분을 EC2 기반 서비스를 운영할 때와 마찬가지로 Autoscaling을 할 수 있는 방법에 대해 학습하고 실습을 진행할 예정이다.

0. 환경 구성

환경 구성은 이번엔 특별하게 추가 진행한 부분은 없고 가시다님이 제공해주신 스크립트를 통해 환경 구성을 진행했다.
yaml 배포를 진행하고 프로메테우스&그라파나 설치를 진행했다.

# 사용 리전의 인증서 ARN 확인
CERT_ARN=`aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text`
echo $CERT_ARN

# repo 추가
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts

# 파라미터 파일 생성
cat <<EOT > monitor-values.yaml
prometheus:
  prometheusSpec:
    podMonitorSelectorNilUsesHelmValues: false
    serviceMonitorSelectorNilUsesHelmValues: false
    retention: 5d
    retentionSize: "10GiB"

  verticalPodAutoscaler:
    enabled: true

  ingress:
    enabled: true
    ingressClassName: alb
    hosts: 
      - prometheus.$MyDomain
    paths: 
      - /*
    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/load-balancer-name: myeks-ingress-alb
      alb.ingress.kubernetes.io/group.name: study
      alb.ingress.kubernetes.io/ssl-redirect: '443'

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

  ingress:
    enabled: true
    ingressClassName: alb
    hosts: 
      - grafana.$MyDomain
    paths: 
      - /*
    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/load-balancer-name: myeks-ingress-alb
      alb.ingress.kubernetes.io/group.name: study
      alb.ingress.kubernetes.io/ssl-redirect: '443'

defaultRules:
  create: false
kubeControllerManager:
  enabled: false
kubeEtcd:
  enabled: false
kubeScheduler:
  enabled: false
alertmanager:
  enabled: false
EOT

# 배포
kubectl create ns monitoring
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 45.27.2 \
--set prometheus.prometheusSpec.scrapeInterval='15s' --set prometheus.prometheusSpec.evaluationInterval='15s' \
-f monitor-values.yaml --namespace monitoring

# Metrics-server 배포
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

EKS Node Viewer도 설치를 진행한다. EKS Node Viewer은 예약된 Pod 리소스 요청과 노드의 할당 가능한 용량을 표시해줍니다. AutoScaling을 진행할 때 도움이 되는 Viewer이니 미리 설치를 진행한다.

# go 설치
yum install -y go

# EKS Node Viewer 설치 : 현재 ec2 spec에서는 설치에 다소 시간이 소요됨 = 2분 이상
go install github.com/awslabs/eks-node-viewer/cmd/eks-node-viewer@latest

# bin 확인 및 사용 
tree ~/go/bin
cd ~/go/bin
./eks-node-viewer
3 nodes (875m/5790m) 15.1% cpu ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ $0.156/hour | $113.880/month
20 pods (0 pending 20 running 20 bound)

ip-192-168-3-82.ap-northeast-2.compute.internal  cpu ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  17% (6 pods) t3.medium/$0.0520 On-Demand - Ready
ip-192-168-2-196.ap-northeast-2.compute.internal cpu ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  17% (7 pods) t3.medium/$0.0520 On-Demand - Ready
ip-192-168-1-205.ap-northeast-2.compute.internal cpu ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  12% (7 pods) t3.medium/$0.0520 On-Demand - ReadyPress any key to quit

명령 샘플
# Standard usage
./eks-node-viewer

# Display both CPU and Memory Usage
./eks-node-viewer --resources cpu,memory

# Karenter nodes only
./eks-node-viewer --node-selector "karpenter.sh/provisioner-name"

# Display extra labels, i.e. AZ
./eks-node-viewer --extra-labels topology.kubernetes.io/zone
3 nodes (875m/5790m)     15.1% cpu    ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ $0.156/hour | $113.880/month
        390Mi/10165092Ki 3.9%  memory ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
20 pods (0 pending 20 running 20 bound)

ip-192-168-3-82.ap-northeast-2.compute.internal  cpu    ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  17% (6 pods) t3.medium/$0.0520 On-Demand - Ready
                                                 memory █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   2%
ip-192-168-2-196.ap-northeast-2.compute.internal cpu    ██████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  17% (7 pods) t3.medium/$0.0520 On-Demand - Ready
                                                 memory ███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   8%
ip-192-168-1-205.ap-northeast-2.compute.internal cpu    ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  12% (7 pods) t3.medium/$0.0520 On-Demand - Ready
                                                 memory █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   2%

# Specify a particular AWS profile and region
AWS_PROFILE=myprofile AWS_REGION=us-west-2

# select only Karpenter managed nodes
node-selector=karpenter.sh/provisioner-name

# display both CPU and memory
resources=cpu,memory

1. Auto Scaling?

k8s에서 Auto Scaling은 크게 3가지 방식으로 작동하게 된다. HPA(Horizontal Pod Autoscaler), VPA(Vertical Pod Autoscaler), CA(Cluster Autoscaler)

https://www.oreilly.com/library/view/production-kubernetes/9781492092292/ch01.html

HPA는 Scale In/Out 방식으로 Resource API을 통해 15분 마다 메모리/CPU 사용량을 수집하여 정책에 맞게 Pod의 수를 증가/감소 시키는 Auto Scaling을 작동하게 된다.

VPA는 Scale Up/Down 방식으로 Resource 사용량을 수집하여 Pod을 Restart 하면서 Pod의 Resource을 증가/감소 시키는 Auto Scaling 방식이다.

CA는 노드 레벨에서의 감시가 이루어지며 워커 노드의 Resource을 확인하여 부족할 경우 Node을 추가 배포하여 이후 Pod을 새로운 Node에 배포해주는 방식이다.

2. HPA – Horizontal Pod Autoscaler

HPA 방식을 사용해서 Auto Scaling을 진행해본다. Pod의 수량 변동을 확인하기 위해 kube-ops-view와 그라파나 대쉬보드(대쉬보드 import id #17125)를 통해 모니터링 한다. 
우선 테스트에 사용할 Pod을 배포하고 모니터링을 진행한다.

# Run and expose php-apache server
curl -s -O https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/application/php-apache.yaml
cat php-apache.yaml | yh
kubectl apply -f php-apache.yaml

# 확인
kubectl exec -it deploy/php-apache -- cat /var/www/html/index.php
...

# 모니터링 : 터미널2개 사용
watch -d 'kubectl get hpa,pod;echo;kubectl top pod;echo;kubectl top node'
kubectl exec -it deploy/php-apache -- top

# 접속
PODIP=$(kubectl get pod -l run=php-apache -o jsonpath={.items[0].status.podIP})
curl -s $PODIP; echo

# Create the HorizontalPodAutoscaler : requests.cpu=200m - 알고리즘
# Since each pod requests 200 milli-cores by kubectl run, this means an average CPU usage of 100 milli-cores.
kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10
kubectl describe hpa
Warning: autoscaling/v2beta2 HorizontalPodAutoscaler is deprecated in v1.23+, unavailable in v1.26+; use autoscaling/v2 HorizontalPodAutoscaler
Name:                                                  php-apache
Namespace:                                             default
Labels:                                                <none>
Annotations:                                           <none>
CreationTimestamp:                                     Thu, 25 May 2023 21:37:49 +0900
Reference:                                             Deployment/php-apache
Metrics:                                               ( current / target )
  resource cpu on pods  (as a percentage of request):  0% (1m) / 50%
Min replicas:                                          1
Max replicas:                                          10
Deployment pods:                                       1 current / 1 desired
Conditions:
  Type            Status  Reason               Message
  ----            ------  ------               -------
  AbleToScale     True    ScaleDownStabilized  recent recommendations were higher than current one, applying the highest recent recommendation
  ScalingActive   True    ValidMetricFound     the HPA was able to successfully calculate a replica count from cpu resource utilization (percentage of request)
  ScalingLimited  False   DesiredWithinRange   the desired count is within the acceptable range
Events:           <none>

# HPA 설정 확인
kubectl krew install neat
kubectl get hpa php-apache -o yaml
kubectl get hpa php-apache -o yaml | kubectl neat | yh
spec: 
  minReplicas: 1               # [4] 또는 최소 1개까지 줄어들 수도 있습니다
  maxReplicas: 10              # [3] 포드를 최대 5개까지 늘립니다
  scaleTargetRef: 
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache           # [1] php-apache 의 자원 사용량에서
  metrics: 
  - type: Resource
    resource: 
      name: cpu
      target: 
        type: Utilization
        averageUtilization: 50  # [2] CPU 활용률이 50% 이상인 경우

모니터링이 잘 되고 있는 것을 확인했으니 이제 부하를 발생시켜서 Pod가 증가하는지 확인해보도록 한다. POD IP을 직접 타겟해서 반복 접속을 실행했고 CPU 사용량이 증가하면서 Pod가 1개에서 2개로 늘어난 것을 확인할 수 있다. Pod의 CPU는 200m으로 할당이 되어있었고 50%이상 사용 시 증가하는 규칙이 있기 때문에 1번 Pod의 사용량이 100m이 넘으면서 2번 Pod가 배포 된 것을 알 수 있다.

# 반복 접속 1 (파드1 IP로 접속) >> 증가 확인 후 중지
while true;do curl -s $PODIP; sleep 0.5; done

2번째 테스트는 Pod IP가 아닌 서비스명 도메인으로 접속하는 테스트를 진행해보았다.
부하 생성을 진행했고 서비스 도메인으로 접속하기 때문에 Pod가 늘어날 때마다 Pod가 늘어났다. 다만, 10개를 MAX로 했지만 7개까지밖에 늘어나지 않는 것을 확인할 수 있었다.
이유는 아마도, 이 정도의 부하만으로는 7개의 Pod가 견딜 수 있고 그로인해 CPU 사용률이 50%을 넘지않아 Pod가 더 추가되지 않는 것으로 보인다.

# 반복 접속 2 (서비스명 도메인으로 접속) >> 증가 확인(몇개까지 증가되는가? 그 이유는?) 후 중지 >> 중지 5분 후 파드 갯수 감소 확인
# Run this in a separate terminal
# so that the load generation continues and you can carry on with the rest of the steps
kubectl run -i --tty load-generator --rm --image=busybox:1.28 --restart=Never -- /bin/sh -c "while sleep 0.01; do wget -q -O- http://php-apache; done"

부하 발생을 중지하고 약 5분 정도의 시간이 지난 뒤에 Pod가 1개로 감소한 것을 확인할 수 있었다. 감소 규칙도 잘 적용된 것을 확인할 수 있었다.

3. KEDA – Kubernetes based Event Driven Autoscaler

위에서 실습한 HPA는 CPU, Memory와 같은 Resource Metic을 기반으로 스케일을 구성하게 되는데 KEDA는 특정 이벤트 기반으로 스케일을 구성할 수 있

https://keda.sh/docs/2.10/concepts/

테스트를 진행하기에 앞서 그라파나 대시보드를 구성하는데 아래 json 파일을 Import 해서 준비하였다.
https://github.com/kedacore/keda/blob/main/config/grafana/keda-dashboard.json
이후 KEDA을 설치하고 테스트를 진행해보았다.
ScaledObject 정책은 minReplica 0, MaxReplica 2에 정시부터 15분 간격으로 Pod가 생성되고 다시 5분부터 15분 단위로 Pod가 줄어드는 규칙이 적용되어 있다.

# KEDA 설치
cat <<EOT > keda-values.yaml
metricsServer:
  useHostNetwork: true

prometheus:
  metricServer:
    enabled: true
    port: 9022
    portName: metrics
    path: /metrics
    serviceMonitor:
      # Enables ServiceMonitor creation for the Prometheus Operator
      enabled: true
    podMonitor:
      # Enables PodMonitor creation for the Prometheus Operator
      enabled: true
  operator:
    enabled: true
    port: 8080
    serviceMonitor:
      # Enables ServiceMonitor creation for the Prometheus Operator
      enabled: true
    podMonitor:
      # Enables PodMonitor creation for the Prometheus Operator
      enabled: true

  webhooks:
    enabled: true
    port: 8080
    serviceMonitor:
      # Enables ServiceMonitor creation for the Prometheus webhooks
      enabled: true
EOT

kubectl create namespace keda
helm repo add kedacore https://kedacore.github.io/charts
helm install keda kedacore/keda --version 2.10.2 --namespace keda -f keda-values.yaml

# KEDA 설치 확인
kubectl get-all -n keda
kubectl get all -n keda
kubectl get crd | grep keda

# keda 네임스페이스에 디플로이먼트 생성
kubectl apply -f php-apache.yaml -n keda
kubectl get pod -n keda

# ScaledObject 정책 생성 : cron
cat <<EOT > keda-cron.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: php-apache-cron-scaled
spec:
  minReplicaCount: 0
  maxReplicaCount: 2
  pollingInterval: 30
  cooldownPeriod: 300
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  triggers:
  - type: cron
    metadata:
      timezone: Asia/Seoul
      start: 00,15,30,45 * * * *
      end: 05,20,35,50 * * * *
      desiredReplicas: "1"
EOT
kubectl apply -f keda-cron.yaml -n keda

# 모니터링
watch -d 'kubectl get ScaledObject,hpa,pod -n keda'
kubectl get ScaledObject -w

# 확인
kubectl get ScaledObject,hpa,pod -n keda
kubectl get hpa -o jsonpath={.items[0].spec} -n keda | jq
...
"metrics": [
    {
      "external": {
        "metric": {
          "name": "s0-cron-Asia-Seoul-00,15,30,45xxxx-05,20,35,50xxxx",
          "selector": {
            "matchLabels": {
              "scaledobject.keda.sh/name": "php-apache-cron-scaled"
            }
          }
        },
        "target": {
          "averageValue": "1",
          "type": "AverageValue"
        }
      },
      "type": "External"
    }

# KEDA 및 deployment 등 삭제
kubectl delete -f keda-cron.yaml -n keda && kubectl delete deploy php-apache -n keda && helm uninstall keda -n keda
kubectl delete namespace keda

테스트를 진행하게 되면 처음엔 0개의 Pod로 시작해서 정시부터 15분 단위로 1개의 Pod가 생성되고 다시 약 10분 뒤에 Pod가 줄어들어 0개의 Pod가 되는 것을 확인할 수 있다.
이렇게 간단하게 KEDA을 사용해서 이벤트 기반으로 Autoscale을 진행해보았다.

4. VPA – Vertical Pod Autoscaler

Pod의 수를 증가/감소시켰던 HPA에 이어 이번에는 Pod의 Resource 자체를 증가/감소시키는 VPA을 실습한다.
아래 코드를 사용하여 VPA 환경을 배포하고 그라파나 대시보드는 14588로 Import 하여 준비한다.

# 코드 다운로드
git clone https://github.com/kubernetes/autoscaler.git
cd ~/autoscaler/vertical-pod-autoscaler/
tree hack

# openssl 버전 확인
openssl version
OpenSSL 1.0.2k-fips  26 Jan 2017

# openssl 1.1.1 이상 버전 확인
yum install openssl11 -y
openssl11 version
OpenSSL 1.1.1g FIPS  21 Apr 2020

# 스크립트파일내에 openssl11 수정
sed -i 's/openssl/openssl11/g' ~/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/gencerts.sh

# Deploy the Vertical Pod Autoscaler to your cluster with the following command.
watch -d kubectl get pod -n kube-system
cat hack/vpa-up.sh
./hack/vpa-up.sh
kubectl get crd | grep autoscaling

VPA Autoscaler을 작동시켜보았다. CPU와 Memory가 확장된 것을 볼 수 있다.
VPA을 통해 HPA의 수평 확장과 다르게 수직 확장을 하는 것을 확인할 수 있었다. 다만 VPA는 HPA와 동시에 사용할 수 없기 때문에 단일 Pod에서의 처리량을 높일지 아니면 Pod의 수량을 늘려서 처리량을 받아낼지 결정을 해서 HPA와 VPA 중에 선택하면 좋을 것 같다.

# 모니터링
watch -d kubectl top pod

# 공식 예제 배포
cd ~/autoscaler/vertical-pod-autoscaler/
cat examples/hamster.yaml | yh
kubectl apply -f examples/hamster.yaml && kubectl get vpa -w

# 파드 리소스 Requestes 확인
kubectl describe pod | grep Requests: -A2
    Requests:
      cpu:        100m
      memory:     50Mi
--
    Requests:
      cpu:        587m
      memory:     262144k
--
    Requests:
      cpu:        587m
      memory:     262144k

# VPA에 의해 기존 파드 삭제되고 신규 파드가 생성됨
kubectl get events --sort-by=".metadata.creationTimestamp" | grep VPA
111s        Normal    EvictedByVPA             pod/hamster-5bccbb88c6-dq4td         Pod was evicted by VPA Updater to apply resource recommendation.
51s         Normal    EvictedByVPA             pod/hamster-5bccbb88c6-bpfwj         Pod was evicted by VPA Updater to apply resource recommendation.

5. CA – Cluster Autoscaler

CA는 위의 HPA, VPA와 다르게 Pod의 수나 리소스를 증가/감소시키는 게 아닌 Node을 증가/감소시키는 Scaler 이다.
Pending 상태인 Pod가 있을 경우 Node의 리소스가 부족해 Pod가 배포되지 않는 것으로 인지하기 때문에 Node을 증가시킨다. EKS는 AWS 환경 내에서 작동하기 때문에 ASG(Auto Scaling Group)과 병행하여 사용할 수 있다.

https://catalog.us-east-1.prod.workshops.aws/workshops/9c0aa9ab-90a9-44a6-abe1-8dff360ae428/ko-KR/100-scaling/200-cluster-scaling

CA 설정을 하기 전에 기존 배포되어 있는 EKS의 CA 설정 확인을 통해 CA 설정이 활성화 되어있는 것을 확인할 수 있었다.

# EKS 노드에 이미 아래 tag가 들어가 있음
# k8s.io/cluster-autoscaler/enabled : true
# k8s.io/cluster-autoscaler/myeks : owned
aws ec2 describe-instances  --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Reservations[*].Instances[*].Tags[*]" --output yaml | yh
...
    - Key: k8s.io/cluster-autoscaler/enabled
      Value: 'true'
    - Key: k8s.io/cluster-autoscaler/myeks
      Value: owned
...

CA는 앞서 위에서 설명한 것과 같이 ASG와 통합해서 구성되기 때문에 ASG의 Launch Template을 기반으로 Auto Scaling을 진행한다.
ASG 정보를 확인하고 기본 설정으로 되어있는 min, max값을 확인한 다음 max 값을 6으로 수정한다.

# 현재 autoscaling(ASG) 정보 확인
# aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='클러스터이름']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" --output table
aws autoscaling describe-auto-scaling-groups \
    --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" \
    --output table
-----------------------------------------------------------------
|                   DescribeAutoScalingGroups                   |
+------------------------------------------------+----+----+----+
|  eks-ng1-4ec4291a-0b0e-503f-7a2a-ccc015608963  |  3 |  3 |  3 |
+------------------------------------------------+----+----+----+

# MaxSize 6개로 수정
export ASG_NAME=$(aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].AutoScalingGroupName" --output text)
aws autoscaling update-auto-scaling-group --auto-scaling-group-name ${ASG_NAME} --min-size 3 --desired-capacity 3 --max-size 6

# 확인
aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" --output table
-----------------------------------------------------------------
|                   DescribeAutoScalingGroups                   |
+------------------------------------------------+----+----+----+
|  eks-ng1-4ec4291a-0b0e-503f-7a2a-ccc015608963  |  3 |  6 |  3 |
+------------------------------------------------+----+----+----+

Max값이 변경 된 것을 확인했다면 CA 서비스를 배포하고 CA Pod가 동작하는 Node가 추후에 Auto Scaling 정책으로 인해 evict 되지 않도록 설정도 진행한다. 해당 설정을 진행하지 않으면 사용량 증가로 인해 Node가 증설됐다가 추후 감소될 때 CA Pod가 배포되어 있는 Node가 사라질 경우 CA 서비스에 영향이 갈 수 있으니 설정을 진행하도록 한다.

# 배포 : Deploy the Cluster Autoscaler (CA)
curl -s -O https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-autodiscover.yaml
sed -i "s/<YOUR CLUSTER NAME>/$CLUSTER_NAME/g" cluster-autoscaler-autodiscover.yaml
kubectl apply -f cluster-autoscaler-autodiscover.yaml

# 확인
kubectl get pod -n kube-system | grep cluster-autoscaler
cluster-autoscaler-74785c8d45-mqkd4             1/1     Running   0             26s

kubectl describe deployments.apps -n kube-system cluster-autoscaler
Name:                   cluster-autoscaler
Namespace:              kube-system
CreationTimestamp:      Fri, 26 May 2023 10:02:21 +0900
Labels:                 app=cluster-autoscaler
Annotations:            deployment.kubernetes.io/revision: 1
Selector:               app=cluster-autoscaler
Replicas:               1 desired | 1 updated | 1 total | 1 available | 0 unavailable

# (옵션) cluster-autoscaler 파드가 동작하는 워커 노드가 퇴출(evict) 되지 않게 설정
kubectl -n kube-system annotate deployment.apps/cluster-autoscaler cluster-autoscaler.kubernetes.io/safe-to-evict="false"
deployment.apps/cluster-autoscaler annotated

설정 확인 및 변경을 진행했으니 Test App을 배포하고 CA가 잘 작동하는지 테스트 해보도록 한다.
Test App의 Replicas를 15로 변경하면 Node가 2개 추가되면서 Pending 상태의 Pod들을 배포하는 것을 확인할 수 있었다.
CA가 잘 작동한 것을 확인했다면 test app을 삭

# 모니터링 
kubectl get nodes -w
while true; do kubectl get node; echo "------------------------------" ; date ; sleep 1; done
while true; do aws ec2 describe-instances --query "Reservations[*].Instances[*].{PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text ; echo "------------------------------"; date; sleep 1; done

# Deploy a Sample App
# We will deploy an sample nginx application as a ReplicaSet of 1 Pod
cat <<EoF> nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-to-scaleout
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        service: nginx
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx-to-scaleout
        resources:
          limits:
            cpu: 500m
            memory: 512Mi
          requests:
            cpu: 500m
            memory: 512Mi
EoF

kubectl apply -f nginx.yaml
kubectl get deployment/nginx-to-scaleout

# Scale our ReplicaSet
# Let’s scale out the replicaset to 15
kubectl scale --replicas=15 deployment/nginx-to-scaleout && date

# 확인
kubectl get pods -l app=nginx -o wide --watch
kubectl -n kube-system logs -f deployment/cluster-autoscaler

# 노드 자동 증가 확인
kubectl get nodes
aws autoscaling describe-auto-scaling-groups \
    --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" \
    --output table

./eks-node-viewer
5 nodes (8725m/9650m) 90.4% cpu ████████████████████████████████████░░░░ $0.260/hour | $189.800/month
42 pods (0 pending 42 running 42 bound)

ip-192-168-3-82.ap-northeast-2.compute.internal  cpu █████████████████████████████████░░  95% (9 pods)  t3.medium/$0.0520 On-Demand - Ready
ip-192-168-2-196.ap-northeast-2.compute.internal cpu ███████████████████████████████████ 100% (11 pods) t3.medium/$0.0520 On-Demand - Ready
ip-192-168-1-205.ap-northeast-2.compute.internal cpu ███████████████████████████████░░░░  89% (10 pods) t3.medium/$0.0520 On-Demand - Ready
ip-192-168-1-60.ap-northeast-2.compute.internal  cpu █████████████████████████████░░░░░░  84% (6 pods)  t3.medium/$0.0520 On-Demand - Ready
ip-192-168-2-125.ap-northeast-2.compute.internal cpu █████████████████████████████░░░░░░  84% (6 pods)  t3.medium/$0.0520 On-Demand - Ready

CA 증가가 잘 되는 것을 확인했다면 감소도 잘 작동하는지 확인해보도록 한다.
노드 갯수 축소를 강제로 진행할 수 있지만 test app을 삭제하고 10분 정도 대기 후에 node가 감소하는지를 확인해보도록 한다.
약 10분 정도 시간이 지나면 Node가 감소하는 것을 알 수 있다. 증가 된 Node가 제거되고 기존 운영 중이던 Node는 유지되는 것까지 확인하였다. (AGE를 통해)

# 디플로이먼트 삭제
kubectl delete -f nginx.yaml && date

# 노드 갯수 축소 : 기본은 10분 후 scale down 됨, 물론 아래 flag 로 시간 수정 가능 >> 그러니 디플로이먼트 삭제 후 10분 기다리고 나서 보자!
# By default, cluster autoscaler will wait 10 minutes between scale down operations, 
# you can adjust this using the --scale-down-delay-after-add, --scale-down-delay-after-delete, 
# and --scale-down-delay-after-failure flag. 
# E.g. --scale-down-delay-after-add=5m to decrease the scale down delay to 5 minutes after a node has been added.

# 터미널1
watch -d kubectl get node

CA 실습이 마무리 되었다면 다시 Node MAX을 기본값이었던 3으로 수정하도록 한다.

CA는 단순 Pod을 늘리거나 Pod의 리소스를 증가시켜주는 방식이 아닌 Node의 수를 늘리는 방식으로 순간 많은 Pod가 필요할 때 유용한 방식처럼 보인다. 하지만 CA에는 몇가지 문제점이 있다.
우선, 실습을 하면서 느낀 부분이지만 HPA, VPA에 비해 Scaling 속도가 매우 느리다. Node가 새로 배포되어야 하니 Pod만 새로 배포하는 거에 비해 시간이 오래 소요됐다.
그리고 ASG와 EKS의 결합으로 구성되는 Scaler이기 때문에 각각 서로의 정보 동기화가 부족하다. 서로 다른 정보를 갖고 있기 때문에 EKS에서 노드를 삭제해도 ASG에는 인스턴스가 남아있는 상황이 발생하게 된다. 그리고 노드=인스턴스가 성립되지 않기 때문에 Pod가 적게 배포 된 Node 먼저 없애는 게 사실상 어려운 상황이다.
그리고 Scaling 조건이 Pending 상태의 Pod가 생겼을 때이기 때문에 Req 양이 많을 때 무조건적인 ScaleOut이 발생하는 게 아니어서 서비스 장애가 발생할 수 있다.
이런 부분을 잘 참고해서 CA 사용을 고려해봐야 할 것 같다.

6. CPA – Cluster Proportional Autoscaler

CPA는 CA 등으로 인해 Node의 Scaling 이 발생했을 때 노드 수가 증가함에 따라 성능 이슈가 발생할 수 있는 어플리케이션 Pod의 수를 수평적으로 증가시켜주는 서비스이다.
예를 들면, Node가 증가함에 따라 CoreDNS의 부하가 증가할 수 있는데 그 부분을 해소시키기 위해 Node의 증가폭에 따라 CoreDNS Pod을 수평 증가시켜준다.

CPA 테스트를 하기 위해 test app과 CPA을 설치한다.
CPA 규칙을 설정하지 않은 상태에서 CPA을 릴리즈 하려고 하면 실패하게 된다.
test app과 cpa 규칙 설정을 할 cpa-values.yaml을 생성한 다음 CPA 릴리즈를 다시 진행한다.
CPA 설정은 Node 수에 따라 Pod 수가 동일하게 증가하도록 설정을 했다. 따라서 CPA 릴리즈가 된다면 현재 Node가 3대이기 때문에 Test App의 Pod도 3개로 증가해야 하는데 릴리즈 되자마자 Pod가 3개가 되는 것을 확인할 수 있었다.

#
helm repo add cluster-proportional-autoscaler https://kubernetes-sigs.github.io/cluster-proportional-autoscaler

# CPA규칙을 설정하고 helm차트를 릴리즈 필요
# CPA 규칙을 설정하지 않은 상태에서는 helm 차트 릴리즈가 실패한다.
helm upgrade --install cluster-proportional-autoscaler cluster-proportional-autoscaler/cluster-proportional-autoscaler
Release "cluster-proportional-autoscaler" does not exist. Installing it now.
Error: execution error at (cluster-proportional-autoscaler/templates/deployment.yaml:3:3): options.target must be one of deployment, replicationcontroller, or replicaset

# nginx 디플로이먼트 배포
cat <<EOT > cpa-nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        resources:
          limits:
            cpu: "100m"
            memory: "64Mi"
          requests:
            cpu: "100m"
            memory: "64Mi"
        ports:
        - containerPort: 80
EOT
kubectl apply -f cpa-nginx.yaml

# CPA 규칙 설정
cat <<EOF > cpa-values.yaml
config:
  ladder:
    nodesToReplicas:
      - [1, 1]
      - [2, 2]
      - [3, 3]
      - [4, 3]
      - [5, 5]
options:
  namespace: default
  target: "deployment/nginx-deployment"
EOF

# 모니터링
watch -d kubectl get pod

# helm 업그레이드
helm upgrade --install cluster-proportional-autoscaler -f cpa-values.yaml cluster-proportional-autoscaler/cluster-proportional-autoscaler

이제는 Node을 5개로 증가시켜보도록 하겠다.
min, max 그리고 desired을 모두 5로 변경하면 시간이 2~3분 정도 흐른 뒤 Node 2개가 추가로 생성 된다. 이후 Node가 등록되면 바로 Pod도 5개로 증가하는 것을 확인할 수 있었다.

# 노드 5개로 증가
export ASG_NAME=$(aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].AutoScalingGroupName" --output text)
aws autoscaling update-auto-scaling-group --auto-scaling-group-name ${ASG_NAME} --min-size 5 --desired-capacity 5 --max-size 5
aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" --output table
-----------------------------------------------------------------
|                   DescribeAutoScalingGroups                   |
+------------------------------------------------+----+----+----+
|  eks-ng1-4ec4291a-0b0e-503f-7a2a-ccc015608963  |  5 |  5 |  5 |
+------------------------------------------------+----+----+----+

Node 감소 테스트도 진행해보도록 한다.
증가 때와 마찬가지로 max, min 그리고 desired을 모두 4로 변경하면 잠시 후 Node 1개가 제외되고 Pod도 3개로 변경되는 것을 확인할 수 있다. (node 4:pod 3규칙이기 때문에)


# 노드 4개로 축소
aws autoscaling update-auto-scaling-group --auto-scaling-group-name ${ASG_NAME} --min-size 4 --desired-capacity 4 --max-size 4
aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" --output table
-----------------------------------------------------------------
|                   DescribeAutoScalingGroups                   |
+------------------------------------------------+----+----+----+
|  eks-ng1-4ec4291a-0b0e-503f-7a2a-ccc015608963  |  4 |  4 |  4 |
+------------------------------------------------+----+----+----+


CPA는 CA와 병행해서 사용하면 Node가 증가할 때 자연스럽게 Pod을 증가시킬 수 있어서 CA의 아쉬운 부분을 조금 채울 수 있는 서비스라고 생각한다.

7. Karpenter : K8S Native AutoScaler & Fargate

Karpenter는 k8s를 위한 Auto Scaling 솔루션 중 하나로 Cluster 내의 워크로드를 효율적으로 관리하고 리소스를 효과적으로 활용할 수 있게 해준다. CSP나 다른 스케줄러와 독립적으로 동작하며 k8s object와 native하게 통합되는 특징이 있다.

Karpenter 실습을 진행하기 전에 앞서 진행했던 실습 내용들을 정리하기 위해 작업을 진행한다.

# helm 삭제
helm uninstall -n kube-system kube-ops-view
helm uninstall -n monitoring kube-prometheus-stack

# cloudformation 삭제
eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME

이전 실습 내용이 삭제됐다면 카펜터 실습을 위한 환경을 다시 배포한다.

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

# CloudFormation 스택 배포
aws cloudformation deploy --template-file karpenter-preconfig.yaml --stack-name myeks2 --parameter-overrides KeyName=aewspair SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=AKIA5... MyIamUserSecretAccessKey='CVNa2...' ClusterBaseName=myeks2 --region ap-northeast-2

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

# 작업용 EC2 SSH 접속
ssh -i aewspair.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks2 --query 'Stacks[*].Outputs[0].OutputValue' --output text)

실습 환경을 배포 후 EKS 배포 전 사전 확인 및 EKS Node Viewer을 설치한다.

# IP 주소 확인 : 172.30.0.0/16 VPC 대역에서 172.30.100.0/24 대역을 사용 중
ip -br -c addr
lo               UNKNOWN        127.0.0.1/8 ::1/128
eth0             UP             172.30.1.100/24 fe80::ac:c5ff:fec2:77b8/64
docker0          DOWN           172.17.0.1/16

# EKS Node Viewer 설치 : 현재 ec2 spec에서는 설치에 다소 시간이 소요됨 = 2분 이상
go install github.com/awslabs/eks-node-viewer/cmd/eks-node-viewer@latest

# [터미널1] bin 확인 및 사용
tree ~/go/bin
cd ~/go/bin
./eks-node-viewer -h

사전 확인 및 EKS Node Viewer 설치를 완료했다면 이제 EKS 배포를 진행한다.
아래 진행할 EKS 배포에는 다음과 같은 내용이 포함되어 있다.
1) IAM Policy, Role, EC2 Instance Profile을 생성
2) Karpenter가 인스턴스를 시작할 수 있도록 IRSA 사용
3) Karpenter Node Role을 kube-auth configmap에 추가하여 연결 허용
4-1) kube-system 및 karpenter Namespace에 EKS Managed Node Group 사용
4-2) Managed Node Group 대신 Fargate을 사용하고 싶다면 Fargate 주석을 제거하고 managedNodeGroups에 주석 처리를 진행
5) Spot Instance 허용 역할 생성
6) helm 통한 Karpenter 설치

# 환경변수 정보 확인
export | egrep 'ACCOUNT|AWS_|CLUSTER' | egrep -v 'SECRET|KEY'
declare -x ACCOUNT_ID="..."
declare -x AWS_ACCOUNT_ID="..."
declare -x AWS_DEFAULT_REGION="ap-northeast-2"
declare -x AWS_PAGER=""
declare -x AWS_REGION="ap-northeast-2"
declare -x CLUSTER_NAME="myeks"

# 환경변수 설정
export KARPENTER_VERSION=v0.27.5
export TEMPOUT=$(mktemp)
echo $KARPENTER_VERSION $CLUSTER_NAME $AWS_DEFAULT_REGION $AWS_ACCOUNT_ID $TEMPOUT

# CloudFormation 스택으로 IAM Policy, Role, EC2 Instance Profile 생성 : 3분 정도 소요
curl -fsSL https://karpenter.sh/"${KARPENTER_VERSION}"/getting-started/getting-started-with-karpenter/cloudformation.yaml  > $TEMPOUT \
&& aws cloudformation deploy \
  --stack-name "Karpenter-${CLUSTER_NAME}" \
  --template-file "${TEMPOUT}" \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides "ClusterName=${CLUSTER_NAME}"

# 클러스터 생성 : myeks2 EKS 클러스터 생성 19분 정도 소요
eksctl create cluster -f - <<EOF
---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: ${CLUSTER_NAME}
  region: ${AWS_DEFAULT_REGION}
  version: "1.24"
  tags:
    karpenter.sh/discovery: ${CLUSTER_NAME}

iam:
  withOIDC: true
  serviceAccounts:
  - metadata:
      name: karpenter
      namespace: karpenter
    roleName: ${CLUSTER_NAME}-karpenter
    attachPolicyARNs:
    - arn:aws:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerPolicy-${CLUSTER_NAME}
    roleOnly: true

iamIdentityMappings:
- arn: "arn:aws:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}"
  username: system:node:{{EC2PrivateDNSName}}
  groups:
  - system:bootstrappers
  - system:nodes

managedNodeGroups:
- instanceType: m5.large
  amiFamily: AmazonLinux2
  name: ${CLUSTER_NAME}-ng
  desiredCapacity: 2
  minSize: 1
  maxSize: 10
  iam:
    withAddonPolicies:
      externalDNS: true

## Optionally run on fargate
# fargateProfiles:
# - name: karpenter
#  selectors:
#  - namespace: karpenter
EOF

# eks 배포 확인
eksctl get cluster
eksctl get nodegroup --cluster $CLUSTER_NAME
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
eksctl get addon --cluster $CLUSTER_NAME

# [터미널1] eks-node-viewer
cd ~/go/bin && ./eks-node-viewer

# k8s 확인
kubectl cluster-info
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
kubectl get pod -n kube-system -owide
kubectl describe cm -n kube-system aws-auth
...
mapRoles:
----
- groups:
  - system:bootstrappers
  - system:nodes
  rolearn: arn:aws:iam::...:role/KarpenterNodeRole-myeks
  username: system:node:{{EC2PrivateDNSName}}
- groups:
  - system:bootstrappers
  - system:nodes
  rolearn: arn:aws:iam::...:role/eksctl-myeks-nodegroup-myeks-ng-NodeInstanceRole-8QULZTOJ5C5D  username: system:node:{{EC2PrivateDNSName}}
...

# 카펜터 설치를 위한 환경 변수 설정 및 확인
export CLUSTER_ENDPOINT="$(aws eks describe-cluster --name ${CLUSTER_NAME} --query "cluster.endpoint" --output text)"
export KARPENTER_IAM_ROLE_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:role/${CLUSTER_NAME}-karpenter"
echo $CLUSTER_ENDPOINT $KARPENTER_IAM_ROLE_ARN

# EC2 Spot Fleet 사용을 위한 service-linked-role 생성 확인 : 만들어있는것을 확인하는 거라 아래 에러 출력이 정상!
# If the role has already been successfully created, you will see:
# An error occurred (InvalidInput) when calling the CreateServiceLinkedRole operation: Service role name AWSServiceRoleForEC2Spot has been taken in this account, please try a different suffix.
aws iam create-service-linked-role --aws-service-name spot.amazonaws.com || true

# docker logout : Logout of docker to perform an unauthenticated pull against the public ECR
docker logout public.ecr.aws

# karpenter 설치
helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter --version ${KARPENTER_VERSION} --namespace karpenter --create-namespace \
  --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=${KARPENTER_IAM_ROLE_ARN} \
  --set settings.aws.clusterName=${CLUSTER_NAME} \
  --set settings.aws.defaultInstanceProfile=KarpenterNodeInstanceProfile-${CLUSTER_NAME} \
  --set settings.aws.interruptionQueueName=${CLUSTER_NAME} \
  --set controller.resources.requests.cpu=1 \
  --set controller.resources.requests.memory=1Gi \
  --set controller.resources.limits.cpu=1 \
  --set controller.resources.limits.memory=1Gi \
  --wait

# 확인
kubectl get-all -n karpenter
kubectl get all -n karpenter
kubectl get cm -n karpenter karpenter-global-settings -o jsonpath={.data} | jq
kubectl get crd | grep karpenter

EKS와 karpenter 설치를 완료하고 pod가 생성되는 걸 확인하기 위해 kubeopsview와 external-dns 설치도 진행했다.

# ExternalDNS
MyDomain=bs-yang.com
echo "export MyDomain=bs-yang.com" >> /etc/profile
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text)
echo $MyDomain, $MyDnzHostedZoneId
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml
MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst < externaldns.yaml | kubectl apply -f -

# kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set env.TZ="Asia/Seoul" --namespace kube-system
kubectl patch svc -n kube-system kube-ops-view -p '{"spec":{"type":"LoadBalancer"}}'
kubectl annotate service kube-ops-view -n kube-system "external-dns.alpha.kubernetes.io/hostname=kubeopsview.$MyDomain"
echo -e "Kube Ops View URL = http://kubeopsview.$MyDomain:8080/#scale=1.5"

provisioner 생성을 진행하도록 한다. provisioner는 Karpenter의 핵심 구성 요소로, 워크로드의 요구 사항에 따라 클러스터에 노드를 프로비저닝하고, 확장 및 축소를 관리하게 된다. Provisioner는 노드 유형, 가용성 영역, 리소스 제한 등과 같은 정책을 사용하여 프로비저닝을 수행하는 구성 요소이다. provisioner의 정책에 따라 scale out 시 프로비저닝이 진행된다고 보면 된다.

#
cat <<EOF | kubectl apply -f -
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: default
spec:
  requirements:
    - key: karpenter.sh/capacity-type
      operator: In
      values: ["spot"]
  limits:
    resources:
      cpu: 1000
  providerRef:
    name: default
  ttlSecondsAfterEmpty: 30
---
apiVersion: karpenter.k8s.aws/v1alpha1
kind: AWSNodeTemplate
metadata:
  name: default
spec:
  subnetSelector:
    karpenter.sh/discovery: ${CLUSTER_NAME}
  securityGroupSelector:
    karpenter.sh/discovery: ${CLUSTER_NAME}
EOF

# 확인
kubectl get awsnodetemplates,provisioners
NAME                                        AGE
awsnodetemplate.karpenter.k8s.aws/default   2m35s

NAME                               AGE
provisioner.karpenter.sh/default   2m35s

Pod가 늘어나는 속도를 체감해보기 위해 프로메테우스와 그라파나를 설치하였다.

#
helm repo add grafana-charts https://grafana.github.io/helm-charts
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

kubectl create namespace monitoring

# 프로메테우스 설치
curl -fsSL https://karpenter.sh/"${KARPENTER_VERSION}"/getting-started/getting-started-with-karpenter/prometheus-values.yaml | tee prometheus-values.yaml
helm install --namespace monitoring prometheus prometheus-community/prometheus --values prometheus-values.yaml --set alertmanager.enabled=false

# 그라파나 설치
curl -fsSL https://karpenter.sh/"${KARPENTER_VERSION}"/getting-started/getting-started-with-karpenter/grafana-values.yaml | tee grafana-values.yaml
helm install --namespace monitoring grafana grafana-charts/grafana --values grafana-values.yaml --set service.type=LoadBalancer

# 그라파나 접속
kubectl annotate service grafana -n monitoring "external-dns.alpha.kubernetes.io/hostname=grafana.$MyDomain"
echo -e "grafana URL = http://grafana.$MyDomain"

Test App의 Replica을 0개로 배포하고 이후 Scale Out과 In을 테스트하면서 Scaling 속도가 얼마나 빠른지 지켜보았다.
우선은 Scale Out부터 진행하였는데 Replicas를 15개로 지정해보았다.
회사 랩탑 보안 상 영상으로 담지 못한 게 아쉬울 정도로 빠른 속도로 Pod 배포가 완료되었다. 순식간에 Spot Instance가 올라오고 Pod가 15개가 배포되는데 아주 짧은 시간밖에 걸리지 않았다.

# pause 파드 1개에 CPU 1개 최소 보장 할당
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: inflate
spec:
  replicas: 0
  selector:
    matchLabels:
      app: inflate
  template:
    metadata:
      labels:
        app: inflate
    spec:
      terminationGracePeriodSeconds: 0
      containers:
        - name: inflate
          image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
          resources:
            requests:
              cpu: 1
EOF
kubectl scale deployment inflate --replicas 15
kubectl logs -f -n karpenter -l app.kubernetes.io/name=karpenter -c controller

# 스팟 인스턴스 확인!
aws ec2 describe-spot-instance-requests --filters "Name=state,Values=active" --output table
kubectl get node -l karpenter.sh/capacity-type=spot -o jsonpath='{.items[0].metadata.labels}' | jq
kubectl get node --label-columns=eks.amazonaws.com/capacityType,karpenter.sh/capacity-type,node.kubernetes.io/instance-type
NAME                                                 STATUS   ROLES    AGE   VERSION                CAPACITYTYPE   CAPACITY-TYPE   INSTANCE-TYPE
ip-192-168-129-195.ap-northeast-2.compute.internal   Ready    <none>   51s   v1.24.13-eks-0a21954                  spot            c5d.4xlarge
ip-192-168-31-133.ap-northeast-2.compute.internal    Ready    <none>   38m   v1.24.13-eks-0a21954   ON_DEMAND                      m5.large
ip-192-168-40-55.ap-northeast-2.compute.internal     Ready    <none>   38m   v1.24.13-eks-0a21954   ON_DEMAND                      m5.large

반대로 Scale Down을 진행해보았다.
Deployment delete을 했고 바로 Spot Instance가 삭제되는 것을 확인할 수 있었다.

kubectl delete deployment inflate
kubectl logs -f -n karpenter -l app.kubernetes.io/name=karpenter -c controller

아래는 그라파나를 통해 Scaling 상황을 캡쳐한 화면이다. 실시간으로 영상 촬영은 하지 못했지만 엄청 빠른 속도로 배포가 되고 또 제거가 된 것을 알 수 있었다.

8. 정리

VPA, HPA 그리고 CA와 Karpenter을 통해 k8s Scaling을 경험해볼 수 있었다.
최근에 Karpenter에 대한 얘기들이 많이 나오고 있어 궁금했던 차에 경험해 볼 수 있어서 좋았던 것 같다.
다만, spot instance을 사용하는 건 조금 운영상 불안한 측면이 있기 때문에 이 부분만 고려한다면 좋은 Scaling 구성을 진행할 수 있을 것 같다는 생각을 했다.

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을 활용하는 부분도 실습해볼 예정이다.