Terraform Study #5(테라폼 워크플로)

이번 주제는 테라폼 워크플로와 파이프라인에 대해 다뤄볼 예정이다.

1. 워크플로

Terraform은 인프라스트럭처를 코드로 작성하고 관리할 수 있게 해주는 도구로, 일련의 명확한 워크플로를 제공한다. 이 워크플로를 따르면 사용자는 안정적이고 반복 가능한 방식으로 클라우드 리소스를 배포하고 변경할 수 있다. Terraform 워크플로는 크게는 Write, Plan, Apply로 구성되지만 상세한 단계는 다음 단계들로 구성된다.

초기화 (Initialization): Terraform 작업 디렉토리를 초기화한다. 명령어는 terraform init을 사용한다. 이 단계에서는 Terraform 설정 파일과 필요한 프로바이더 플러그인을 로드하고 프로바이더는 AWS, Azure, GCP 등과 같은 여러 클라우드 서비스에 대한 API 호출을 수행하게 된다.
코드 작성(Write): 사용자는 *.tf 파일에 IaC을 작성한다. 이 코드는 사용하려는 리소스, 설정 및 프로바이더 정보를 정의한다.
실행 계획 (Plan): 명령어로는 terraform plan을 사용한다. 현재 상태와 Terraform 코드를 비교하여 변경 사항을 표시한다. 이는 “예비 실행”과 같으며 실제 리소스에는 변경사항이 적용되지 않는다. 사용자는 이 단계에서 어떤 변경이 발생할지 미리 확인할 수 있다.
적용 (Apply): 명령어로는 terraform apply을 사용한다. 실행 계획에서 제시된 변경 사항을 실제 리소스에 적용한다. 사용자는 변경 사항을 승인한 후에만 이를 적용할 수 있다.
상태 관리 (State Management): Terraform은 .tfstate 파일에 리소스의 현재 상태를 저장한다. 이 상태 파일을 통해 Terraform은 실제 리소스와 Terraform 코드 사이의 매핑을 관리한다.
변수와 출력: variables.tf와 outputs.tf 파일을 사용하여 입력 변수를 정의하고 결과 출력을 관리할 수 있다.
모듈: 복잡한 인프라 구성을 모듈로 분리하여 코드 재사용성을 향상시킬 수 있다.
제거 (Destroy): terraform destroy: Terraform 코드에 정의된 리소스를 제거한다.

위 워크플로에 따라 Terraform은 선언적인 방식으로 인프라를 구성하고 관리할 수 있게 해준다. 사용자는 원하는 최종 상태만을 정의하면 되며, Terraform은 이를 실제 인프라에 반영하는데 필요한 모든 단계를 처리한다.

2. 워크플로 구분

워크플로는 단순하지만 개인과 다수 작업자에 따라 조금 내용이 달라질 수 있다.

개인 워크플로 (Individual Workflow):

  • 이 워크플로는 개발자나 관리자가 혼자 작업할 때 적합하다.
  • Terraform 코드를 로컬 시스템에서 직접 실행하게 된다.
  • Terraform 상태 파일도 로컬에 저장될 수 있다.
  • 이런 방식은 간단한 프로젝트나 테스트, 프로토타이핑 등에 적합하다.

단계:

terraform init: 초기화
terraform plan: 변경 사항 미리보기
terraform apply: 변경 사항 적용
필요한 경우, terraform destroy로 리소스 제거

다중 작업자 워크플로 (Team Workflow):

  • 팀이나 조직에서 여러 사람이 함께 작업할 때 사용되는 워크플로이다.
  • 상태 파일은 원격 스토리지(예: Amazon S3, Terraform Cloud)에 저장되어 여러 사람이 공유할 수 있게 된다.
  • 원격 스토리지를 사용하면 상태 파일의 동시 변경을 방지하는 락 기능을 활용할 수 있다.
  • Terraform Cloud나 Terraform Enterprise는 협업 기능과 함께 실행 환경, 상태 관리, 모듈 저장소 등의 추가 기능을 제공한다.

단계:

원격 스토리지 및 락 설정: Terraform 설정에서 backend를 사용하여 원격 스토리지를 지정합니다.
terraform init: 초기화 시 원격 스토리지에 연결
terraform plan: 변경 사항 미리보기
terraform apply: 변경 사항 적용
필요한 경우, terraform destroy로 리소스 제거

주의사항:
여러 작업자가 동시에 Terraform을 실행하지 않도록 주의해야 한다. 또한, 락 기능을 활용하여 동시 변경을 방지할 수 있다.
모든 팀원이 동일한 Terraform 버전을 사용하는 것이 좋다.
코드 리뷰, 버전 관리 (예: Git) 및 CI/CD 파이프라인과 같은 현대적인 개발 워크플로를 함께 사용하는 것이 좋다.

3. 격리

Terraform에서의 “격리”는 주로 Terraform 상태, 리소스, 구성 요소를 서로 독립적으로 관리하고 운영하기 위한 구조와 워크플로를 의미한다. 협업을 할 때 격리 구조를 설계하지 않으면 여러 문제점이 발생할 수 있다. 격리는 여러 목적으로 사용될 수 있으며, 주로 다음과 같은 이유로 필요하다.

  1. 환경별 분리: 개발, 스테이징, 프로덕션과 같은 다양한 환경을 독립적으로 관리하고 운영하려면 해당 환경별로 리소스와 상태를 분리해야 한다.
  2. 작업 영역 분리: 다양한 팀이나 프로젝트, 애플리케이션 등에 대한 작업을 독립적으로 수행하려면 해당 영역별로 리소스와 상태를 분리해야 한다.
  3. 변경의 최소화: 특정 구성 요소나 서비스에 변경이 발생했을 때, 그 영향을 해당 부분에 국한시켜 다른 부분에 영향을 주지 않도록 하기 위해 필요하다.\

Terraform에서 격리를 달성하는 주요 방법은 다음과 같다.

  1. 스테이트 격리: Terraform 상태 파일(terraform.tfstate)을 통해 리소스의 현재 상태를 추적한다. 다양한 환경이나 작업 영역에서 독립적인 상태 관리를 위해 각각의 상태 파일을 분리할 수 있다. 원격 스토리지(예: Amazon S3)를 사용하면 각 환경별로 별도의 버킷 또는 경로에 상태 파일을 저장하여 격리할 수 있다.
  2. 워크스페이스 사용: Terraform은 워크스페이스라는 기능을 제공하여 다양한 환경을 동일한 구성 내에서 격리할 수 있다. 각 워크스페이스는 고유한 상태를 가지며, 환경 변수를 통해 다른 환경의 설정 값을 제공할 수 있다.
  3. 모듈화: Terraform 모듈을 사용하면 코드를 재사용 가능한, 독립적인 단위로 분리할 수 있다. 이를 통해 리소스와 구성 요소를 독립적으로 관리하고 테스트할 수 있다.
  4. 환경 구성 분리: 다양한 환경(예: 개발, 스테이징, 프로덕션)에 대한 구성을 별도의 파일이나 디렉토리로 분리하여, 환경별로 다른 변수와 설정 값을 제공할 수 있다.

4. 프로비저닝 파이프라인 설계

스터디 실습에서는 Github Action과 Terraform Cloud 등을 사용하는 예제를 썼지만 나는 업무에 사용해봤던 AWS Code Commit/Pipeline/Build을 사용하려고 한다.

4.1 CodeCommit 생성

AWS Console에서 Code Commit 메뉴를 검색해 들어간 뒤 레포지토리 생성을 클릭한다.

리포지토리 이름을 입력하고 생성 버튼을 클릭한다.

생성은 거의 곧바로 완료되고 아래와 같은 화면을 확인할 수 있다.

vscode에서 CodeCommit에 push 등을 하기 위해 자격증명을 진행한다. 이때 IAM User을 사용했고 해당 User에는 AWSCodeCommitFullAccess 정책을 연결해주었다.
이후 해당 사용자에 대해 자격 증명 생성을 위해 IAM Console에서 해당 사용자에 대해 AWS CodeCommint에 대한 HTTPS Git 자격 증명 생명을 진행해주었다.

HTTPS(GRC)을 통해 연결하기 위해서 macOS에 git-remote-codecommit을 설치했다.

다른 계정과 충돌되지 않기 위해 aws configure –profile을 통해 별도 프로파일로 진행하였다

HTTPS(GRC) 복제를 한다.

git clone 명령어를 통해 vscodedㅔ서 코드를 작성 중인 폴더와 레포지토리 클론을 진행한다.

4.2 CodeBuild 생성

코드 빌드 및 테스트를 위해 Code Build을 생성한다. 빌드를 진행하기 전에 위에서 만든 CodeCommit에 브랜치 정보 확인이 가능해야 하기 때문에 사전에 readme.md 같은 파일 하나만이라도 push을 진행해주면 좋다.

# readme.md 작성
vi readme.md

# git push
git add -A
git commit -m 'Initial checkin'
git push
오브젝트 나열하는 중: 3, 완료.
오브젝트 개수 세는 중: 100% (3/3), 완료.
오브젝트 쓰는 중: 100% (3/3), 229 bytes | 229.00 KiB/s, 완료.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: Validating objects: 100%
To codecommit::ap-northeast-2://terraformiac
 * [new branch]      master -> master

AWS Console에서 CodeBuild 선택 후 프로젝트 만들기를 클릭한다.

빌드 프로젝트 이름 설정과 소스 설정을 진행해준다. 소스는 위에서 만든 CodeCommit과 브랜치를 선택해준다.

환경에 대한 내용도 작성해준다. 특별하게 필요한 내용이 없기 때문에 간단하게 설정해준다.

추가로 입력해줄 내용이 없으면 프로젝트 빌드 생성을 눌러준다.

빌드가 돌 때 Terraform 설치 및 init/plan을 하기 위해 buildspec 파일을 작성해준다.

#buildspec.yml
version: 0.2

phases:
  install:
    commands:
      - echo "Installing Terraform"
      - sudo yum install -y yum-utils
      - sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
      - sudo yum -y install terraform
      - terraform init
  pre_build:
    commands:
      - echo "Running terraform fmt"
      - terraform fmt
  build:
    commands:
      - echo "Running terraform plan"
      - terraform plan

4.3 Code 작성

테스트는 EC2 배포하는 것으로 진행할 예정이니 간단하게 EC2 작성하는 코드를 작성하고 Push까지 진행해본다.

# main.tf
provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_instance" "example" {
  ami           = "ami-084e92d3e117f7692"
  instance_type = "t2.micro"

  tags = {
    Name = "ybs-iac-ec2"
  }

}

#git push 진행
git add -A
git commit -m 'Initial checkin'
git push

오브젝트 나열하는 중: 5, 완료.
오브젝트 개수 세는 중: 100% (5/5), 완료.
Delta compression using up to 10 threads
오브젝트 압축하는 중: 100% (4/4), 완료.
오브젝트 쓰는 중: 100% (4/4), 691 bytes | 691.00 KiB/s, 완료.
Total 4 (delta 0), reused 0 (delta 0), pack-reused 0
remote: Validating objects: 100%
To codecommit::ap-northeast-2://terraformiac
   f8bf77b..0c7981d  master -> master

4.4 CodeDeploy/Pipeline 생성

Commit/Build를 만들었다면 deploy와 pipeline을 생성해준다.

CodeDeploy을 먼저 만들어준다. 간단하게 이름과 EC2를 선택해주고 생성을 누른다.

deploy 단계에서 작동할 appspec.yml파일을 작성해준다. 이때 build와는 다르게 apply도 같이 진행해준다.

#appspec.yml
version: 0.0
os: linux
files:
  - source: /
    destination: /home/ec2-user/terraform/
hooks:
  ApplicationStart:
    - location: scripts/run_build.sh
      timeout: 300
      runas: root

#ipts/run_build.sh
#!/bin/bash
terraform -chdir=/home/ec2-user/terraform/ init
terraform -chdir=/home/ec2-user/terraform/ apply -auto-approve

그 다음 pipeline을 만들어준다. 파이프라인의 이름을 간단하게 작성하고 다음을 누른다.

소스는 위에서 만든 codecommit과 브랜치를 선택해주고 다음을 누른다.

빌드 또한 위에서 만든 build 값을 입력해준다.

배포 스테이지는 위에서 만든 Deploy Application 정보를 입력해준다. (배포 그룹은 간단하게 만들 수 있으니 만들어서 넣어준다.)

그 후 별다른 수정사항이 없으면 파이프라인 생성을 눌러 생성을 진행한다.

파이프라인이 잘 생성되면 알아서 최초 1회 실행되는 것을 확인할 수 있다.

Build 단계에서 실패가 떴다.

원인을 찾아보니 EC2 생성하는 권한이 없어서 오류가 발생한 것으로 보인다. 권한을 추가해주고 다시 실행을 시켰다.

작은 몇 가지 트러블 슈팅을 진행하면서 정상적으로 돌아가기를 기다렸다.

EC2가 제대로 배포된 것을 확인할 수 있다.

5. 정리

이번 시간에는 워크플로와 나름의 파이프라인을 작성해서 테스트를 해보았다. 파이프라인은 프로젝트를 통해 조금은 익숙해졌다고 생각했는데 Role마다 넣어줘야 하는 권한들이 있어 자잘한 트러블슈팅이 꽤 많은 시간을 잡아먹은 거 같다. 그래도 AWS 네이비트 요소들로 테스트할 수 있어서 나름의 도움이 된 거 같다.

Terraform Study #4(State, Module, 협업)

오늘은 Terraform state, Module 그리고 협업에 대해 간략하게 알아볼 예정이다. IaC는 협업을 하기 위함임을 생각하면 오늘 배우는 내용이 중요할 거 같다.

1. State

1.1 State의 목적과 의미

State의 주요 목적과 의미는 다음과 같다.
현재 인프라 상태의 추적: State 파일은 Terraform 구성이 실제 클라우드 인프라에 어떻게 매핑되는지에 대한 정보를 포함한다. 이를 통해 Terraform은 다음 실행 시 어떤 리소스를 생성, 수정, 삭제할지 결정할 수 있다.
변경의 빠른 감지: Terraform은 State 파일을 기반으로 현재 인프라 상태와 구성의 차이점을 판단한다. 이를 통해 Terraform은 최소한의 변경만을 적용하여 인프라를 원하는 상태로 만든다.
출력 변수 저장: Terraform 출력 변수는 State에 저장된다. 이를 통해 다른 Terraform 구성에서 이 값을 참조할 수 있다.
팀과의 협업: 원격 상태 저장소를 사용하면 여러 팀원이 동일한 인프라에 대해 Terraform을 실행할 때 State의 일관성과 동기화를 유지할 수 있다.
리소스 의존성 관리: State를 통해 Terraform은 리소스 간의 의존성을 파악하고, 리소스를 올바른 순서로 생성, 수정, 삭제한다.
드리프트 감지: State 파일을 사용하여 드리프트를 감지하고, 필요한 경우 Terraform을 통해 원하는 상태로 되돌릴 수 있다.
요약하면, State는 Terraform에서 중심적인 역할을 하는 요소로, 실제 클라우드 인프라와 Terraform 구성 간의 차이점과 연관성을 추적하고 관리하는 데 필요하다.

이런 중요한 State에는 몇 가지 조건이 필요하다.
정확성: State 파일은 항상 현재 인프라의 정확한 표현이어야 한다. State와 실제 리소스 간의 불일치는 문제를 일으킬 수 있으므로, State는 최신 상태를 유지해야 한다.
접근 제한: State 파일은 민감한 정보(예: 비밀번호, 엑세스 키 등)를 포함할 수 있기 때문에 적절한 접근 제어가 필요하며, 필요한 팀원만 접근할 수 있도록 해야 한다.
동기화: 여러 개발자나 시스템 관리자가 동일한 인프라에 작업할 경우 원격 상태 저장소를 사용하여 State를 중앙에서 관리하고, 동시에 발생하는 변경을 피하도록 구성해야 한다.
백업: State 파일은 중요한 데이터를 포함하므로, 정기적으로 백업해야 하고, 이를 통해 잘못된 변경이나 손상된 경우 이전 상태로 복구할 수 있다.
버전 관리: 원격 상태 저장소에서는 State 파일의 버전 관리 기능을 활용할 수 있다. AWS S3의 경우 Object Versioning을 활성화하여 State의 변경 이력을 추적할 수 있다.
잠금 Mechanism: Terraform은 리소스 생성 및 수정 작업 중에 state 파일을 잠글 수 있는 기능을 제공한다. 이렇게 하면 동시에 여러 사용자가 같은 state 파일을 변경하는 것을 방지할 수 있다.
암호화: 위의 접근 제한과 마찬가지로 중요 정보를 담고 있을 수 있기 때문에 저장소에 저장되는 상태 파일은 암호화되어야 한다. AWS S3를 원격 상태 저장소로 사용하는 경우, S3의 서버 측 암호화를 활용하여 데이터를 암호화할 수 있다.

1.2 State 동기화

테라폼 구성 파일은 기존 State와 구성을 비교해 실행 계획에서 생성, 수정, 삭제 여부를 결정한다.

https://kschoi728.tistory.com/135

Terraform 액션에 따라 State에 어떤 동작이 발생하는지 유형 별로 확인해보도록 하겠다.

테라폼 구성과 State 흐름 : Plan 과 Apply 중 각 리소스에 발생할 수 있는 네 가지 사항, 아래 실행 계획 출력 기호와 의미

기호의미
+Create
Destroy
-/+Replace
~Updated in-place

Replace 동작은 기본값을 삭제 후 생성하지만 lifecycle의 create_before_destroy 옵션을 통해 생성 후 삭제 설정 가능

1.3 워크스페이스

Terraform의 워크스페이스는 Terraform 구성의 여러 버전 또는 세트를 관리하도록 도와주는 기능이다. 워크스페이스를 사용하면 동일한 구성으로 여러 다른 환경(예: 개발, 스테이징, 프로덕션 등)을 관리할 수 있다.
워크스페이스의 주요 특징 및 사용 사례는 몇 가지가 있다.
환경 분리: 워크스페이스는 다양한 환경을 분리된 상태로 유지하는 데 도움이 된다. 예를 들어 dev, staging, prod와 같은 워크스페이스를 만들어 환경별로 다른 변수나 리소스를 적용할 수 있다.
독립적인 State 관리: 각 워크스페이스는 자체적인 Terraform state 파일을 갖게 된다. 따라서, 한 환경에서의 변경이 다른 환경의 state에 영향을 주지 않는다.
변수 재사용: 워크스페이스별로 변수를 재정의하면서 동일한 Terraform 코드를 재사용할 수 있다. 예를 들어, 각 환경에 대한 다른 서브넷이나 크기의 VM을 지정할 수 있다.
워크스페이스 명령어는 몇 가지가 있다.
terraform workspace new [workspace-name]: 새로운 워크스페이스를 생성한다.
terraform workspace select [workspace-name]: 특정 워크스페이스로 전환한다.
terraform workspace list: 사용 가능한 모든 워크스페이스를 나열한다.
terraform workspace show: 현재 선택된 워크스페이스를 표시한다.

간단한 실습으로 State와 워크스페이스를 확인해본다.

# workspace 확인
terraform workspace list
* default

# 간단 EC2 배포 main.tf
resource "aws_instance" "mysrv1" {
  ami           = "ami-0ea4d4b8dc1e46212"
  instance_type = "t2.micro"
  tags = {
    Name = "t101-week4"
  }
}

# Terraform 배포 후 state 확인
terraform init && terraform apply -auto-approve
terraform state list
aws_instance.mysrv1

# Public/Private IP 확인
cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
3.39.231.x
cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.private_ip'
172.31.39.105
cat terraform.tfstate | jq -r '.resources[0].instances[0].private' | base64 -d | jq
{
  "e2bfb730-ecaa-11e6-8f88-34363bc7c4c0": {
    "create": 600000000000,
    "delete": 1200000000000,
    "update": 600000000000
  },
  "schema_version": "1"
}

새로운 워크스페이스를 만들어서 apply을 진행해봤다. workspace가 새로 생기면 state도 새로 생기고 개별로 구성되는 것을 확인할 수 있다.

# 새 워크스페이스 생성
terraform workspace new mywork1
terraform workspace list
  default
* mywork1
terraform workspace show
mywork1

tree terraform.tfstate.d
terraform.tfstate.d
└── mywork1
2 directories, 0 files

# plan 할 경우 기존 워크스페이스라면 create가 표시되지 않아야 하나 새로운 워크스페이스(mywork1)이 선택되어 있어서 create로 표시
terraform plan

# apply을 하면 plan 내용처럼 배포가 진행 된다.
terraform apply -auto-approve

# 워크스페이스 확인
terraform workspace list
  default
* mywork1

# State 확인하면 기존 배포한 것과 새로 배포한 것의 Public IP가 차이가 난다.
cat terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
3.39.231.x
cat terraform.tfstate.d/mywork1/terraform.tfstate | jq -r '.resources[0].instances[0].attributes.public_ip'
15.164.x.x

# 실습 리소스 삭제
terraform workspace select default
terraform destroy -auto-approve
terraform workspace select mywork1
terraform destroy -auto-approve

워크스페이스의 장단점은 아래와 같다.

장점
하나의 루트 모듈에서 다른 환경을 위한 리소스를 동일한 테라폼 구성으로 프로비저닝하고 관리
기존 프로비저닝된 환경에 영향을 주지 않고 변경 사항 실험 가능
깃의 브랜치 전략처럼 동일한 구성에서 서로 다른 리소스 결과 관리

단점
State가 동일한 저장소에 저장되어 State 접근 권한 관리가 불가능(어려움)
모든 환경이 동일한 리소스를 요구하지 않을 수 있으므로 테라폼 구성에 분기 처리가 다수 발생 가능
프로비저닝 대상에 대한 인증 요소를 완벽히 분리하기 어려움-> 가잔 큰 단점은 완벽한 격리가 불가능
-> 해결방안 1. 해결하기 위해 루트 모듈을 별도로 구성하는 디렉터리 기반의 레이아웃 사용
-> 새결방안 2. Terraform Cloud 환경의 워크스페이스를 활용

2. 모듈

Terraform 모듈은 Terraform 코드의 집합이다. 이를 사용하면 코드를 재사용하고, 구조화하며, 공유할 수 있다. 기본적으로 모듈은 Terraform의 스크립트나 구성을 논리적 단위로 캡슐화하는 방법이다. 모듈을 사용하여 공통 구성 요소를 쉽게 재사용하고 관리할 수 있다.
Terraform 모듈의 주요 특징 및 용도는 아래와 같다.
코드 재사용: 모듈은 반복되는 코드 패턴을 줄이기 위해 공통 리소스나 구성을 캡슐화한다. 예를 들어, 여러 프로젝트나 환경에서 동일한 VPC, 보안 그룹, EC2 인스턴스 설정을 사용해야 할 때 모듈로 정의하면 이를 쉽게 재사용할 수 있다.
논리적 구조: 모듈을 사용하여 복잡한 Terraform 코드를 관리하기 쉬운 논리적 단위로 분리할 수 있다. 이렇게 하면 코드의 가독성과 유지 관리성이 향상된다.
변수 및 출력: 모듈은 입력 변수를 통해 매개 변수화되고, 출력 변수를 통해 다른 Terraform 구성 또는 모듈에 데이터를 반환할 수 있다.
버전 관리: Terraform 모듈은 Git과 같은 소스 코드 관리 시스템에 저장되어 버전 관리될 수 있다. 또한, Terraform Registry나 다른 버전화된 저장소를 사용하여 공유 및 배포될 수 있다.
안정성과 표준화: 모듈을 사용하면 안정적이고 표준화된 인프라 구성 요소를 제공할 수 있다. 특히 큰 팀이나 여러 프로젝트에서 일관된 인프라 구성을 유지하기 위해 유용하다.

module "vpc" {
  source  = "aws/vpc/aws"
  version = "2.0.0"
  ...
}

간단한 예제로 위의 예제에서는 AWS VPC 모듈을 사용하여 VPC를 생성하며, 해당 모듈은 공식 Terraform Registry에서 가져온다
모듈을 사용하면 Terraform 코드의 재사용성과 유지 관리성을 크게 향상시킬 수 있다. 여러 프로젝트나 환경 간에 일관된 인프라 코드를 유지하기 위한 기본 도구로 간주된다.

간단한 모듈화 테스트를 해보려고 한다. VPC을 만드는 모듈을 사용해볼 예정이다.
먼저 my_vpc_module라는 디렉터리를 만들고 모듈 코드를 작성한다.

#my_vpc_module/main.tf
resource "aws_vpc" "ybs-vpc" {
  cidr_block = var.cidr_block
  tags = {
    Name = var.vpc_name
  }
}

output "vpc_id" {
  value = aws_vpc.ybs-vpc.id
}

variable "cidr_block" {
  description = "CIDR block for the VPC"
  type        = string
}

variable "vpc_name" {
  description = "Name tag for the VPC"
  type        = string
}

루트 디렉토리에 main.tf을 만들어서 vpc 모듈을 호출한다.

module "my_vpc" {
  source    = "./my_vpc_module"
  cidr_block = "10.0.0.0/16"
  vpc_name   = "ybs-test-vpc"
}

output "created_vpc_id" {
  value = module.my_vpc.vpc_id
}

해당 모듈을 호출하는 테스트를 진행해본다. 잘 생성되는 것과 더불어 vpc 이름까지 잘 적용된 것을 확인할 수 있다.

#terraform init 및 Plan/Apply 실행
terraform init && terraform apply -auto-approve

#terraform state 확인
terraform state list                          
module.my_vpc.aws_vpc.ybs-vpc

조금 더 심화 내용으로 모듈을 2개 사용하는 실습을 진행할 예정이다. VPC 모듈과 EC2 모듈을 각각 작성하고 두 모듈을 호출해서 리소스를 생성할 예정이다.

먼저 vpc 폴더를 만들고 main.tf로 vpc 모듈을 작성한다.

#vpc/main.tf
resource "aws_vpc" "ybs-vpc" {
  cidr_block = var.cidr_block
  tags = {
    Name = var.vpc_name
  }
}

resource "aws_subnet" "ybs-subnet" {
  vpc_id     = aws_vpc.ybs-vpc.id
  cidr_block = var.subnet_cidr_block
  availability_zone = var.availability_zone
  tags = {
    Name = "${var.vpc_name}-subnet"
  }
}

output "vpc_id" {
  value = aws_vpc.ybs-vpc.id
}

output "subnet_id" {
  value = aws_subnet.ybs-subnet.id
}

variable "cidr_block" {
  description = "CIDR block for the VPC"
 
}

variable "vpc_name" {
  description = "VPC Name"
  type = string
 
}
variable "subnet_cidr_block" {
  description = "CIDR block for the Subnet"
 
}

variable "availability_zone" {
  description = "The availability zone where the subnet will be created"
  type = string
}

EC2도 마찬가지로 폴더를 만들고 모듈을 작성한다.

#ec2/main.tf
resource "aws_instance" "ybs-ec2" {
  ami           = var.ami_id
  instance_type = var.instance_type
  subnet_id     = var.subnet_id
  tags = {
    Name = var.instance_name
  }
}

output "instance_id" {
  value = aws_instance.ybs-ec2.id
}

variable "ami_id" {
  description = "AMI ID for the EC2 instance"
  type        = string
}

variable "instance_type" {
  description = "Instance type for the EC2 instance"
  type        = string
}

variable "subnet_id" {
  description = "Subnet ID where the EC2 instance will be launched"
  type        = string
}

variable "instance_name" {
  description = "Name tag for the EC2 instance"
  type        = string
}

두 모듈을 호출하는 main.tf를 루트 디렉토리에 만든다.

#main.tf
module "my_vpc" {
  source    = "./vpc"
  cidr_block = "10.0.0.0/16"
  vpc_name   = "MyVPC"
}

module "my_ec2" {
  source         = "./ec2"
  ami_id         = "ami-0123456789abcdef0" # 예시 AMI ID
  instance_type  = "t2.micro"
  subnet_id      = module.my_vpc.vpc_id # VPC 모듈에서 생성된 VPC의 ID를 사용
  instance_name  = "MyInstance"
}

output "created_vpc_id" {
  value = module.my_vpc.vpc_id
}

output "created_instance_id" {
  value = module.my_ec2.instance_id
}

모듈을 실행시키면 VPC와 EC2가 제대로 만들어지는 걸 확인할 수 있다.

#terraform init 및 Plan/Apply 실행
terraform init && terraform apply -auto-approve

#terraform state 확인
terraform state list 
module.my_ec2.aws_instance.ybs-ec2
module.my_vpc.aws_subnet.ybs-subnet
module.my_vpc.aws_vpc.ybs-vpc      

3.협업

인프라 규모가 커지고 관리 팀원이 늘어날 수록 구성 코드 관리가 필요하다. → 서로 작성 코드 점검 및 협업 환경 구성

구성 요소 : 코드를 다수의 작업자가 유지 보수 할 수 있도록 돕는 VCS Version Control System + 테라폼 State를 중앙화하는 중앙 저장소

https://kschoi728.tistory.com/139

유형 1 : VCS, 중앙 저장소 없음

  • 동일한 대상을 관리하는 여러 작업자는 동일한 프로비저닝을  위해 각자 자신이 작성한 코드를 수동으로 공유가 필요
  • 작업자의 수가 늘어날수록 코드 동기화는 어려워지고, 각 작업자가 작성한 코드를 병합하기도 어렵다 → VCS 도구 도입 고민 시점

유형 2 : VCS(SVN, Git), 중앙 저장소 도입

  • 형성관리 도구를 통해 여러 작업자가 동일한 테라폼 코드를 공유해 구성 작업
    • 변경 이력 관리 및 이전 버전으로 롤백 가능
  • 공유파일은 테라폼 구성파일과 State ← 테라폼 프로비저닝의 결과물로 데이터 저장소와 같음
  • 작업자가 서로 다른 프로비저닝한 State 결과를 공유를 위해서 백엔드(공유 저장소) 설정을 제공

유형 3 : VCS(Github), 중앙 저장소 도입

  • 테라폼 코드 형상관리를 위한 중앙 저장소 + State 백엔드 → 작업자는 개별적으로 프로비저닝을 테스트하고 완성된 코드를 공유
    • State 는 중앙 관리되어, 작업자가 프로비저닝을 수행하면 원격 State의 상태를 확인하고 프로비저닝 수행

4. 정리

협업은 아직 실습까진 진행하지 않았고 기초적인 이론만 살펴보았다. IaC는 궁극적으로 협업을 하기 위한 도구에 지나지 않는다고 생각한다. 그 협업을 하기 위해 State 관리가 필요하고 모듈을 세분화해서 관리하는 게 유리하기 때문에 State와 모듈에 대해 자세히 알아볼 필요가 있는 것 같다.
다음 시간에는 협업에 대한 실습을 상세히 진행해보면 좋을 거 같다.

Terraform Study #3(조건식, 함수, 프로바이더, 프로비저너)

오늘은 Terraform 조건식을 이용해서 리소스를 배포하는 것과 프로비저너, 함수 그리고 moved 블록 등을 사용해서 aws 리소스를 배포해보는 실습을 진행할 예정이다.

1. 조건식 (도전과제 #1)

Terraform에서의 조건식은 주로 boolean 값, 비교 연산자, 논리 연산자를 사용하여 표현된다. 조건식을 사용하여 변수나 리소스의 속성 값에 따라 다른 결과를 도출할 수 있다.
일반적으로 비교, 논리 연산자를 사용해 조건을 확인하고 조건식은 ? 기호를 기준으로 왼쪽은 조건, 오른쪽은 : 기준으로 왼쪽이 조건에 대해 true가 반환되는 경우이고 오른쪽이 false가 반환되는 경우에 대한 값이다.
예를 들어 아래와 같은 연산조건식이 있다면, var.is_produntion 변수가 참일 경우 t2.large를 사용하고 false일 경우 t2.micro을 사용하게 된다.

locals {
  instance_size = var.is_production ? "t2.large" : "t2.micro"
}

조건식에 대해서 간단한 예제 겸 도전과제를 진행해보도록 한다.
아래 코드는 create_instance라는 변수를 입력 받아서 true일 경우 EC2를 생성하고 false 경우 생성하지 않는 조건식을 담고 있다.

variable "create_instance" {
  type        = bool
}

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

resource "aws_instance" "ybs-ec2-bool" {
  count = var.create_instance ? 1 : 0

  ami           = "ami-084e92d3e117f7692"
  instance_type = "t2.micro"

  tags = {
    Name = "ybs-ec2-bool"
  }
}

terraform apply 후 false을 입력하면 아래와 같이 아무런 변화가 발생하지 않는 것을 볼 수 있다.

반대로 true을 입력하면 EC2가 정상적으로 실행되는 것을 볼 수 있다.

이렇게 조건식을 간단하게 사용할 수 있다. 추후 Prd, Stg, Dev 등 환경을 나눠서 배포해야 하는 경우거나 Application 종류에 따라 다른 태그값 등을 입력하고 싶을 때 조건식을 사용하는 게 큰 도움이 될 거 같다.

2. 함수(도전과제 #2)

Terraform은 프로그래밍 언어적인 특성을 갖고 있어서 값의 유형을 변경하거나 조합할 수 있는 내장 함수를 제공하여 코드를 작성하는데 도움을 준다.
이 함수들은 문자열 처리, 숫자 연산과 데이터 구조의 변환 그리고 조건 판단 등 다양한 작업을 수행하게 된다.
함수에는 몇 가지가 있는데 아래와 같은 종류가 있다.

문자열 함수:
join(separator, list): 리스트의 요소를 주어진 구분자로 연결
split(separator, string): 문자열을 구분자로 분리하여 리스트를 생성
lower(string): 문자열을 소문자로 변환

숫자 함수:
max(a, b, …): 주어진 숫자 중 가장 큰 값을 반환
min(a, b, …): 주어진 숫자 중 가장 작은 값을 반환
abs(number): 주어진 숫자의 절대값을 반환

리스트와 맵 함수:
length(list): 리스트나 맵의 길이를 반환
element(list, index): 리스트에서 특정 인덱스의 요소를 반환
merge(map1, map2, …): 두 개 이상의 맵을 병합

조건 함수:
coalesce(value1, value2, …): 주어진 값 중 첫 번째 null이 아닌 값을 반환
count(list): 주어진 리스트의 길이를 반환
contains(list, value): 리스트가 특정 값을 포함하는지 여부를 반환

타입 변환 함수:
tostring(value): 주어진 값을 문자열로 변환
tonumber(value): 주어진 값을 숫자로 변환
tolist(value): 주어진 값을 리스트로 변환

기타:
file(path): 파일의 내용을 문자열로 읽음
templatefile(path, vars): 템플릿 파일을 읽어와 변수를 주입
timestamp(): 현재 시간을 ISO 8601 형식의 문자열로 반환

위 내용을 기반으로 간단한 예제 겸 도전과제 2번을 진행해봤다.
샘플은 도전과제 #1에서 사용한 내용을 기반으로 하였고 Enviroment 값을 추가로 입력 받아 Name Tag와 merge하여 EC2의 Tag을 정의하는 내용으로 진행했다.

variable "create_instance" {
  type        = bool
}

variable "app_env" {
  type = string

  validation {
    condition     = contains(["prd", "dev", "stg"], var.app_env)
    error_message = "prd, dev, stg 중 하나만 입력하세요."

  }
}


locals {
  common_tags = {
    "Name" = "ybs-ec2-fn"
  }
  env_tags = {
    "Environment" = var.app_env
  }


  final_tags = merge(local.common_tags, local.env_tags)
}

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

resource "aws_instance" "ybs-ec2-fn" {
  count = var.create_instance ? 1 : 0

  ami           = "ami-084e92d3e117f7692"
  instance_type = "t2.micro"

  tags = local.final_tags
}

terraform apply을 수행하면 app_env을 물어보고 이후에 생성 가/부를 물어본다. 각각 prd, true을 입력하면 아래와 같이 태그가 입력되면서 EC2가 생성되는 것을 확인할 수 있다.

3. 프로비저너(도전과제 #3)

Terraform의 프로비저너(provisioners)는 리소스를 생성, 수정, 또는 제거한 후에 로컬 머신이나 원격 머신에서 특정 작업을 실행할 수 있도록 해준다. 일반적으로 프로비저너는 구성 관리 도구, 초기 스크립트 실행, 또는 다른 로직을 적용하기 위해 사용된다.
예를 들어서 EC2를 배포 후 특정 패키지를 설치하거나 파일을 생성해야 하는 경우에 프로비저너를 사용하여 진행하게 된다.

프로비저너를 사용하면 저런 편리함을 가질 수 있지만 아래와 같은 주의해야 하는 부분도 있다.
프로비저너는 오류 복구가 어려울 수 있으며, 선언적이지 않은 로직을 도입할 수 있다.
가능하면 초기 구성은 구성 관리 도구나 클라우드 초기화 스크립트 (예: cloud-init) 등을 통해 처리하는 것이 더 좋다.

그럼 프로비저너를 사용하는 간단한 예제 겸 도전과제 3번을 진행하도록 한다.
EC2 인스턴스를 생성하고 file 프로비저너를 사용해서 로컬에 있는 test.sh 파일을 업로드 특정 위치에 업로드 하고 remote-exec 프로비저너를 통해 해당 업로드한 test.sh을 실행하는 로직으로 구성되어 있다.
apply을 실행하면 생성하고 접속 후 file의 내용을 remote-exec을 통해 잘 불러오는 것을 확인할 수 있다.

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

resource "aws_key_pair" "pem_key" {
  key_name   = "testpem"
  public_key = file("~/.ssh/id_rsa.pub")
}

resource "aws_security_group" "allow_ssh" {
  name        = "inssh"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "example" {
  ami           = "ami-084e92d3e117f7692"
  instance_type = "t2.micro"

  key_name          = aws_key_pair.pem_key.key_name
  security_groups   = [aws_security_group.allow_ssh.name]
  associate_public_ip_address = true

#로컬 파일에 있는 sh 파일을 원격 업로드
  provisioner "file" {
    source      = "./test.sh"
    destination = "/tmp/test.sh"
  }

#업로드한 sh 파일을 실행(chmod로 실행 권한 부여 진행)
    provisioner "remote-exec" {
    inline = [
      "chmod +x /tmp/test.sh",
      "/tmp/test.sh"
    ]
  }

  connection {
    type        = "ssh"
    host        = self.public_ip
    user        = "ec2-user"
    private_key = file("~/.ssh/id_rsa")
  }

  tags = {
    Name = "ybs-remote-test"
  }
}

생성과 실행까지 잘 되었지만 테스트 과정에서 프로비저너를 몇 번 수정해야 하는 경우가 있었다. sh 파일 경로를 잘못 입력하거나 chmod로 권한을 부여하지 않아 권한 오류가 뜨는 등 몇 번의 수정이 필요했다.
그때마다 수정한 내용이 반영되지 않아 destroy을 진행 후 다시 apply을 해야했는데 이는 프로비저너의 경우 테라폼의 상태 파일과 동기화되지 않기 때문이다. 따라서 수정을 하더라도 상태 파일에서는 수정했다고 인지하지 않아 변경점이 적용되지 않는다. 이는 실제 운영단계에서는 참고해서 진행해야 하는 부분이라고 생각한다.

4. null_resource와 terraform_data

테라폼 1.4 버전이 릴리즈되면서 기존 null_resource 리소스를 대체하는 terraform_data 리소스가 추가되었다.
null_resource는 Terraform에서 특별한 동작을 하지 않는 리소스이다. 이는 다른 리소스가 생성, 수정, 또는 삭제될 때마다 동작을 트리거하는 등, 복잡한 조건이나 의존성을 관리하는 데 주로 사용된다. null_resource 자체는 아무런 리소스를 생성하지 않지만, provisioner나 triggers와 같은 추가 설정을 통해 다른 리소스에 영향을 줄 수 있다.
예를 들어, null_resource를 사용하여 앞서 진행한 프로비저너를 통해 AWS EC2 인스턴스가 생성될 때마다 로컬 스크립트를 실행하도록 할 수 있다.

간단한 예제는 아래와 같다.
아래 예제를 실행하면 ec2 인스턴스가 생성 될 때마다 local-exec 프로비저너를 실행하게 된다.

resource "aws_instance" "example" {
  ami           = "ami-xxxxxxxxxx"
  instance_type = "t2.micro"
}

resource "null_resource" "example" {
  triggers = {
    instance_id = aws_instance.example.id
  }

  provisioner "local-exec" {
    command = "echo ${aws_instance.example.id} >> instances.txt"
  }
}

terraform_data 리소스는 null_resource와 마찬가지로 아무것도 수행하지 않지만 null_resource와의 차이점은 별도의 프로바이더 구성 없이 Terraform 자체에 포함 된 기본 수명주기 관리자가 제공된다는 것이 특징이다.

5. moved 블록(도전과제 #5)

Terraform의 state에 기록되는 리소스 주소의 이름이 변경되면 기존 리소스는 삭제되고 새로운 리소스가 생성된다. 하지만 Terraform 리소스를 선언하다 보면 이름을 변경해야 하는 상황이 발생하게 된다. 이때 이름만 변경하고 리소스는 삭제 후 재생성되지 않게 유지하기 위한 방법으로 moved 블록을 Terraform 1.1 버전부터 제공한다.
moved 블록 제공 이전에는 state을 직접 편집하는 terraform state mv 명령을 사용하여 state를 편집해야 하는 부담이 있었지만 moved 블록을 사용하게 되면 리소스 영향 없이 쉽게 리소스 이름을 변경할 수 있다.

간단한 실습 겸 도전과제를 진행해보도록 하겠다.
앞서 도전과제 #1에서 사용한 코드로 우선 EC2를 생성하였다.

variable "create_instance" {
  type        = bool
}

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

resource "aws_instance" "ybs-ec2-bool" {
  count = var.create_instance ? 1 : 0

  ami           = "ami-084e92d3e117f7692"
  instance_type = "t2.micro"

  tags = {
    Name = "ybs-ec2-bool"
  }
}

이후 moved 블록을 사용하여 리소스 이름만 변경해보았다.

variable "create_instance" {
  type        = bool
}

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

#moved 블록으로 이름 전환
  moved {
    from = aws_instance.ybs-ec2-bool
    to   = aws_instance.ybs-ec2-bool-new
  }

#리소스 이름도 같이 변경
resource "aws_instance" "ybs-ec2-bool-new" {
  count = var.create_instance ? 1 : 0

  ami           = "ami-084e92d3e117f7692"
  instance_type = "t2.micro"

  tags = {
    Name = "ybs-ec2-bool"
  }
}

apply을 실행하면 added, changed, destroyed 모두 0으로 표시되고 Resource 이름이 -new로 잘 바뀐 것을 확인할 수 있다.

6. 프로바이더

프로바이더는 Terraform이 인프라스트럭처 서비스, 플랫폼 서비스, 기타 API에 대한 CRUD(Create, Read, Update, Delete) 작업을 수행할 수 있도록 API와의 상호작용을 담당한다. 예를 들어, AWS, Azure, GCP와 같은 클라우드 제공 업체에 대한 프로바이더가 있으며, GitHub, GitLab, Datadog 등 다른 서비스에 대한 프로바이더도 있다.

프로바이더는 특정 서비스와의 상호작용을 추상화하므로, 사용자는 서비스의 구체적인 API를 직접 다루지 않아도 된다. 대신, Terraform의 HCL(HashiCorp Configuration Language)을 사용하여 서비스 리소스를 정의하고 관리한다.

https://www.hashicorp.com/blog/making-terraform-provider-development-more-accessible

프로바이더를 사용하면 서로 다른 리전에 동일한 종류의 리소스를 배포하는 것도 가능하다.
다만, 주의사항이 있다.
Multi-region is hard – active-active 멀티 리전 서비스를 위해서 ‘지역간 지연 시간, 고유 ID, 최종 일관성’ 등 여러가지 고려사항이 많아서 프로덕션 수준의 멀티 리전은 어렵다.
Use aliases sparingly alias를 빈번하게 사용하지 말자 – 별칭을 사용하여 두 리전에 배포하는 단일 테라폼 모듈은 한 리전이 다운 시, plan과 apply 시도가 실패하게 된다.

주의사항을 알아봤으니 간단하게 실습 겸 도전과제로 서울과 버지니아 북부 리전에 S3를 배포하는 실습을 진행하보도록 한다.

2개의 프로바이더를 선언하고 리소스를 생성할 때 provider를 통해 각각의 리전을 호출해서 생성하는 코드를 작성했다.

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

provider "aws" {
  region = "us-east-1"
  alias  = "virginia"
}

# 서울 리전에 S3 버킷 생성
resource "aws_s3_bucket" "bucket_seoul" {
  provider = aws.seoul
  bucket   = "ybs-t102-prv-seoul"

  tags = {
    Name        = "ybs-t102-prv-seoul"
  }
}

# 버지니아북부 리전에 S3 버킷 생성
resource "aws_s3_bucket" "bucket_virginia" {
  provider = aws.virginia
  bucket   = "ybs-t102-prv-virginia"

  tags = {
    Name        = "ybs-t102-prv-virginia"
  }
}

프로바이더를 사용해서 별 무리 없이 각각의 리전에 S3 버킷을 배포할 수 있다.

7. 정리

조건식과 함수를 통해 리소스를 조금 더 환경에 맞춰 배포하는 방법을 익힐 수 있었다.
프로바이더를 활용하여 리전에 맞춰 배포하는 것도 흥미로웠다. 오늘 배운 내용들을 추후에 대규모 인프라를 배포할 때 참고해서 진행하면 좋을 거 같다.

Terraform Study #2

오늘은 데이터소스와 변수 그리고 반복문에 대해 진행할 예정이다.

1. Data Source(도전과제 #1 포함)

Terraform에서 Data Source는 구성된 제공자(provider)에서 읽을 수 있는 데이터를 나타낸다. Data Source는 이미 존재하는 리소스에 대한 정보를 조회하거나 외부와 통신하여 정보를 얻을 수도 있다. 이 정보는 다른 리소스를 새엉하거나 구성할 때 사용할 수 있다.
예를 들어, AWS에서 실행 중인 EC2 인스턴스의 정보를 얻으려면 aws_instance Data Source을 사용해서 얻을 수 있다.

Data Source는 data 블록을 사용한다. 미리 배포한 테스트 목적의 EC2에 대한 Public IP을 출력하는 내용을 진행해봤다.

data "aws_instance" "example" {
  instance_id = "i-12345678"
}

output "instance_public_ip" {
  value = data.aws_instance.example.public_ip
}

#초기화 및 plan 진행
terraform init
terraform plan

EC2 ID에 맞는 Public IP을 호출하는 것을 확인할 수 있다.

간단하게 Data Source을 사용해봤다면 도전과제를 진행해보도록 한다.
해당 내용을 진행하기 위한 과정을 간단하게 정리하면 다음과 같다.
1. AWS 서울 리전에서 사용 가능한 가용 영역을 Data Source을 통해 가져온다.
2. 가져온 가용 영역을 사용하여 VPC 내 Subnet을 생성

위 내용을 아래 코드로 작성하였다.
사용 가능한 가용 영역 목록을 Data Source을 통해 불러오고 VPC IP CIdr을 10.0.0./16으로 선택한 후 서브넷을 각각 /24로 생성하는 과정이다.

data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_vpc" "ybs-c1-vpc" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "ybs-c1-vpc"
  }
}

resource "aws_subnet" "ybs-c1-vpc-subnet" {
  count = length(data.aws_availability_zones.available.names)

  cidr_block = "10.0.${count.index + 1}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]
  vpc_id = aws_vpc.ybs-c1-vpc.id

  tags = {
    Name = "ybs-c1-vpc-subnet-${count.index}"
  }
}

# 테라폼 초기화/plan/실행
terraform init && terraform plan && terraform apply -auto-approve

VPC와 서브넷 4개가 잘 생성된 것을 볼 수 있다.

콘솔에서도 확인했을 때 가용 영역이 a~d까지 잘 생성된 것으로 보인다.

graph 생성을 통해 본 로직은 아래와 같다.

2. 입력 변수 Variable

Terraform에서 입력 변수(Variable)는 Terraform 구성에 값을 주입하는 방법을 제공한다. 이는 동적인 구성을 가능하게 하고 동일한 코드를 다양한 환경 또는 조건에 재사용할 수 있게 해준다. 변수는 var 식별자를 통해 참조할 수 있고 구성 내에서 다양한 리소스의 출력에 사용 된다.

변수를 선언하기 위해 variable 블록을 사용한다. 이 블록에는 변수의 이름, 타입과 기본값 그리고 설명 등을 포함한다.

간단하게 작성을 하면 아래와 같다. region이라는 변수를 선언하고 provider 블록에서 var.region을 통해 region 변수 값을 호출할 수 있다.

variable "region" {
  description = "The AWS region to deploy into"
  default     = "ap-northeast-2"
  type        = string
}

provider "aws" {
  region = var.region
}

아무 변수 이름이나 사용할 수 있는 것은 아니고 Terraform에서 예약 변수로 사용하고 있는 변수들은 사용할 수 없다.(source, version, providers, count, for_each, lifecycle, depends_on, locals 등)

변수 타입은 아래와 같다.

String : 텍스트 값
Number : 숫자
Bool : true/false
List([type]) : 같은 유형의 여러 값
Set([type]) : 유일한 값들의 집합
Map([type]) : 키-값 쌍의 집합
Object({ key: type, … }) : 여러 필드를 가진 객체
Tuple([type, …]) : 여러 유형을 가진 리스트

3. VPC/SG/EC2 배포(Data Source+변수 사용, 도전과제 #2, #3, #4)

Data Source와 변수를 사용해서 VPC/SG/EC2를 배포하는 실습을 진행해봤다.

우선 vpc.tf을 통해 vpc와 Subnet, Route Table 그리고 IGW를 생성했다.
가용 영역의 경우 az1, az3라는 변수를 통해 서브넷 생성 시 변수 호출할 수 있게 작성했다.
IGW로 라우팅을 할 수 있도록 Route Table을 만들고 2개의 Subnet에 연결해준 뒤 0.0.0.0/0에 대해 IGW로 라우팅할 수 있는 Route을 등록해줬다.

variable "region" {
  description = "The AWS region to deploy into"
  default     = "ap-northeast-2"
  type        = string
}

variable "az1" {
  description = "The availability zone a"
  default     = "ap-northeast-2a"
  type        = string
}

variable "az3" {
  description = "The availability zone c"
  default     = "ap-northeast-2c"
  type        = string
}

provider "aws" {
  region  = var.region
}

resource "aws_vpc" "ybs-vpc" {
  cidr_block       = "10.10.0.0/16"

  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "ybs-vpc"
  }
}

resource "aws_subnet" "ybs-subnet1" {
  vpc_id     = aws_vpc.ybs-vpc.id
  cidr_block = "10.10.1.0/24"

  availability_zone = var.az1

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

resource "aws_subnet" "ybs-subnet2" {
  vpc_id     = aws_vpc.ybs-vpc.id
  cidr_block = "10.10.2.0/24"

  availability_zone = var.az3

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

resource "aws_internet_gateway" "ybs-igw" {
  vpc_id = aws_vpc.ybs-vpc.id

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

resource "aws_route_table" "ybs-rt1" {
  vpc_id = aws_vpc.ybs-vpc.id

  tags = {
    Name = "ybs-rt1"
  }
}

resource "aws_route_table_association" "ybs-rt1-asso1" {
  subnet_id      = aws_subnet.ybs-subnet1.id
  route_table_id = aws_route_table.ybs-rt1.id
}

resource "aws_route_table_association" "ybs-rt1-asso2" {
  subnet_id      = aws_subnet.ybs-subnet2.id
  route_table_id = aws_route_table.ybs-rt1.id
}

resource "aws_route" "ybs-rt1-drt" {
  route_table_id         = aws_route_table.ybs-rt1.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.ybs-igw.id
}

output "aws_vpc_id" {
  value = aws_vpc.ybs-vpc.id
}

# 테라폼 초기화/plan/실행
terraform init && terraform plan && terraform apply -auto-approve

# state list 확인
terraform state list
aws_internet_gateway.ybs-igw
aws_route.ybs-rt1-drt
aws_route_table.ybs-rt1
aws_route_table_association.ybs-rt1-asso1
aws_route_table_association.ybs-rt1-asso2
aws_subnet.ybs-subnet1
aws_subnet.ybs-subnet2
aws_vpc.ybs-vpc

보안그룹은 특별한 내용 없이 지난 주 실습 내용을 토대로 Egress Any와 Ingress 80 Port에 대한 허용 설정을 진행해줬다. 여기서도 Ingress 80 Port와 Any Cidr Block는 변수로 설정해서 추후 SG가 추가될 때 참조할 수 있도록 했다.

variable "http" {
  description = "The HTTP port"
  default     = 80
  type        = number
}

variable "anycidr" {
  description = "The any cidr block"
  default     = ["0.0.0.0/0"]
  type        = list
}

resource "aws_security_group" "ybs-sg1" {
  vpc_id      = aws_vpc.ybs-vpc.id
  name        = "YBS SG 1"
  description = "YBS Study SG 1"
}

resource "aws_security_group_rule" "ybs-in-http-rule" {
  type              = "ingress"
  from_port         = var.http
  to_port           = var.http
  protocol          = "tcp"
  cidr_blocks       = var.anycidr
  security_group_id = aws_security_group.ybs-sg1.id
}

resource "aws_security_group_rule" "ybs-out-any-rule" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = var.anycidr
  security_group_id = aws_security_group.ybs-sg1.id
}

# 테라폼 초기화/plan/실행
terraform init && terraform plan && terraform apply -auto-approve

# state list 확인
terraform state list
aws_internet_gateway.ybs-igw
aws_route.ybs-rt1-drt
aws_route_table.ybs-rt1
aws_route_table_association.ybs-rt1-asso1
aws_route_table_association.ybs-rt1-asso2
aws_security_group.ybs-sg1
aws_security_group_rule.ybs-in-http-rule
aws_security_group_rule.ybs-out-any-rule
aws_subnet.ybs-subnet1
aws_subnet.ybs-subnet2
aws_vpc.ybs-vpc

앞서 생성한 VPC와 SG을 참조해서 EC2를 생성하도록 한다. 해당 EC2에는 Data Source을 통해 ami id을 참조하도록 한다. 그리고 아래 다룰 locals을 사용해서 ec2 이름을 입력하도록 했다.
배포를 하고 Public IP을 통해 접속 테스트까지 진행했다.

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

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

  owners = ["amazon"]
}

locals {
  ec2name = {
    Name = "ybs-ec2"
  }
}


resource "aws_instance" "ybs-ec2" {

  depends_on = [
    aws_internet_gateway.ybs-igw
  ]

  ami                         = data.aws_ami.ybs-aml2id.id
  associate_public_ip_address = true
  instance_type               = "t2.micro"
  vpc_security_group_ids      = ["${aws_security_group.ybs-sg1.id}"]
  subnet_id                   = aws_subnet.ybs-subnet1.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
              RZAZ=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone-id)
              IID=$(curl 169.254.169.254/latest/meta-data/instance-id)
              LIP=$(curl 169.254.169.254/latest/meta-data/local-ipv4)
              echo "<h1>RegionAz($RZAZ) : Instance ID($IID) : Private IP($LIP) : Web Server</h1>" > index.html
              nohup ./busybox httpd -f -p 80 &
              EOF

  user_data_replace_on_change = true

  tags = local.ec2name
}

output "ybs-ec2-publicip" {
  value       = aws_instance.ybs-ec2.public_ip
  description = "The public IP of the Instance"
}

# 테라폼 초기화/plan/실행
terraform init && terraform plan && terraform apply -auto-approve

# state list 확인
terraform state list
data.aws_ami.ybs-aml2id
aws_instance.ybs-ec2
aws_internet_gateway.ybs-igw
aws_route.ybs-rt1-drt
aws_route_table.ybs-rt1
aws_route_table_association.ybs-rt1-asso1
aws_route_table_association.ybs-rt1-asso2
aws_security_group.ybs-sg1
aws_security_group_rule.ybs-in-http-rule
aws_security_group_rule.ybs-out-any-rule
aws_subnet.ybs-subnet1
aws_subnet.ybs-subnet2
aws_vpc.ybs-vpc

# 데이터소스 값 확인
terraform console
> data.aws_ami.ybs-aml2id
{
  "architecture" = "x86_64"
  "arn" = "arn:aws:ec2:ap-northeast-2::image/ami-05749a4578e04e5ac"
  "block_device_mappings" = toset([
...
}

# 출력된 EC2 퍼블릭IP로 cul 접속 확인
terraform output -raw ybs-ec2-publicip
MYIP=$(terraform output -raw ybs-ec2-publicip)
while true; do curl --connect-timeout 1  http://$MYIP/ ; echo "------------------------------"; date; sleep 1; done
2023년 9월  8일 금요일 17시 48분 27초 KST
<h1>RegionAz(apne2-az1) : Instance ID(i-06633af887ed4a47a) : Private IP(10.10.1.84) : Web Server</h1>
------------------------------
2023년 9월  8일 금요일 17시 48분 28초 KST
<h1>RegionAz(apne2-az1) : Instance ID(i-06633af887ed4a47a) : Private IP(10.10.1.84) : Web Server</h1>
------------------------------

접속이 잘 되는 것과 내가 설정한 AZ를 포함해서 Private IP까지 호출되는 화면을 확인할 수 있다.

4. local 지역 값

Terraform에서 local 지역 값은 구성 내에서 변수처럼 사용 되지만, 입력이나 출력으로 사용되지 않는 중간 계산 결과를 저장할 수 있다. Terraform 코드 내에서 재사용될 수 있는 값들을 저장하는데 사용 된다고 보면 된다. locals 블록 내에서 하나 이상의 로컬 값을 정의하여 사용할 수 있다.

local값 정의와 사용은 아래와 같이 쓸 수 있다.
아래와 같은 값을 사용하면 간단하게 resource name을 설정할 수 있다.

locals {
  calculated_value = var.some_input * 10
  name_prefix      = "dev-"
  full_name        = "${local.name_prefix}${var.resource_name}"
}

resource "aws_instance" "example" {
  tags = {
    Name = local.full_name
  }
}

간단한 실습 예제를 통해 테스트를 진행해봤다. iamuser의 이름을 locals 블록에 넣고 실행한 내용이다.

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

locals {
  name = "mytest"
  team = {
    group = "dev"
  }
}

resource "aws_iam_user" "myiamuser1" {
  name = "${local.name}1"
  tags = local.team
}

resource "aws_iam_user" "myiamuser2" {
  name = "${local.name}2"
  tags = local.team
}

#
terraform init && terraform apply -auto-approve
terraform state list
aws_iam_user.myiamuser1
aws_iam_user.myiamuser2
terraform state show aws_iam_user.myiamuser1
resource "aws_iam_user" "myiamuser1" {
    arn           = "arn:aws:iam::xxxxxxxxxx:user/mytest1"
    force_destroy = false
    id            = "mytest1"
    name          = "mytest1"
....
}

5. 반복문

Terraform에서 반복문은 크게 4가지 정도가 있다. count, for, for_each, dynamic이다.

Count
count는 리소스 또는 모듈 블록에서 사용되며, 동일한 형태의 리소스를 여러 개 생성할 수 있다. count.index를 통해 현재 반복의 인덱스에 접근할 수 있다.
아래 예제를 실행시키면 3개의 AWS EC2를 생성하며 각 EC2의 이름에 index 번호를 붙일 수 있다.

resource "aws_instance" "example" {
  count = 3

  ami           = "ami-xxxxxxxxxxxxxx"
  instance_type = "t2.micro"

  tags = {
    Name = "example-instance-${count.index}"
  }
}

위의 예와 같이 count를 넣은 숫자 만큼 반복하며 그 count 숫자의 index 값을 tags 등에 활용할 수 있는 것을 볼 수 있다.

For_each
for_each는 count와 비슷하지만, 목록 또는 맵에 대해 반복을 실행한다. for_each를 사용하면 생성된 리소스에 더 의미 있는 이름을 부여할 수 있고, 특정 리소스에만 변경을 적용할 수 있다.
아래 코드를 실행시키면 로컬의 user_map을 기반으로 AWS IAM 사용자를 생성한다. each.key는 alice, bob 등의 사용자 이름을, each.value는 admin, developer 등의 역할을 나타낸다. 이렇게 for_each을 사용해서 tag을 입력하는 용도로 사용할 수 있다.

locals {
  user_map = {
    alice = "admin"
    bob   = "developer"
  }
}

resource "aws_iam_user" "example" {
  for_each = local.user_map

  name = each.key
  tags = {
    Role = each.value
  }
}

For 표현식
for 표현식은 목록, 맵, 집합 등에 대한 반복을 수행할 수 있다. 이는 리소스 블록 외부에서 리스트나 맵을 생성할 때 주로 사용된다.
아래 코드는 숫자 목록 [1, 2, 3, 4]에 대해 제곱을 구하고, 이를 맵 형태로 저장한다. 출력 결과는 {“1” = 1, “2” = 4, “3” = 9, “4” = 16}과 같이 출력 된다.

locals {
  numbers    = [1, 2, 3, 4]
  square_map = { for num in local.numbers : tostring(num) => num * num }
}

output "square_map" {
  value = local.square_map
}

dynamic
dynamic 블록은 복잡한 객체의 반복을 처리하기 위해 사용된다. 대부분의 경우, 리소스의 중첩된 블록(예: AWS 보안 그룹 규칙, VPC 서브넷 등)을 동적으로 생성할 때 유용하게 쓰인다. dynamic 블록을 사용하면 코드 중복을 줄이고, 조건부 또는 반복적인 구성을 쉽게 할 수 있습다.
dynamic 블록 내에서 for_each나 count 같은 반복문을 사용하여 여러 개의 중첩된 블록을 생성할 수 있으며, content 블록을 사용하여 실제로 생성될 내용을 정의한다.

아래 코드에서 dynamic “ingress” 블록은 for_each를 통해 여러 인그레스 규칙을 동적으로 생성한다. content 블록 내에서는 실제 규칙에 대한 세부 정보를 지정한다.

resource "aws_security_group" "example" {
  name        = "example"
  description = "Example security group"

  dynamic "ingress" {
    for_each = [
      {
        from_port   = 22
        to_port     = 22
        protocol    = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
      },
      {
        from_port   = 80
        to_port     = 80
        protocol    = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
      }
    ]
    content {
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}

간단하게 반복문에 대해 알아봤고 반복문을 사용한 실습을 진행해보도록 한다.

count부터 실습을 진행해봤다. iam user을 count 입력 받아 반복해서 생성하는 실습이다.
iam user을 생성하고 console 명령어를 통해 user 정보까지 확인해보았다.

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

resource "aws_iam_user" "myiam" {
  count = 3
  name  = "myuser.${count.index}"
}

# 테라폼 초기화/plan/실행
terraform init && terraform plan && terraform apply -auto-approve

# state list 확인
terraform state list
aws_iam_user.myiam[0]
aws_iam_user.myiam[1]
aws_iam_user.myiam[2]

terraform console
> aws_iam_user.myiam[0]
{
  "arn" = "arn:aws:iam::xxxxxxxxxxxxx:user/myuser.0"
  "force_destroy" = false
  "id" = "myuser.0"
  "name" = "myuser.0"
  "path" = "/"
  "permissions_boundary" = tostring(null)
  "tags" = tomap(null) /* of string */
  "tags_all" = tomap({})
  "unique_id" = "xxxxxxxxxxxxxxxxxxxx"
}
> aws_iam_user.myiam[0].name
"myuser.0"

#삭제
terraform destroy -auto-approve

이번에는 위와 같이 count을 사용하지만 입력변수를 받아 iam user 이름을 정의하도록 한다.
iam user을 count 길이(여기서는 3) 만큼 만들고 각 사용자의 arn을 출력하도록 한다.

variable "user_names" {
  description = "Create IAM users with these names"
  type        = list(string)
  default     = ["gasida", "akbun", "ssoon"]
}

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

resource "aws_iam_user" "myiam" {
  count = length(var.user_names)
  name  = var.user_names[count.index]
}

output "first_arn" {
  value       = aws_iam_user.myiam[0].arn
  description = "The ARN for the first user"
}

output "all_arns" {
  value       = aws_iam_user.myiam[*].arn
  description = "The ARNs for all users"
}

# 테라폼 초기화/plan/실행
terraform init && terraform plan && terraform apply -auto-approve

# state list 확인
terraform state list
aws_iam_user.myiam[0]
aws_iam_user.myiam[1]
aws_iam_user.myiam[2]

#output 출력
terraform output
terraform output all_arns
all_arns = [
  "arn:aws:iam::xxxxxxxxxxxxx:user/gasida",
  "arn:aws:iam::xxxxxxxxxxxxx:user/akbun",
  "arn:aws:iam::xxxxxxxxxxxxx:user/ssoon",
]
first_arn = "arn:aws:iam::xxxxxxxxxxxxx:user/gasida"
[
  "arn:aws:iam::xxxxxxxxxxxxx:user/gasida",
  "arn:aws:iam::xxxxxxxxxxxxx:user/akbun",
  "arn:aws:iam::xxxxxxxxxxxxx:user/ssoon",
]

이번에는 for_each을 사용해서 iam user 3명을 만들어보도록 하겠다.
count에서 이름을 받아 길이를 통해 만드는 것과 유사하다. 입력 변수에 사용자명 3개를 만들고 for_each에 변수를 호출한다. output에서 사용자 이름을 출력하는 것도 같이 입력해준다.
count와 다르게 state list을 했을 때 [0]~[2]가 아니라 [사용자명]~[사용자명]으로 출력되는 것을 볼 수 있다.

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

resource "aws_iam_user" "myiam" {
  for_each = toset(var.user_names)
  name     = each.value
}

variable "user_names" {
  description = "Create IAM users with these names"
  type        = list(string)
  default     = ["gasida", "akbun", "ssoon"]
}

output "all_users" {
  value = aws_iam_user.myiam
}


# 테라폼 초기화/plan/실행
terraform init && terraform plan && terraform apply -auto-approve

# state list 확인
terraform state list
aws_iam_user.myiam["akbun"]
aws_iam_user.myiam["gasida"]
aws_iam_user.myiam["ssoon"]

#output 출력
terraform output
all_users = {
  "akbun" = {
    "arn" = "arn:aws:iam::xxxxxxxxxxxxx:user/akbun"
    "force_destroy" = false
    "id" = "akbun"
    "name" = "akbun"
    "path" = "/"
....
}

6. 정리

오늘은 프로그래밍 언어를 배우다보면 기초적으로 배우게 되는 변수 활용과 반복문에 대해서 진행해보았다. 테라폼은 비교적 단순한 구조를 갖고 있지만 이런 부분들을 활용하지 못하면 복잡한 구조의 인프라를 배포하기 어려울 거 같다는 생각을 하게 됐다.

Terraform Study #1

예전 테라폼 스터디에 참여한 적이 있었는데 그때는 배운 뒤 업무에 사용할 일이 없어 많이 응용할 기회가 없었다.

최근 테라폼을 사용해야 할 일이 생겼는데 마침 다시 좋은 기회가 생겨 스터디에 참여하게 됐다. 이번 스터디를 통해 IaC에 대해 조금 더 익숙해지고 테라폼을 익숙하게 사용하는 계기가 되길 바란다.

IaC와 테라폼

IaC (Infrastructure as Code)
IaC는 Infrastructure을 Code로 관리하는 것을 의미한다. 기존 관리 방식은 제공되는 콘솔 등을 통해 설정하는 방식이었다면 IaC를 통해 Code로 표현하고 관리할 수 있게 됐다.
IaC의 특징은 다음과 같다.
버전 관리: 인프라를 코드로 관리하면서 소프트웨어 코드처럼 버전 관리가 가능
복제 용이: 코드를 재사용하여 쉽게 동일한 인프라를 다른 환경에 배포 가능
자동화: 인프라 변경 작업을 자동화하여, 오류 가능성을 줄이고 효율성 상승

Terraform
Terraform은 HashiCorp사에 의해 개발 된 IaC 도구이다. 여러 CSP 또는 기존 On-premise 환경에 걸쳐 인프라를 안전하고 효율적으로 구축하기 위한 도구이다.
Terraform의 특징은 다음과 같다.
선언적 언어: HCL(Hashicorp Configuration Language)라는 선언적 언어를 사용하고 이를 통해서 원하는 결과값을 코드로 작성하면 이를 인프라로 구성해준다.
Provider 지원: 앞서 Terraform에 대한 설명에 나와있듯이 CSP을 포함한 다양한 서비스 제공자에 대한 플러그인이 포함되어 있기 때문에 여러 환경의 인프라를 관리할 수 있다.
상태 관리: Terraform은 관리하는 인프라의 상태를 추적하고 이를 원하는 상태로 만들기 위한 계획을 자동으로 생성한다.

0. 환경 구성

Terraform 실습 환경 구성은 특별할 게 없다. 이미 내 PC에는 실습환경이 구성되어 있기 때문에 스터디에서 제공 받은 내용을 간단하게 공유한다.

# tfenv 설치
brew install tfenv

# 설치 가능 버전 리스트 확인
tfenv list-remote

# 테라폼 1.5.6 버전 설치
tfenv install 1.5.6

# tfenv로 설치한 버전 확인(변경 전)
tfenv list
  1.5.6
* 1.5.1 (set by /usr/local/Cellar/tfenv/3.0.0/version)

# 테라폼 1.5.6 버전 사용 설정 
tfenv use 1.5.6

# tfenv로 설치한 버전 확인(변경 후)
tfenv list
* 1.5.6 (set by /usr/local/Cellar/tfenv/3.0.0/version)
  1.5.1

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

# 자동완성
terraform -install-autocomplete
## 참고 .zshrc 에 아래 추가됨
cat ~/.zshrc
autoload -U +X bashcompinit && bashcompinit
complete -o nospace -C /usr/local/bin/terraform terraform

1. EC2 배포

Terraform을 사용해서 기본적인 EC2를 배포해보도록 하겠다.
이는 어려운 부분이 아니라 간단하게 스터디 내용을 참고해서 진행했다.

#최신 Amazon Linux2 버전 ami ID 찾기
#aws ec2 describe-images --owners self amazon
aws ec2 describe-images --owners self amazon --query 'Images[*].[ImageId]' --output text

aws ssm get-parameters-by-path --path /aws/service/ami-amazon-linux-latest
aws ssm get-parameters-by-path --path /aws/service/ami-amazon-linux-latest --query "Parameters[].Name"
aws ssm get-parameters-by-path --path /aws/service/ami-amazon-linux-latest --query "Parameters[].Value"

생성 입력을 넣었을 때 생성되는 순간을 확인하기 위해 별도 터미널에서 모니터링을 진행한다.

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

EC2 생성하는 tf 파일을 만든다.

# VS Code 터미널2
cat <<EOT > main.tf
provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_instance" "example" {
  ami           = "ami-084e92d3e117f7692"
  instance_type = "t2.micro"
}
EOT

초기화와 Plan 그리고 배포를 순서대로 진행한다.

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

위 사진들을 순서대로 보면 초기화/Plan/배포까지 별 문제없이 잘 진행된 것을 알 수 있다.

EC2 배포를 완료했으니 이제 .tf 파일을 수정해서 EC2의 Tag 정보를 수정해보도록 하겠다.
기존에는 EC2에 Name Tag을 입력하지 않았으나 이번엔 tf 파일에 Name Tag을 추가하였다.

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

resource "aws_instance" "example" {
  ami           = "ami-084e92d3e117f7692"
  instance_type = "t2.micro"

  tags = {
    Name = "t101-study"
  }

}
EOT

Name Tag을 추가한 tf 파일의 배포를 진행하기 전에 Plan을 진행했다.
Plan 단계에서 Name Tag가 추가되는 것을 확인할 수 있다. (Patch Group은 무관한 내용이다.)

Plan 내용을 확인하고 Apply을 진행했다. 모니터링 중인 터미널 창에서도 Name Tag을 통해 이름이 등록된 것을 확인할 수 있다.

Terraform 으로 생성한 리소스를 삭제할 때는 생성과 같이 간단하다. 다만 모든 리소스가 삭제 될 수 있으니 이 부분은 조심해야 한다. destroy 명령어를 입력하면 EC2 리소스가 잘 삭제되는 것을 확인할 수 있다. EC2 뿐만 아니라 EC2가 생성되며 같이 생성 되는 ebs도 같이 잘 삭제 된 것을 알 수 있다.

#삭제 명령어
terraform destroy -auto-approve

2. EC2 1대 배포 & 웹 서버 설정(도전과제 #1 병행)

간단하게 EC2를 1대 배포/수정/삭제해보았다면 이제 해당 EC2에 Web Server 설정을 진행하는 부분까지 진행해보도록 하겠다. 기본적인 EC2 생성은 동일하게 진행하지만 웹서버로 작동시키기 위해 보안그룹을 생성하고 연결하는 과정이 추가 될 예정이다.

우선 웹서버 역할을 할 Ubuntu 22.04 Image을 사용하여 EC2를 배포해준다.
이때 user_data라는 구문을 사용하여 bash 쉘을 통해 내 이름을 출력하는 index.html 생성 및 httpd 설치와 80 Port Open을 진행해준다. 이후 배포 된 EC2의 Public IP에 접속 테스트를 다른 터미널에서 진행해주었다.

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

resource "aws_instance" "example" {
  ami                    = "ami-0c9c942bd7bf113a2"
  instance_type          = "t2.micro"

  user_data = <<-EOF
              #!/bin/bash
              echo "I'm Boseung" > index.html
              nohup busybox httpd -f -p 80 &
              EOF

  tags = {
    Name = "terraform-Study-101"
  }
}
EOT

# init
terraform init

# plan
terraform plan

# apply 실행
terraform apply -auto-approve

# [터미널3] 변수 지정
PIP=<EC2 Public IP>
while true; do curl --connect-timeout 1  http://$PIP:80/ ; echo "------------------------------"; date; sleep 1; done

어쩌면 당연한 결과겠지만 제대로 연결되지 않는 것을 확인할 수 있다. 서두에서 말한 것처럼 보안그룹의 설정이 필요하다.

8080을 허용해주는 보안그룹을 생성하고 EC2에 해당 보안그룹을 연결하는 구문을 추가하였다. 그리고 output 명령어를 통해 배포되는 EC2의 Public IP을 확인한다.

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

resource "aws_instance" "example" {
  ami                    = "ami-0c9c942bd7bf113a2"
  instance_type          = "t2.micro"
  vpc_security_group_ids = [aws_security_group.instance.id]

  user_data = <<-EOF
              #!/bin/bash
              echo "I'm Boseung" > index.html
              nohup busybox httpd -f -p 80 &
              EOF

  tags = {
    Name = "Single-WebSrv"
  }
}

resource "aws_security_group" "instance" {
  name = var.security_group_name

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

variable "security_group_name" {
  description = "The name of the security group"
  type        = string
  default     = "terraform-example-instance"
}

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

# plan/apply
terraform plan
terraform apply -auto-approve

보안 그룹이 생성되었기 때문에 웹서버에 정상적으로 접근이 되는 것을 확인할 수 있다. 또한 Output을 통해 Public IP가 호출되는 것도 확인할 수 있다.

생성을 완료하고 리소스 다이어그램을 살펴보기 위해 graphviz을 통해 다이어그램을 실행시켜봤다.

#graph 다이어그램 실행
terraform graph > websrv.dot

websrv.dot 파일 생성 후 해당 파일 선택한 다음 MAC OS 기준 CMD+Shift+V을 눌러 다이어그램을 띄워봤다.
Terraform이 어떤 식으로 작동되는지를 간단하게나마 확인할 수 있었다.

3. S3 Backend 활용 리소스 배포(도전과제 #2)

Terraform에서 S3을 Backend을 사용해 리소스를 배포하는 도전과제를 진행해볼 예정이다.
그에 앞서, Terraform에서의 Backend는 어떤 의미인지 알아보자.

Terraform에서 Backend는 크게 2가지의 기능을 수행한다.

  1. 상태 파일 관리: Terraform은 .tfstate라는 상태 파일을 사용하여 인프라의 현재 상태를 추적한다. 이 파일은 로컬 파일 시스템에 저장할 수도 있고, 원격 스토리지 서비스(AWS S3, Azure Blob Storage 등)에 저장할 수도 있다. Backend 설정을 통해 이러한 원격 저장소를 지정할 수 있고, 여러 팀원이 상태 파일을 공유할 수 있게 된다. 따라서 협업을 진행할 때 해당 상태 파일을 통해 내용을 확인할 수 있다.
  2. Execution Lock: 여러 사람이나 시스템이 동시에 같은 Terraform 구성에 대한 변경을 수행하는 것을 막기 위해 사용한다다. 원격 Backend를 사용하면, Terraform은 자동으로 실행 중인 작업을 잠금 처리(Lock)하여 동시 변경을 방지한다. 이를 통해 협업을 진행할 때 동시에 작업이 수행되어 문제가 발생하는 것을 방지할 수 있다.

위와 같은 기능을 갖고 있는 Terraform Backend을 AWS S3에 저장하여 진행할 예정이다.

S3 Bucket을 미리 생성하고 해당 S3 Bucket을 Backend로 사용하는 내용으로 진행한다.

# backend.tf 파일 생성
cat <<EOT > backend.tf
terraform {
  backend "s3" {
    bucket = "ybs-tf-backend"
    key    = "backend/terraform.tfstate"
    region = "ap-northeast-2"
  }
}
EOT

# terraform init 진행
terraform init

기존 만들어둔 S3을 사용해서 backend.tf을 만들고 terraform init을 진행했다.
터미널 창에 S3을 사용한 backend 구성이 진행된 것을 확인할 수 있다.

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

resource "aws_instance" "ybs-websrv-ec2" {
  ami                    = "ami-0c9c942bd7bf113a2"
  instance_type          = "t2.micro"
  vpc_security_group_ids = [aws_security_group.ybs-websrv-sg.id]

  user_data = <<-EOF
              #!/bin/bash
              echo "I'm Boseung" > index.html
              nohup busybox httpd -f -p 80 &
              EOF

  tags = {
    Name = "Single-WebSrv"
  }
}

resource "aws_security_group" "ybs-websrv-sg" {
  name = var.security_group_name

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

variable "security_group_name" {
  description = "The name of the security group"
  type        = string
  default     = "ybs-websrv-sg"
}

output "public_ip" {
  value       = aws_instance.ybs-websrv-ec2.public_ip
  description = "The public IP of the Instance"
}
EOT

# Plan/Appliy 진행
terraform plan
terraform apply -auto-approve

EC2 배포는 당연히 문제없이 잘 되었다.

하지만 우리가 궁금한 건 EC2 배포가 아닌 Backend 설정이었으니 Backend로 할당한 S3을 확인해보도록 한다.

Bucket에 들어가보면 tfstate 파일이 생성된 것을 볼 수 있다.

파일을 다운로드 받아 열어보면 아래와 같은 내용도 확인 가능하다.

tfstate 파일은 중요한 내용들을 담고있을 수 있으니 파일에 대한 권한 설정도 확실히 해야 한다. 실제 협업을 할 때는 접근해야 하는 사용자에 한해서만 tfstate 파일에 접근 권한을 줘야 할 것 같다.

4. 정리

Terraform을 오랜만에 다시 접하니 처음 접한 마음처럼 느껴졌다. 조금은 더 익숙해질 수 있도록 이것저것 다뤄봐야 할 것 같다.

AEWS Study #7 – EKS Automation

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

0. 환경 구성

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

1. AWS Controller for Kubernetes (ACK)

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

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

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

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

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

1-1. ACK for S3

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

echo "${BUCKET_MANIFEST}" > bucket.yaml

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

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

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

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

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

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

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

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

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

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

1-2. ACK for EC2&VPC

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

# [터미널1] 모니터링
while true; do aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table; date ; sleep 1 ; done
+----------------+-----------------+------------------+----------+
|  InstanceName  |  PrivateIPAdd   |   PublicIPAdd    | Status   |
+----------------+-----------------+------------------+----------+
|  myeks-ng1-Node|  192.168.3.165  |  54.180.xxx.xxx  |  running |
|  myeks-ng1-Node|  192.168.2.5    |  3.35.xxx.xxx    |  running |
|  myeks-ng1-Node|  192.168.1.10   |  43.201.xxx.xxx  |  running |
|  myeks-bastion |  192.168.1.100  |  54.180.xxx.xxx  |  running |
+----------------+-----------------+------------------+----------+
Sun Jun  4 22:09:42 KST 2023

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


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

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

aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
+----------------+-----------------+------------------+----------+
|  InstanceName  |  PrivateIPAdd   |   PublicIPAdd    | Status   |
+----------------+-----------------+------------------+----------+
|  myeks-ng1-Node|  192.168.3.165  |  54.180.xxx.xxx  |  running |
|  myeks-ng1-Node|  192.168.2.5    |  3.35.xxx.xxx    |  running |
|  myeks-ng1-Node|  192.168.1.10   |  43.201.xxx.xxx  |  running |
|  myeks-bastion |  192.168.1.100  |  54.180.xxx.xxx  |  running |
|  None          |  10.0.5.137     |  13.209.xxx.xxx  |  running |
+----------------+-----------------+------------------+----------+
Sun Jun  4 22:10:42 KST 2023

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

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

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

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

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

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

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

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

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

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

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

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

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

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

aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
+----------------+-----------------+------------------+----------+
|  InstanceName  |  PrivateIPAdd   |   PublicIPAdd    | Status   |
+----------------+-----------------+------------------+----------+
|  myeks-ng1-Node|  192.168.3.165  |  54.180.xxx.xxx  |  running |
|  myeks-ng1-Node|  192.168.2.5    |  3.35.xxx.xxx    |  running |
|  myeks-ng1-Node|  192.168.1.10   |  43.201.xxx.xxx  |  running |
|  myeks-bastion |  192.168.1.100  |  54.180.xxx.xxx  |  running |
|  None          |  10.0.5.137     |  13.209.xxx.xxx  |  running |
|  None          |  10.0.128.130   |  None            |  running |
+----------------+-----------------+------------------+----------+

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

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

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

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

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

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

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

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

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

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

aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
+----------------+-----------------+------------------+----------+
|  InstanceName  |  PrivateIPAdd   |   PublicIPAdd    | Status   |
+----------------+-----------------+------------------+----------+
|  myeks-ng1-Node|  192.168.3.165  |  54.180.xxx.xxx  |  running |
|  myeks-ng1-Node|  192.168.2.5    |  3.35.xxx.xxx    |  running |
|  myeks-ng1-Node|  192.168.1.10   |  43.201.xxx.xxx  |  running |
|  myeks-bastion |  192.168.1.100  |  54.180.xxx.xxx  |  running |
+----------------+-----------------+------------------+----------+

1-3. ACK for RDS

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

kubectl apply -f rds-field-exports.yaml

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

# 확인
kubectl get dbinstance myrds
NAME    STATUS
myrds   available

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

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

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

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

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

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

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

# 파드 삭제
kubectl delete pod app

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

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

1-4. ACK for DynamoDB

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

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

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

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

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

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

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

2. Flux

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

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

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

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

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

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

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

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

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

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

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

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

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

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

kubectl -n flux-system get pod,svc

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

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

# Ingress 설정

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

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

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

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

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

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

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

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

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

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

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

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

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

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

kubectl get pod,svc nginx-example1

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

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

kubectl get pod,svc nginx-example1

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

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

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

# flux 삭제

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

3. GitOps with ArgoCD

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

kubectl get crd | grep argoproj

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4. Crossplane

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

kubectl get buckets
No resources found

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

kubectl get bucket
No resources found

aws s3 ls

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

5. eksdemo

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

6. 정리

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

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

AEWS Study #6 – EKS Security

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

0. 환경 구성

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

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

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

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

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

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

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

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

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

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

1. K8S 인증/인가

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

k1 delete pods nginx
pod "nginx" deleted

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

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


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

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

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

k2 delete pods nginx
pod "nginx" deleted

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

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

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

2. EKS 인증/인가

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

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


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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

Action=GetCallerIdentity&

Version=2011-06-15&

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

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

X-Amz-Date=20230602T055815Z&

X-Amz-Expires=60&

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

# EC2 IP 확인 : myeks-bastion-EC2-2 PublicIPAdd 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
-----------------------------------------------------------------------
|                          DescribeInstances                          |
+----------------------+----------------+------------------+----------+
|     InstanceName     | PrivateIPAdd   |   PublicIPAdd    | Status   |
+----------------------+----------------+------------------+----------+
|  myeks-ng1-Node      |  192.168.3.248 |  54.180.xxx.xx   |  running |
|  myeks-ng1-Node      |  192.168.2.161 |  3.38.xxx.xx     |  running |
|  myeks-bastion-EC2-2 |  192.168.1.200 |  54.180.xxx.xxx  |  running |
|  myeks-bastion-EC2   |  192.168.1.100 |  43.201.xxx.xxx  |  running |
|  myeks-ng1-Node      |  192.168.1.41  |  52.78.xxx.xxx   |  running |
+----------------------+----------------+------------------+----------+

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

kubectl api-resources -v5

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

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

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

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

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

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

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

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

3. IRSA

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5. 정리

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

AEWS Study #5 – EKS Autoscaling

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

0. 환경 구성

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

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

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

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

  verticalPodAutoscaler:
    enabled: true

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

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

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

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

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

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

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

# go 설치
yum install -y go

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

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

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

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

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

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

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

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

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

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

# display both CPU and memory
resources=cpu,memory

1. Auto Scaling?

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

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

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

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

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

2. HPA – Horizontal Pod Autoscaler

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

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

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

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

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

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

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

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

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

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

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

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

3. KEDA – Kubernetes based Event Driven Autoscaler

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4. VPA – Vertical Pod Autoscaler

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

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

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

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

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

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

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

# 모니터링
watch -d kubectl top pod

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

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

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

5. CA – Cluster Autoscaler

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

# 터미널1
watch -d kubectl get node

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

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

6. CPA – Cluster Proportional Autoscaler

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

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

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

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

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

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

# 모니터링
watch -d kubectl get pod

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

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

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

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


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


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

7. Karpenter : K8S Native AutoScaler & Fargate

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

NAME                               AGE
provisioner.karpenter.sh/default   2m35s

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

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

kubectl create namespace monitoring

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

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

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

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

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

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

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

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

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

8. 정리

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

AEWS Study #4 – EKS Observability

AEWS 4회차는 EKS Observability에 대해 다룬다. 이전에 kOps 때 다뤘던 모니터링 및 대쉬보드에 대한 내용을 주로 다룰 예정이다.

0. 환경 구성

환경 구성은 매번 실습 때 가시다님이 제공해주시는 One Click Yaml을 통해 배포를 진행한다.
이번 실습 때는 이전에 시도했다가 성공하지 못한 Cross-Account ExternalDNS을 통해 진행하려고 한다. Cross-Account ExternalDNS 설정은 아래와 같이 진행할 예정이다. 여기서 Route53 Account는 Route53 Record을 관리하는 Account이고 EKS Account는 실습에 사용하는 EKS을 배포하는 Account이다.

EKS Account에 Policy&Role을 생성한다.
Policy는 Route53 Account에서 만들 Role에 대해 AssumeRole하는 Action으로 만든다.
Role은 Trust relationships을 작성할 때 EKS에서 확인 가능한 OIDC Provider URL을 참고해서 입력하고 Permission Policy는 위에서 만든 Policy을 선택한다

#Policy 생성
#Policy Name : external-dns-policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::ROUTE53Account:role/external-dns-cross-account"
        }
    ]
}

#Role 생성
#Role Name : external-dns
#Trust relationships
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Principal": {
				"AWS": "arn:aws:iam::EKSAccount:root"
			},
			"Action": "sts:AssumeRole",
			"Condition": {}
		},
		{
			"Effect": "Allow",
			"Principal": {
				"Federated": "arn:aws:iam::EKSAccount:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/2FD..."
			},
			"Action": "sts:AssumeRoleWithWebIdentity",
			"Condition": {
				"StringEquals": {
					"oidc.eks.ap-northeast-2.amazonaws.com/id/2FD...:sub": "system:serviceaccount:kube-system:external-dns"
				}
			}
		}
	]
}

Route53 Account에도 Role을 생성해준다.
Permission Policy는 나는 AmazonRoute53FullAccess을 줬지만 필요에 따라 최소한의 정책을 설정해줘도 된다. 그리고 Trust relationships 작성 시에 EKSAccount의 위에서 만든 Role인 external-dns Role에 대해 sts:AssumeRole 허용해줬다.

#Role 생성
#Role Name : external-dns-cross-account
#Trust relationships
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::EKSAccount:role/external-dns"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

위와 같이 Role을 만들어줬다면 externaldns 배포에 사용되는 yaml 파일을 수정한 후 배포를 진행하면 된다.
ServiceAccount kind에서 annotations 부분에 EKSAccount의 Role ARN을 입력해준다.
Deployment kind에서는 Containers args 부분을 찾은 뒤 Route53 Account의 Role ARN을 입력해준다.

#externaldns.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns
  namespace: kube-system
  #아래 내용 삽입
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::EKSAccount:role/external-dns
  #
  labels:
    app.kubernetes.io/name: external-dns
.......
apiVersion: apps/v1
kind: Deployment

          args:
            - --source=service
            - --source=ingress
            - --domain-filter=${MyDomain} # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
            - --provider=aws
            #아래 내용 삽입
            - --aws-assume-role=arn:aws:iam::Route53Account:role/external-dns-cross-account
            #
            #- --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
            - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
            - --registry=txt
            - --txt-owner-id=${MyDnzHostedZoneId}
...........

이후 ExternalDNS 배포를 진행하면 Cross Account ExternalDNS을 배포할 수 있게 된다.
이때 Route53 Hosted Zone ID는 수동으로 입력해주었다.
ExternalDNS 및 kube-ops-view을 설치 후 ExternalDNS Log을 확인해보면 제대로 조회 및 등록이 된 것을 확인할 수 있다. 해당 URL로 접근하면 페이지도 제대로 나오는 것을 알 수 있다.

# ExternalDNS
MyDomain=bs-yang.com
echo "export MyDomain=bs-yang.com" >> /etc/profile
MyDnzHostedZoneId=Z030...
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"

# Log 확인
kubectl logs -n kube-system external-dns-
time="2023-05-16T06:56:38Z" level=info msg="Instantiating new Kubernetes client"
time="2023-05-16T06:56:38Z" level=info msg="Using inCluster-config based on serviceaccount-token"
time="2023-05-16T06:56:38Z" level=info msg="Created Kubernetes client https://10.100.0.1:443"
time="2023-05-16T06:56:38Z" level=info msg="Assuming role: arn:aws:iam::Route53Account:role/external-dns-cross-account"
time="2023-05-16T06:56:39Z" level=info msg="Applying provider record filter for domains: [bs-yang.com. .bs-yang.com.]"
time="2023-05-16T06:56:40Z" level=info msg="All records are already up to date"
time="2023-05-16T06:57:39Z" level=info msg="Applying provider record filter for domains: [bs-yang.com. .bs-yang.com.]"
time="2023-05-16T06:57:39Z" level=info msg="Desired change: CREATE cname-kubeopsview.bs-yang.com TXT [Id: /hostedzone/Z030...]"
time="2023-05-16T06:57:39Z" level=info msg="Desired change: CREATE kubeopsview.bs-yang.com A [Id: /hostedzone/Z030...]"
time="2023-05-16T06:57:39Z" level=info msg="Desired change: CREATE kubeopsview.bs-yang.com TXT [Id: /hostedzone/Z030...]"
time="2023-05-16T06:57:40Z" level=info msg="3 record(s) in zone bs-yang.com. [Id: /hostedzone/Z030...] were successfully updated"

LB Controller와 EBS csi driver, gp3 sc, EFS sc 등을 설치 및 설정하면서 환경 설정을 마무리한다.

# 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
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
  allowAutoIOPSPerGBIncrease: 'true'
  encrypted: 'true'
EOT
kubectl apply -f gp3-sc.yaml
kubectl get sc

# EFS csi driver 설치
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=602401143452.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

# EFS 스토리지클래스 생성 및 확인
curl -s -O https://raw.githubusercontent.com/kubernetes-sigs/aws-efs-csi-driver/master/examples/kubernetes/dynamic_provisioning/specs/storageclass.yaml
sed -i "s/fs-92107410/$EfsFsId/g" storageclass.yaml
kubectl apply -f storageclass.yaml
kubectl get sc efs-sc

1. EKS Console

EKS에 대한 옵저빌리티 중 EKS Console에서 확인하는 내용이다. EKS라고 이름은 되어있지만 EKS Console 상에서 확인하는 대부분의 내용은 k8s API을 통해 호출된다.
예전에 혼자 EKS을 배포하는 테스트를 할 때 EKS 배포는 했는데 EKS Console에서 상세한 내용을 확인할 수 없는 문제가 있었다. 분명 Admin User였어서 권한의 문제는 없어야하는데 아래와 같은 메시지가 나오면서 확인되지 않는 부분이 있었다.


이런 메시지가 나오는 이유는 아무리 AdminAccess 권한을 갖고 있더라도 eks cluster role을 부여받지 않으면 저런 메시지가 호출되게 된다.
물론 EKS 배포에 사용한 iam user의 경우 자신이 EKS Cluster Owner이기 때문에 해당 권한을 갖고있지 않더라도 EKS Console 및 kubectl 등 명령어를 통해 확인하는데는 문제가 없다.
해당 Admin User에 EKS Rolebinding을 해주면 문제 없이 EKS Console 등에서 확인이 가능하다.
EKS Console 에서 확인 가능한 내용은 다양하다. Pod 정보를 포함하여 k8s에서 조회가 가능한 대부분의 내용을 확인할 수 있다고 보면 된다.


2. Logging in EKS

EKS에서의 Logging의 경우 CP, node, App 등에 대한 로깅이 가능하다.
CP 로깅 종류는 API Server, Authenticator, Audit, Controller Manager, Scheduler가 있다. 기본적으로는 로깅 Off로 되어있고 이걸 aws cli을 통해 활성화 할 수 있다.
CP 로깅의 자세한 내용은 아래 링크를 참고 가능하다.
https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/control-plane-logs.html

# 모든 로깅 활성화
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

Container Pod 로깅도 가능하다. Pod 로깅을 테스트하기 위해 Helm에서 nginx 서버를 배포한다.

# 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

# 파라미터 파일 생성
cat <<EOT > nginx-values.yaml
service:
    type: NodePort

ingress:
  enabled: true
  ingressClassName: alb
  hostname: nginx.$MyDomain
  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 14.1.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

# (참고) 삭제 시
helm uninstall nginx

설치하고 브라우저에서 정상적으로 페이지가 열리는 것을 확인했다면 별도의 모니터링용 터미널을 하나 더 띄우고 반복접속과 모니터링 결과를 확인한다. 접속이 하나 들어올 때마다 로그에도 내용이 추가되는 것을 확인할 수 있었다. 이렇게 간략하게 Pod의 Application 로그를 확인하는 내용을 진행해보았다.

# 반복 접속
while true; do curl -s https://nginx.$MyDomain -I | head -n 1; date; sleep 1; done

# 로그 모니터링
kubectl logs deploy/nginx -f

# 컨테이너 로그 파일 위치 확인
kubectl exec -it deploy/nginx -- ls -l /opt/bitnami/nginx/logs/
total 0
lrwxrwxrwx 1 root root 11 Apr 24 10:13 access.log -> /dev/stdout
lrwxrwxrwx 1 root root 11 Apr 24 10:13 error.log -> /dev/stderr

3. Container Insights metrics in Amazon CloudWatch & Fluent Bit (Logs)

CCI(CloudWatch Container Insight)는 노드에 Cloudwatch Agent 파드와 Fluent Bit 파드가 데몬셋으로 배치되어 Metrics와 Log을 수집할 수 있다.
Fluent Bit는 Cloudwatch Logs에 로그를 전송하기 위한 데몬셋이다.
Fluent Bit 데몬셋으로 로그를 수집하고 시각화하는 과정은 3단계로 작동하게 되는데, 수집-저장-시각화의 단계로 이루어진다.
[수집] : Fluent Bit 파드를 데몬셋으로 동작시키고 아래 3가지 종류의 로그를 CW Logs에 전송
1) /aws/containerinsights/Cluster_Name/application : /var/log/containers에 포함 된 모든 로그 파일과 각 컨테이너/파드 로그
2) /aws/containerinsights/Cluster_Name/host : /var/log/dmesg, /var/log/secure, /var/log/message의 로그 파일들과 노드 로그
3) /aws/containerinsights/Cluster_Name/dataplane : /var/log/journal의 로그 파일과 k8s dataplane 로그
[저장] : CW Logs에 로그를 저장한다. 로그 그룹 별 로그 보존 기간 설정 가능하다.
[시각화] : CW의 LogsInsights를 사용하여 대상 로그를 분석하고 CW의 대쉬보드를 시각화한다.
간략하게 보기 위한 그림은 아래와 같다.

이후 로그들을 열어보기 위해 사전에 Node의 로그 위치를 확인해 본다.
Application, host, dataplane 위치를 각각 확인해본다.

# Application 로그 위치 확인
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo tree /var/log/containers; echo; done
>>>>> 192.168.3.182 <<<<<
/var/log/containers
├── aws-load-balancer-controller-6fb4f86d9d-r9tch_kube-system_aws-load-balancer-controller-8de8d0ceaa922575f336ded6a7b5e30f43656765829d268ddb01f36539e80cb3.log -> /var/log/pods/kube-system_aws-load-balancer-controller-6fb4f86d9d-r9tch_48c648fb-83c1-499e-9453-e0ddb65cf088/aws-load-balancer-controller/0.log
├── aws-node-955f2_kube-system_aws-node-c36d95a4abb2e33245350b1acf670b63208e7ad0353b87e39d10a6c32904f643.log -> /var/log/pods/kube-system_aws-node-955f2_fe7aaf5b-f4bd-425b-a099-cdc8821c6844/aws-node/1.log
├── aws-node-955f2_kube-system_aws-node-d05cfd440860db1074b84ead88b5dd79575ded2b078bcb3ca2cb5d2bd1094ecc.log -> /var/log/pods/kube-system_aws-node-955f2_fe7aaf5b-f4bd-425b-a099-cdc8821c6844/aws-node/0.log
...
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo ls -al /var/log/containers; echo; done
>>>>> 192.168.3.182 <<<<<
total 16
drwxr-xr-x  2 root root 8192 May 20 03:54 .
drwxr-xr-x 10 root root 4096 May 17 00:02 ..
lrwxrwxrwx  1 root root  143 May 17 01:20 aws-load-balancer-controller-6fb4f86d9d-r9tch_kube-system_aws-load-balancer-controller-8de8d0ceaa922575f336ded6a7b5e30f43656765829d268ddb01f36539e80cb3.log -> /var/log/pods/kube-system_aws-load-balancer-controller-6fb4f86d9d-r9tch_48c648fb-83c1-499e-9453-e0ddb65cf088/aws-load-balancer-controller/0.log
lrwxrwxrwx  1 root root   92 May 17 00:02 aws-node-955f2_kube-system_aws-node-c36d95a4abb2e33245350b1acf670b63208e7ad0353b87e39d10a6c32904f643.log -> /var/log/pods/kube-system_aws-node-955f2_fe7aaf5b-f4bd-425b-a099-cdc8821c6844/aws-node/1.log
...

# host 로그 위치 확인
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo tree /var/log/ -L 1; echo; done
>>>>> 192.168.3.182 <<<<<
/var/log/
├── amazon
├── audit
├── aws-routed-eni
...
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo ls -la /var/log/; echo; done
>>>>> 192.168.3.182 <<<<<
total 4132
drwxr-xr-x  10 root   root               4096 May 17 00:02 .
drwxr-xr-x  19 root   root                268 May 17 01:21 ..
drwxr-xr-x   3 root   root                 17 May 15 01:33 amazon
drwx------   2 root   root                 61 May 20 01:18 audit
drwxr-xr-x   2 root   root                 69 May 15 01:34 aws-routed-eni
...

# dataplane 로그 위치 확인
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo tree /var/log/journal -L 1; echo; done
>>>>> 192.168.3.182 <<<<<
/var/log/journal
├── ec2179c4f3e906eda92ce733733bd5d0
└── ec221453e78b0818ba3c9f00233580cb

Cloudwatch Container Insight를 설치해서 로그 수집을 하고 내용을 확인해볼 예정이다.

# 설치
FluentBitHttpServer='On'
FluentBitHttpPort='2020'
FluentBitReadFromHead='Off'
FluentBitReadFromTail='On'
curl -s https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/quickstart/cwagent-fluent-bit-quickstart.yaml | sed 's/{{cluster_name}}/'${CLUSTER_NAME}'/;s/{{region_name}}/'${AWS_DEFAULT_REGION}'/;s/{{http_server_toggle}}/"'${FluentBitHttpServer}'"/;s/{{http_server_port}}/"'${FluentBitHttpPort}'"/;s/{{read_from_head}}/"'${FluentBitReadFromHead}'"/;s/{{read_from_tail}}/"'${FluentBitReadFromTail}'"/' | kubectl apply -f -

# 설치 확인
kubectl get-all -n amazon-cloudwatch
kubectl get ds,pod,cm,sa -n amazon-cloudwatch
kubectl describe clusterrole cloudwatch-agent-role fluent-bit-role                          # 클러스터롤 확인
kubectl describe clusterrolebindings cloudwatch-agent-role-binding fluent-bit-role-binding  # 클러스터롤 바인딩 확인
kubectl -n amazon-cloudwatch logs -l name=cloudwatch-agent -f # 파드 로그 확인
kubectl -n amazon-cloudwatch logs -l k8s-app=fluent-bit -f    # 파드 로그 확인
for node in $N1 $N2 $N3; do echo ">>>>> $node <<<<<"; ssh ec2-user@$node sudo ss -tnlp | grep fluent-bit; echo; done

# cloudwatch-agent 설정 확인
kubectl describe cm cwagentconfig -n amazon-cloudwatch
{
  "agent": {
    "region": "ap-northeast-2"
  },
  "logs": {
    "metrics_collected": {
      "kubernetes": {
        "cluster_name": "myeks",
        "metrics_collection_interval": 60
      }
    },
    "force_flush_interval": 5
  }
}

# Fluent Bit Cluster Info 확인
kubectl get cm -n amazon-cloudwatch fluent-bit-cluster-info -o yaml | yh
apiVersion: v1
data:
  cluster.name: myeks
  http.port: "2020"
  http.server: "On"
  logs.region: ap-northeast-2
  read.head: "Off"
  read.tail: "On"
kind: ConfigMap
...

# 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*
...

[FILTER]
    Name                kubernetes
    Match               application.*
    Kube_URL            https://kubernetes.default.svc:443
...

[OUTPUT]
    Name                cloudwatch_logs
    Match               application.*
    region              ${AWS_REGION}
    log_group_name      /aws/containerinsights/${CLUSTER_NAME}/application
...

# (참고) 삭제
curl -s https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/quickstart/cwagent-fluent-bit-quickstart.yaml | sed 's/{{cluster_name}}/'${CLUSTER_NAME}'/;s/{{region_name}}/'${AWS_DEFAULT_REGION}'/;s/{{http_server_toggle}}/"'${FluentBitHttpServer}'"/;s/{{http_server_port}}/"'${FluentBitHttpPort}'"/;s/{{read_from_head}}/"'${FluentBitReadFromHead}'"/;s/{{read_from_tail}}/"'${FluentBitReadFromTail}'"/' | kubectl delete -f -

Cloudwatch Container Insight을 설치하고 CW에서 로그와 매트릭 수집이 되고 있는지 확인 가능하다. 로그 그룹에 들어가보면 위에서 설정한 내용들의 로그그룹이 생성 된 것을 확인할 수 있다. 마찬가지로 Container Insight에 들어가면 메트릭도 확인할 수 있다.

로그가 제대로 나오는지 보기위해 부하를 발생시키고 로그에서 확인을 해본다.

# 부하 발생
curl -s https://nginx.$MyDomain
yum install -y httpd
ab -c 500 -n 30000 https://nginx.$MyDomain/

# 파드 직접 로그 모니터링
kubectl logs deploy/nginx -f

부하 발생을 시키고 동시에 직접 로그 모니터링을 실행한다. 로그 모니터링을 같이 하는 이유는 이후 로그 그룹에서 확인할 때 동일하게 내용이 나오는지 보기 위함이다.
다시 콘솔로 돌아가서 로그 그룹의 내용과 매트릭도 확인해보았다.매트릭은 CPU 사용률이나 기타 다양한 내용들을 확인할 수 있으니 Pod을 실행하고 부하 발생 테스트 등을 할 때 같이 모니터링을 하면 좋을 것 같다.

4. Metrics-server & kwatch & botkube

Metrics-Server : kubelet으로부터 수집한 리소스 메트릭을 수집 및 집계하는 클러스터 애드온 구성 요소로 cAdvisor(kubelet에 포함 된 커네티어 메트릭을 수집, 집계 및 노출하는 데몬)을 통해 데이터를 가져온다.

metrics-server을 배포하고 설치 내용을 확인해본다.
메트릭 값의 경우 15초 간격으로 cAdvisor을 통해 가져오게 되어있다. 그러니 최초 배포 후 일정 시간 후에 확인해보도록 하자.

# 배포
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
NAME                              READY   STATUS    RESTARTS   AGE
metrics-server-6bf466fbf5-zxjb2   1/1     Running   0          46s
kubectl api-resources | grep metrics
nodes                                          metrics.k8s.io/v1beta1                 false        NodeMetrics
pods                                           metrics.k8s.io/v1beta1                 true         PodMetrics
kubectl get apiservices |egrep '(AVAILABLE|metrics)'
NAME                                   SERVICE                      AVAILABLE   AGE
v1beta1.metrics.k8s.io                 kube-system/metrics-server   True        85s

# 노드 메트릭 확인
kubectl top node

# 파드 메트릭 확인
kubectl top pod -A
NAMESPACE           NAME                                            CPU(cores)   MEMORY(bytes)
amazon-cloudwatch   cloudwatch-agent-4k4mm                          7m           33Mi
amazon-cloudwatch   cloudwatch-agent-krmd4                          7m           34Mi
amazon-cloudwatch   cloudwatch-agent-x6bz4                          6m           30Mi
...
kubectl top pod -n kube-system --sort-by='cpu'
NAME                                            CPU(cores)   MEMORY(bytes)
kube-ops-view-558d87b798-6z5bt                  12m          38Mi
metrics-server-6bf466fbf5-zxjb2                 5m           19Mi
ebs-csi-controller-67658f895c-8zqph             5m           60Mi
...
kubectl top pod -n kube-system --sort-by='memory'
NAME                                            CPU(cores)   MEMORY(bytes)
ebs-csi-controller-67658f895c-8zqph             6m           60Mi
ebs-csi-controller-67658f895c-lm6wq             2m           52Mi
aws-node-n962c                                  5m           47Mi
...

kwatch : Kubernetes(K8s) 클러스터의 모든 변경 사항을 모니터링하고, 실행 중인 앱의 충돌을 실시간으로 감지하고, 채널(Slack, Discord 등)에 즉시 알림을 게시하도록 도와주는 서비스이다. 지난 kOps 스터디 때도 다뤘던 서비스이다.
나는 스터디에서 제공한 Slack 채널이 아닌 별도 관리하는 채널에 테스트를 진행할 예정이다.

일전에 만들어둔 WebHookURL을 입력해서 Configmap을 생성하고 kwatch 배포를 진행했다. 배포가 되면 내 슬랙 채널에 메시지가 뜨는 것을 확인할 수 있다.

# 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: 'MyWebhook URL'
        title: $NICK-EKS
        #text:
    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.3/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

위 내용을 수정해보도록 한다.
kubectl set image pod nginx-19 nginx-pod=nginx:1.19
위 명령어로 잘못된 이미지 태그 정보를 수정했더니 바로 pod가 Running 상태로 변경된 것을 확인할 수 있다.

botkube : k8s 클러스터에서 알림 및 이벤트 관리를 위한 오픈소스 도구로 클러스터의 상태 변화, 오류 및 경고 등을 적시에 감지하고 조치를 취할 수 있는 서비스. 커스터마이즈가 가능하여 각각의 알림 룰을 정의하고 원하는 이벤트 유형에 대한 알림을 선택적으로 받을 수 있다. 이를 통해 클러스터의 상태 및 이벤트를 효과적으로 관리하고 모니터링할 수 있게 된다.

kwatch 때처럼 별도의 슬랙 채널에서 테스트할 예정이라 Slack App을 생성해야 한다. App 생성 방법은 공식 사이트를 참고하였다. https://docs.botkube.io/installation/slack/

# Slack API Token 설정
export SLACK_API_BOT_TOKEN='MySlackBotToken'
export SLACK_API_APP_TOKEN='MySlackAppToken'

# repo 추가
helm repo add botkube https://charts.botkube.io
helm repo update

# 변수 지정
export ALLOW_KUBECTL=true
export ALLOW_HELM=true
export SLACK_CHANNEL_NAME=study

#
cat <<EOT > botkube-values.yaml
actions:
  'describe-created-resource': # kubectl describe
    enabled: true
  'show-logs-on-error': # kubectl logs
    enabled: true

executors:
  k8s-default-tools:
    botkube/helm:
      enabled: true
    botkube/kubectl:
      enabled: true
EOT

# 설치
helm install --version v1.0.0 botkube --namespace botkube --create-namespace \
--set communications.default-group.socketSlack.enabled=true \
--set communications.default-group.socketSlack.channels.default.name=${SLACK_CHANNEL_NAME} \
--set communications.default-group.socketSlack.appToken=${SLACK_API_APP_TOKEN} \
--set communications.default-group.socketSlack.botToken=${SLACK_API_BOT_TOKEN} \
--set settings.clusterName=${CLUSTER_NAME} \
--set 'executors.k8s-default-tools.botkube/kubectl.enabled'=${ALLOW_KUBECTL} \
--set 'executors.k8s-default-tools.botkube/helm.enabled'=${ALLOW_HELM} \
-f botkube-values.yaml botkube/botkube

# 참고 : 삭제 시
helm uninstall botkube --namespace botkube

설치가 잘 됐다면 앱 표시에 Botkube가 생성된 것을 확인할 수 있고 채널에 들어가서 초대를 하면 (이상한 게 디폴트 채널을 설정해놔도 바로 입장하지 않았다.) 안내메시지를 호출해준다.

botkube가 제대로 설치되었으니 이제 테스트를 진행해보도록 한다.
슬랙 채널에서 아래 메시지들을 입력하면 botkube가 응답해주는 것을 볼 수 있다. 직접 터미널을 통해 접속하기 어려운 환경일 때 간단한 명령어 등으로 현황을 확인할 수 있는 좋은 방법이라고 생각이 든다. 물론 보안적인 문제는 있을 수 있으니 비공개채널에 만들어서 사용하는 게 좋을 것 같다.

# 연결 상태, notifications 상태 확인
@Botkube ping
@Botkube status notifications

# 파드 정보 조회
@Botkube k get pod
@Botkube kc get pod --namespace kube-system
@Botkube kubectl get pod --namespace kube-system -o wide

# Actionable notifications
@Botkube kubectl

kwatch에서의 테스트처럼 일부러 잘못 된 이미지를 배포해서 알림을 받아보도록 한다.
배포를 진행하면 바로 알림이 오는 것을 알 수 있다. 알림의 속도는 kwatch보다 빠른 것 같다.

5. Prometheus-stack

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

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

Prometheus-stack : kube-prometheus-stack은 Kubernetes 매니페스트, Grafana 대시보드, 문서 및 스크립트와 결합된 Prometheus 규칙을 수집하여 Prometheus Operator를 사용하여 Prometheus에서 엔드 투 엔드 Kubernetes 클러스터 모니터링을 쉽게 운영할 수 있다.
Prometheus-stack을 설치해보도록 한다.

# 모니터링 : 터미널 새로 띄워서 확인
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"

  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: myapssword

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

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

# alertmanager:
#   ingress:
#     enabled: true
#     ingressClassName: alb
#     hosts: 
#       - alertmanager.$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'
EOT
cat monitor-values.yaml | yh

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

# 확인
## alertmanager-0 : 사전에 정의한 정책 기반(예: 노드 다운, 파드 Pending 등)으로 시스템 경고 메시지를 생성 후 경보 채널(슬랙 등)로 전송
## grafana : 프로메테우스는 메트릭 정보를 저장하는 용도로 사용하며, 그라파나로 시각화 처리
## prometheus-0 : 모니터링 대상이 되는 파드는 ‘exporter’라는 별도의 사이드카 형식의 파드에서 모니터링 메트릭을 노출, pull 방식으로 가져와 내부의 시계열 데이터베이스에 저장
## node-exporter : 노드익스포터는 물리 노드에 대한 자원 사용량(네트워크, 스토리지 등 전체) 정보를 메트릭 형태로 변경하여 노출
## operator : 시스템 경고 메시지 정책(prometheus rule), 애플리케이션 모니터링 대상 추가 등의 작업을 편리하게 할수 있게 CRD 지원
## kube-state-metrics : 쿠버네티스의 클러스터의 상태(kube-state)를 메트릭으로 변환하는 파드
helm list -n monitoring
NAME                 	NAMESPACE 	REVISION	UPDATED                                	STATUS  	CHART                        	APP VERSION
kube-prometheus-stack	monitoring	1       	2023-05-20 14:40:34.552674361 +0900 KST	deployed	kube-prometheus-stack-45.27.2	v0.65.1

kubectl get pod,svc,ingress -n monitoring
NAME                                                            READY   STATUS    RESTARTS   AGE
pod/kube-prometheus-stack-grafana-665bb8df8f-547f2              3/3     Running   0          51s
pod/kube-prometheus-stack-kube-state-metrics-5d6578867c-p9g6b   1/1     Running   0          51s
pod/kube-prometheus-stack-operator-74d474b47b-br78c             1/1     Running   0          51s
...

kubectl get-all -n monitoring
NAME                                                                                 NAMESPACE   AGE
configmap/kube-prometheus-stack-alertmanager-overview                                monitoring  68s
configmap/kube-prometheus-stack-apiserver                                            monitoring  68s
configmap/kube-prometheus-stack-cluster-total                                        monitoring  68s
configmap/kube-prometheus-stack-grafana                                              monitoring  68s
...

kubectl get prometheus,servicemonitors -n monitoring
NAME                                                                VERSION   DESIRED   READY   RECONCILED   AVAILABLE   AGE
prometheus.monitoring.coreos.com/kube-prometheus-stack-prometheus   v2.42.0   1         1       True         True        80s

NAME                                                                                  AGE
servicemonitor.monitoring.coreos.com/kube-prometheus-stack-apiserver                  80s
servicemonitor.monitoring.coreos.com/kube-prometheus-stack-coredns                    80s
servicemonitor.monitoring.coreos.com/kube-prometheus-stack-grafana                    80s
...

kubectl get crd | grep monitoring
(k8sadmin@myeks:default) [root@myeks-bastion-EC2 ~]# kubectl get crd | grep monitoring
alertmanagerconfigs.monitoring.coreos.com    2023-05-20T05:40:32Z
alertmanagers.monitoring.coreos.com          2023-05-20T05:40:32Z
podmonitors.monitoring.coreos.com            2023-05-20T05:40:32Z
probes.monitoring.coreos.com                 2023-05-20T05:40:32Z
prometheuses.monitoring.coreos.com           2023-05-20T05:40:33Z
prometheusrules.monitoring.coreos.com        2023-05-20T05:40:33Z
servicemonitors.monitoring.coreos.com        2023-05-20T05:40:33Z
thanosrulers.monitoring.coreos.com           2023-05-20T05:40:33Z

설치하고 ALB을 확인하면 하나의 리스너(443)에 다수의 규칙이 생성 된 것을 확인할 수 있다. 이게 가능한 이유는 배포 yaml 파일의 anontation 부부분에 아래와 같이 group.name을 study로 다 통일시켰기 때문이다.

  alb.ingress.kubernetes.io/group.name: study

이렇게 되면 ALB 자원은 하나만 생성 및 관리해도 되기 때문에 편리하다. 동일한 목적과 수명주기를 갖고 관리하는 서비스들은 위와 같은 방법으로 생성하는 것이 좋을 것ㄱ ㅏㅌ다.

프로메테우스의 모니터링 대상이 되는 서비스는 일반적으로 자체 웹 서버의 /metrics 경로에 다양한 메트릭 정보를 노출한다. 이후 프로메테우스는 해당 경로에 HTTP GET 호출을 하게 되고 TSDB 형식으로 데이터를 저장한다.

ALB 리스너에서 도메인 주소도 확인했다면 프로메테우스에 접속해서 간단하게 확인해보도록 한다. 아래와 같이 데이터가 잘 보인다면 수집 및 출력이 잘 되는 것이다.

6. Grafana

그라파나는 위에서 설명한 프로메테우스가 수집하고 TSDB로 저장한 데이터를 시각화하는 서비스이다. 시각화 솔루션이기 때문에 자체적으로 데이터를 저장하지는 않고 Datasource라고 불리는 수집 데이터를 연결하여 시각화해주는 기능을 제공한다.

프로메테우스 스택으로 설치했기 때문에 그라파나에는 기본적으로 프로메테우스가 Datasource로 추가되어 있다. 별도로 추가하지 않아도 그라파나에서는 위에서 설치한 프로메테우스의 저장 데이터를 불러올 수 있다는 뜻이다.


그라파나에서 확인한 프로메테우스 서비스 주소를 통해 방금 생성한 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
Server:		10.100.0.10
Address:	10.100.0.10#53
Name:	kube-prometheus-stack-prometheus.monitoring.svc.cluster.local
Address: 10.100.32.51

kubectl exec -it netshoot-pod -- curl -s kube-prometheus-stack-prometheus.monitoring:9090/graph -v ; echo
*   Trying 10.100.32.51:9090...
* Connected to kube-prometheus-stack-prometheus.monitoring (10.100.32.51) port 9090 (#0)
> GET /graph HTTP/1.1
> Host: kube-prometheus-stack-prometheus.monitoring:9090
> User-Agent: curl/8.0.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sat, 20 May 2023 06:01:06 GMT
< Content-Length: 734
< Content-Type: text/html; charset=utf-8
<
* Connection #0 to host kube-prometheus-stack-prometheus.monitoring left intact
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/><meta name="theme-color" content="#000000"/><script>const GLOBAL_CONSOLES_LINK="",GLOBAL_AGENT_MODE="false",GLOBAL_READY="true"</script><link rel="manifest" href="./manifest.json" crossorigin="use-credentials"/><title>Prometheus Time Series Collection and Processing Server</title><script defer="defer" src="./static/js/main.c1286cb7.js"></script><link href="./static/css/main.cb2558a0.css" rel="stylesheet"></head><body class="bootstrap"><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

# 삭제
kubectl delete pod netshoot-pod

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

Dashboard->Import을 선택한다.

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

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

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

Application 대쉬보드를 테스트해보기 위해 nginx을 배포해서 테스트해볼 예정이다.

# 모니터링
watch -d kubectl get pod

# 파라미터 파일 생성 : 서비스 모니터 방식으로 nginx 모니터링 대상을 등록하고, export 는 9113 포트 사용, nginx 웹서버 노출은 AWS CLB 기본 사용
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
NAME                         READY   STATUS    RESTARTS   AGE
pod/nginx-85fc957979-lp8tk   2/2     Running   0          29s
NAME                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                       AGE
service/kubernetes   ClusterIP   10.100.0.1       <none>        443/TCP                       5d4h
service/nginx        NodePort    10.100.212.109   <none>        80:31864/TCP,9113:31740/TCP   134m
NAME                   ENDPOINTS                               AGE
endpoints/kubernetes   192.168.1.42:443,192.168.2.209:443      5d4h
endpoints/nginx        192.168.2.105:9113,192.168.2.105:8080   134m

kubectl get servicemonitor -n monitoring nginx
NAME    AGE
nginx   44s

kubectl get servicemonitor -n monitoring nginx -o json | jq
{
  "apiVersion": "monitoring.coreos.com/v1",
  "kind": "ServiceMonitor",
  "metadata": {
    "annotations": {
      "meta.helm.sh/release-name": "nginx",
      "meta.helm.sh/release-namespace": "default"
...

# 메트릭 확인 >> 프로메테우스에서 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
NAME                     READY   STATUS    RESTARTS   AGE
nginx-85fc957979-lp8tk   2/2     Running   0          2m20s

kubectl describe pod -l app.kubernetes.io/instance=nginx
Name:             nginx-85fc957979-lp8tk
Namespace:        default
Priority:         0
Service Account:  default
Node:             ip-192-168-2-82.ap-northeast-2.compute.internal/192.168.2.82
Start Time:       Sat, 20 May 2023 15:08:18 +0900
...

# 접속 주소 확인 및 접속
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

프로메테우스에서도 nginx에 대한 메트릭 정보를 확인할 수 있고 해당 데이터를 그래프로 표현해서 볼 수도 있다.

메트릭 수집이 정상적으로 되는 것을 확인했으니 이를 Grafana에서 확인해보도록 하겠다.
위에서 Dashboard을 Import할 때와 같은 방식으로 Import ID는 12708을 사용해서 생성한 대쉬보드이다. 요청 수나 Active Connection 수 등을 확인할 수 있다. 앞서 Prometheus에서 확인한 Connections_active와 동일한 모양임을 알 수 있다.


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

7. kubecost

kubecost : k8s 클러스터의 비용 분석 및 관리를 위한 오픈 소스 도구로 Kubecost를 사용하면 Kubernetes 클러스터의 리소스 사용량, 비용, 예상 비용 등을 실시간으로 모니터링하고 분석할 수 있다.

https://docs.kubecost.com/install-and-configure/install/provider-installations/aws-eks-cost-monitoring

kubecost을 설치한다. 다만, 설치를 하더라도 bastion 환경에서 바로 대쉬보드를 볼 수는 없기 때문에 이를 Loadbalancer에 연결하는 작업을 이후에 진행해주었다.

# kubecost 설치 진행 : 버전은 https://gallery.ecr.aws/kubecost/cost-analyzer 에서 최신 버전 확인
export VERSION="1.103.3"
helm upgrade -i kubecost \
oci://public.ecr.aws/kubecost/cost-analyzer --version="$VERSION" \
--namespace kubecost --create-namespace \
-f https://raw.githubusercontent.com/kubecost/cost-analyzer-helm-chart/develop/cost-analyzer/values-eks-cost-monitoring.yaml \
--set prometheus.configmapReload.prometheus.enabled="false"

# 설치 상태 확인
kubectl get pod -n kubecost
NAME                                          READY   STATUS    RESTARTS   AGE
kubecost-cost-analyzer-9f75ffc6b-9dww8        2/2     Running   0          80s
kubecost-kube-state-metrics-d6d9b7594-hwzgd   1/1     Running   0          80s
kubecost-prometheus-server-7755c9b669-l8hs5   1/1     Running   0          80s

# LoadBalancer에 연결하여 업데이트
cat <<'EOF' |
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kubecost-alb-ingress
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/scheme: internet-facing
spec:
  rules:
    - http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: kubecost-cost-analyzer
              port:
                number: 9090
EOF
(export NAMESPACE=kubecost && kubectl apply -n $NAMESPACE -f -)

# DNS 연결
export ENDPOINT=$(kubectl get ingress kubecost-alb-ingress -n kubecost --output jsonpath='{.status.loadBalancer.ingress[0].hostname}')
echo "Kubecost UI DNS name: ${ENDPOINT}"
Kubecost UI DNS name: k8s-kubecost-kubecost-7931dc7581-278425107.ap-northeast-2.elb.amazonaws.com

kubectl annotate ingress kubecost-alb-ingress -n kubecost "external-dns.alpha.kubernetes.io/hostname=kubecost.$MyDomain"
ingress.networking.k8s.io/kubecost-alb-ingress annotated
echo -e "kubecost URL = http://kubecost.$MyDomain"
kubecost URL = http://kubecost.bs-yang.com

kubecost 설치와 Loadbalancer 연결이 제대로 됐다면 아래와 같이 대쉬보드가 호출되는 것을 알 수 있다.

클러스터 배포와 kubecost 수집 자체가 얼마 되지 않아서 자세한 데이터들을 볼 수는 없지만 화면을 여기저기 클릭하면서 데이터를 확인할 수 있었다.

8. 정리

kOps에서 다뤘던 내용들이 있어서 이번에는 많이 어렵지 않게 실습을 진행할 수 있었다. 그라파나에서 이미지를 포함한 알림을 보내고 싶었지만 사용하고 있는 계정이 회사 계정이라 정책상 S3 Public Access을 허용할 수 없어서 테스트하지 못해 아쉽다. 추후 개인계정에서 테스트해볼 수 있으면 좋을 것 같다.

AEWS Study #3 – EKS Storage & Node 관리

AEWS 3회차는 EKS Storage와 Node 관리에 대한 내용을 다룬다.

0. 환경 구성

이번에도 스터디에서 제공 된 One Click 배포를 사용하여 환경을 구성한다.
이번에 새롭게 조금 수정 된 yaml 파일을 다운로드 받고 배포 스크립트를 실행하면 잠시의 시간이 지난 뒤 Cloudformation에서 모든 배포가 완료된 것을 확인할 수 있다.

# 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=aewspair SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32  MyIamUserAccessKeyID=AKIA5... MyIamUserSecretAccessKey='CVNa2...' 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 aewspair.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)

배포 완료를 확인하고 AWS LB Controller, ExternalDNS와 kube-ops-view을 설치하여 환경 구성을 마무리한다.
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=bs-yang.com
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 Storage?

k8s에서 pod의 데이터는 pod가 정지되면 모두 삭제되게 되어있다. (stateless application)

하지만 pod에 데이터베이스를 올릴 때도 있고 데이터가 존속되어야 하는 경우도 필요하다. (stateful application) 이럴 때 사용하는 방식이 PV/PVC이다.

Pod가 생성될 때 자동으로 볼륨을 Pod에 마운틑하여 PV/PVC을 사용할 수 있게끔 하는 방식이 동적 프로비저닝(Dynamic provisioning)이라고 한다.

stateless 환경으로 배포하는 것을 테스트해보려고 한다.
data 명령으로 현재 시간을 10초 간격으로 out file하는 pod을 배포해서 시간이 찍히는 것을 확인하고 해당 pod을 삭제 후 재배포한 뒤 이전 기록이 남아있는지 확인해볼 예정이다.


위 사진을 보면 첫 사진의 시간대는 06:54:29부터 시작인데 삭제 후 재배포 된 pod에서는 06:55:21부터 시작하는 것을 확인할 수 있다. 이제 이 부분을 host path을 사용하는 PV/PVC을 통해 statefull 환경으로 배포를 해볼 예정이다.

# 배포
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
NAME                                                   NAMESPACE           AGE
configmap/kube-root-ca.crt                             local-path-storage  7s
configmap/local-path-config                            local-path-storage  7s
pod/local-path-provisioner-759f6bd7c9-rqh7l            local-path-storage  7s
serviceaccount/default                                 local-path-storage  7s
serviceaccount/local-path-provisioner-service-account  local-path-storage  7s
deployment.apps/local-path-provisioner                 local-path-storage  7s
replicaset.apps/local-path-provisioner-759f6bd7c9      local-path-storage  7s

kubectl get pod -n local-path-storage -owide
NAME                                      READY   STATUS    RESTARTS   AGE   IP             NODE                                               NOMINATED NODE   READINESS GATES
local-path-provisioner-759f6bd7c9-rqh7l   1/1     Running   0          12s   192.168.1.29   ip-192-168-1-231.ap-northeast-2.compute.internal   <none>           <none>

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                  84s

PV/PVC를 사용하는 pod을 배포하고 해당 pod에서 파일을 조회하고 해당 pod가 배포되어 있는 Worker node에서도 local path을 사용해서 파일을 조회해본다.
둘 다 동일한 결과값을 갖고있는 것을 확인할 수 있다.

# 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
Thu May 11 07:00:04 UTC 2023
Thu May 11 07:00:09 UTC 2023
... 

# 워커노드 중 현재 파드가 배포되어 있다만, 아래 경로에 out.txt 파일 존재 확인
ssh ec2-user@$N2 tree /opt/local-path-provisioner
/opt/local-path-provisioner
└── pvc-6cfbd87b-10f7-49dd-ad95-a65b7ab4f3f9_default_localpath-claim
    └── out.txt

# 해당 워커노드 자체에서 out.txt 파일 확인 : 아래 굵은 부분은 각자 실습 환경에 따라 다름
ssh ec2-user@$N2 tail -f /opt/local-path-provisioner/pvc-6cfbd87b-10f7-49dd-ad95-a65b7ab4f3f9_default_localpath-claim/out.txt
Thu May 11 07:01:34 UTC 2023
Thu May 11 07:01:39 UTC 2023
... 

이제 해당 pod을 삭제했을 때도 파일이 남아있는지 그리고 재배포를 했을 때도 동일하게 파일을 조회할 수 있는지 확인해보도록 하겠다.
Local Path에도 그대로 남아있고 Pod을 새로 배포했을 때도 동일하게 파일이 조회되는 것을 확인할 수 있다.

# 파드 삭제 후 PV/PVC 확인
kubectl delete pod app
kubectl get pod,pv,pvc
ssh ec2-user@$N2 tree /opt/local-path-provisioner
/opt/local-path-provisioner
└── pvc-6cfbd87b-10f7-49dd-ad95-a65b7ab4f3f9_default_localpath-claim
    └── out.txt

# 파드 다시 실행
kubectl apply -f localpath2.yaml
 
# 확인
kubectl exec -it app -- head /data/out.txt
kubectl exec -it app -- tail -f /data/out.txt

2. AWS EBS Controller

CSI(Container Storage Interface)는의 컨테이너와 스토리지 시스템 간의 통합을 표준화하기 위한 인터페이스로 CSI를 사용하면 컨테이너 Kubernetes과 스토리지 공급자 간의 인터페이스를 표준화하여, 서로 다른 스토리지 시스템과 컨테이너 오케스트레이션 시스템 간의 호환성을 보장할 수 있다.

이전에는 Kubernetes가 FlexVolume과 같은 사용자 정의 볼륨 플러그인을 사용하여 컨테이너와 스토리지 시스템 간의 인터페이스를 구현했지만 FlexVolume은 이식성과 유연성 측면에서 한계가 있었기 때문에, CSI가 도입되면서 대체되었다.

CSI는 다음과 같은 이점을 제공합니다.

  • 유연성: 스토리지 시스템과 컨테이너 오케스트레이션 시스템 간의 인터페이스를 표준화하여, 서로 다른 스토리지 시스템과 컨테이너 오케스트레이션 시스템 간의 호환성 보장
  • 이식성: CSI 스펙을 준수하는 스토리지 공급자는 어떤 컨테이너 오케스트레이션 시스템에서도 사용 가능
  • 모듈성: CSI는 별도의 드라이버를 개발하지 않아도 되기 때문에, 스토리지 공급자는 CSI 스펙을 준수하는 드라이버만 개발 가능

EBS CSI driver 동작은 볼륨을 생성하고 Pod에 해당 볼륨을 연결하는 동작이다.
persistentvolume, persistentvolumeclaim의 accessModes는 ReadWriteOnce로 설정해야 하는데 그 이유는 데이터 일관성과 무결성을 유지하기 위함이다. 여러 Node에서 해당 스토리지를 마운트하고 동시에 Write 작업을 수행하게 될 경우 데이터의 무결성이 보장되지 않을 수 있기 때문에 ReadWriteOnce AccessMode을 사용하여 데이터 일관성과 무결성을 유지해야 한다.
그리고 EBS 스토리지 기본 설정은 동일 AZ에 있는 EC2인스턴스와 그 Pod에 연결되는데 그 이유는 데이터 전송 속도가 빨라지고 더 높은 I/O 처리량을 얻을 수 있기 때문이다. 비용적인 측면에서도 동일 AZ에서의 데이터 전송 네트워크 대역폭 비용이 발생하지 않기 때문에 유리한 부분이 있다. 그리고 위의 AccessMode와 마찬가지로 데이터 일관성을 더 쉽게 보장 받을 수 있기 때문에 동일 AZ에 연결하게끔 되어 있다고 볼 수 있다.

EBS Controller을 설치하는 건 하기 내용을 통해 진행할 수 있다.

# 아래는 aws-ebs-csi-driver 전체 버전 정보와 기본 설치 버전(True) 정보 확인
aws eks describe-addon-versions \
    --addon-name aws-ebs-csi-driver \
    --kubernetes-version 1.24 \
    --query "addons[].addonVersions[].[addonVersion, compatibilities[].defaultVersion]" \
    --output text
v1.18.0-eksbuild.1
Tru
v1.17.0-eksbuild.1
False
...

# 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 확인
kubectl get sa -n kube-system ebs-csi-controller-sa -o yaml | head -5
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

# 확인
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 스토리지 클래스 생성
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
  allowAutoIOPSPerGBIncrease: 'true'
  encrypted: 'true'
  #fsType: ext4 # 기본값이 ext4 이며 xfs 등 변경 가능 >> 단 스냅샷 경우 ext4를 기본으로하여 동작하여 xfs 사용 시 문제가 될 수 있음 - 테스트해보자
EOT
kubectl apply -f gp3-sc.yaml
kubectl get sc
kubectl describe sc gp3 | grep Parameters

3. AWS Volume SnapShots Controller


Volume Snapshots Controller는 Kubernetes 클러스터 내에서 스냅샷을 생성하고 복원하기 위한 컨트롤이다. 이 컨트롤러는 Kubernetes Volume Snapshot API를 사용하여 스냅샷을 관리한다.
Volume Snapshot API를 사용하면 스토리지 클래스에서 스냅샷을 지원하는 경우 스냅샷을 생성할 수 있으며, 이를 사용하여 데이터를 백업하거나 특정 시점의 데이터로 복원할 수 있다.
Kubernetes Volume Snapshots Controller는 스냅샷을 생성하고 복원하기 위한 작업을 수행하고 스냅샷 생성을 위해서는 PVC(Persistent Volume Claim)을 사용하여 스냅샷 대상이 되는 볼륨을 식별하고 이를 기반으로 스냅샷을 생성한다. 스냅샷 생성 후에는 해당 스냅샷을 복원하여 이전 데이터를 다시 가져올 수 있다.
Kubernetes Volume Snapshots Controller는 또한 스냅샷 수명 주기 관리를 지원한다. 이를 통해 스냅샷을 자동으로 삭제하거나 보존할 수 있다. 스냅샷을 자동으로 삭제하면 비용을 절감하고 클러스터 용량을 확보할 수 있다. 반면에 스냅샷을 보존하면 장애 복구 및 데이터 분석 등에 유용하니 환경에 따라 유동적으로 관리할 수 있다.
정리하면 Kubernetes Volume Snapshots Controller를 사용하면 스토리지 클래스에서 스냅샷을 지원하는 경우 PVC를 사용하여 스냅샷을 생성하고 복원할 수 있고, 스냅샷 수명 주기 관리를 통해 비용을 절감하고 데이터를 보존할 수 있다.

VolumeSnapshotController을 설치하고 테스트 PVC/Pod을 통해 테스트를 진행해 볼 생각이다.
스냅샷을 생성하고 Pod와 PVC을 제거한 뒤 만들어둔 스냅샷을 통해 복원하는 과정으로 진행 된다.

# 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
volumesnapshotclasses.snapshot.storage.k8s.io    2023-05-12T03:46:15Z
volumesnapshotcontents.snapshot.storage.k8s.io   2023-05-12T03:46:15Z
volumesnapshots.snapshot.storage.k8s.io          2023-05-12T03:46:15Z
kubectl api-resources  | grep snapshot
volumesnapshotclasses             vsclass,vsclasses   snapshot.storage.k8s.io/v1             false        VolumeSnapshotClass
volumesnapshotcontents            vsc,vscs            snapshot.storage.k8s.io/v1             false        VolumeSnapshotContent
volumesnapshots                   vs                  snapshot.storage.k8s.io/v1             true         VolumeSnapshot

# 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
serviceaccount/snapshot-controller created
clusterrole.rbac.authorization.k8s.io/snapshot-controller-runner created
clusterrolebinding.rbac.authorization.k8s.io/snapshot-controller-role created
role.rbac.authorization.k8s.io/snapshot-controller-leaderelection created
rolebinding.rbac.authorization.k8s.io/snapshot-controller-leaderelection created
deployment.apps/snapshot-controller created

kubectl get deploy -n kube-system snapshot-controller
NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
snapshot-controller   2/2     2            0           19s

kubectl get pod -n kube-system -l app=snapshot-controller
NAME                                   READY   STATUS    RESTARTS   AGE
snapshot-controller-76494bf6c9-j8t2x   1/1     Running   0          42s
snapshot-controller-76494bf6c9-z7275   1/1     Running   0          42s

# 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 yaml
cat <<EOT > awsebs-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ebs-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
  storageClassName: gp3
EOT

# Pod yaml
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

# 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
NAME                                               READYTOUSE   RESTORESIZE   DELETIONPOLICY   DRIVER            VOLUMESNAPSHOTCLASS   VOLUMESNAPSHOT        VOLUMESNAPSHOTNAMESPACE   AGE
snapcontent-3bed7592-e760-42fc-8f2c-eb59d0b6714f   false        4294967296    Delete           ebs.csi.aws.com   csi-aws-vsc           ebs-volume-snapshot   default                   16s

# 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


Snapshot 생성을 확인하고 App/PVC 삭제를 진행했다.
이후 스냅샷을 통해 복원한 후 기존 내용이 그대로 유지되는지 확인하도록 한다.
Pod의 특정 경로 /data/out.txt을 조회했을 때 위의 삭제 된 Pod에서 기존에 조회한 내용과 동일한 내용이 저장되어 있는 것을 확인할 수 있다.

# 스냅샷에서 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

# 파일 내용 저장 확인 : 파드 삭제 전까지의 저장 기록이 남아 있다. 이후 파드 재생성 후 기록도 잘 저장되고 있다
kubectl exec app -- cat /data/out.txt
Fri May 12 04:55:23 UTC 2023
Fri May 12 04:55:28 UTC 2023
Fri May 12 04:55:33 UTC 2023
Fri May 12 04:55:38 UTC 2023
Fri May 12 04:55:43 UTC 2023
Fri May 12 04:55:48 UTC 2023
...

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

4. AWS EFS Controller

EFS Controller는 k8s에서 EFS을 마운트해서 사용하기 위해 사용하는 Controller이다.
EKS EFS Controller를 사용하면 EFS 파일 시스템을 생성하고, 해당 파일 시스템에서 EFS 볼륨을 동적으로 프로비저닝하고, 해당 볼륨을 파드에 마운트할 수 있다.
EKS EFS Controller를 사용하려면, 먼저 EFS 파일 시스템을 생성하고, 해당 파일 시스템에서 EFS 볼륨을 동적으로 프로비저닝할 수 있는 권한을 갖는 IAM 역할이 있어야 한다. 그런 다음, EKS 클러스터에 EFS CSI 드라이버를 설치하고, EKS EFS Controller를 설치하면 된다.

EKS EFS Controller를 사용하면 다음과 같은 이점이 있다.

  • EFS 파일 시스템에서 동적으로 프로비저닝된 EFS 볼륨 관리 편리
  • EFS 파일 시스템에 대한 별도의 설정 불필요
  • EKS 클러스터에서 파일 시스템을 생성하고 EFS 볼륨을 동적으로 프로비저닝하기 위한 AWS 리소스를 관리 불필요
  • EFS 파일 시스템과 EFS 볼륨에 대한 권한 관리 간편

EFS Controller을 설치하고 EFS Filesystem을 만든 뒤 해당 파일 시스템을 다수의 Pod가 사용할 수 있도록 설정하는 테스트를 진행할 예정이다.

우선 아래 내용을 참고해서 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=602401143452.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 파일시스템을 다수의 Pod가 사용하도록 설정을 해서 배포한다.
pod1은 out1.txt에 pod2는 out2.txt에 각각 시간을 입력하는 작업을 진행한다.
Bastion에서 Pod에 연결 된 EFS을 마운트해서 로컬에서도 파일이 동일하게 열리는지 확인하였다.
아래 캡쳐화면들을 보면 Local과 각 Pod에서 문제없이 파일을 불러올 수 있는 것을 확인할 수 있다.

# 모니터링
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

# EFS 확인 : AWS 관리콘솔 EFS 확인해보자
mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport $EfsFsId.efs.ap-northeast-2.amazonaws.com:/ /mnt/myefs
df -hT --type nfs4
mount | grep nfs4

cat pv.yaml | yh

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

5. Deploying WordPress and MySQL with Persistent Volumes

Persistent Volume을 사용하는 WorkPress와 MySQL을 배포하는 실습을 진행해보려고 한다. PV을 사용하지 않을 경우 Pod가 Node의 장애로 인해 죽게 될 경우 데이터가 유실되기 때문에 PV을 사용하는 WordPress와 MySQL을 배포해보려고 한다.
실습은 https://kubernetes.io/docs/tutorials/stateful-application/mysql-wordpress-persistent-volume/ 해당 링크를 참고하였다.

deployment yaml 파일을 다운로드 받는 작업으로 시작한다.

#workdpress, mysql deployment yaml Download
curl -s -O https://kubernetes.io/examples/application/wordpress/mysql-deployment.yaml
curl -s -O https://kubernetes.io/examples/application/wordpress/wordpress-deployment.yaml

cat mysql-deployment.yaml | yh
apiVersion: v1
kind: Service
metadata:
  name: wordpress-mysql
  labels:
    app: wordpress
spec:
  ports:
    - port: 3306
  selector:
    app: wordpress
    tier: mysql
  clusterIP: None
...

cat wordpress-deployment.yaml | yh
apiVersion: v1
kind: Service
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  ports:
    - port: 80
  selector:
    app: wordpress
    tier: frontend
  type: LoadBalancer
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wp-pv-claim
  labels:
    app: wordpress
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
...

kustomization.yaml 파일을 생성한다. 여기서 kustomization은 Kubernetes 클러스터에서 배포하려는 리소스들을 커스터마이징하는 방법을 제공하는 도구로 kustomization.yaml 파일을 통해 배포하고자 하는 리소스들의 목록과 각 리소스별로 커스터마이징을 수행하는 설정을 지정할 수 있다.
Kustomization은 다음과 같은 기능을 갖고 있다.

  • 기존 manifest 파일을 수정하지 않고, 수정된 내용을 적용할 수 있다.
  • 배포 시에 여러 개의 환경(예: dev, stage, prod)에 대한 다른 설정을 지정할 수 있다.
  • 배포할 리소스를 기반으로 자동으로 이름을 생성하여, 리소스 이름 충돌을 방지한다.

kustomization.yaml에 secret manager을 추가해서 mysql secret을 생성할 계획이다.

cat <<EOF >./kustomization.yaml
secretGenerator:
- name: mysql-pass
  literals:
  - password=YOUR_PASSWORD
resources:
  - mysql-deployment.yaml
  - wordpress-deployment.yaml
EOF

kustomization.yaml 파일에 Resources 정보를 담았기 때문에 kustomization.yaml 파일과 deployment yaml을 같은 폴더에 두고 배포를 진행할 예정이다.
deployment yaml 파일들을 보면 pvc를 20Gi로 생성하는 것으로 되어있는데 테스트 목적이기 때문에 4Gi로 변경하고 Internet Facing NLB을 생성하기 위해 wordpress deployment을 일부분 수정하였다.

# Internet Facing NLB 생성을 위해 wordpress deployment yaml 파일 수정
apiVersion: v1
kind: Service
metadata:
  name: wordpress
  labels:
    app: wordpress
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: nlb
    service.beta.kubernetes.io/aws-load-balancer-internal: "false"
spec:
  ports:
    - port: 80
      targetPort: 80

# 배포 진행
kubectl apply -k ./
secret/mysql-pass-2ffhd9htbk unchanged
service/wordpress unchanged
service/wordpress-mysql unchanged
persistentvolumeclaim/mysql-pv-claim created
persistentvolumeclaim/wp-pv-claim created
deployment.apps/wordpress created
deployment.apps/wordpress-mysql created

# secret 생성 확인
kubectl get secrets
NAME                    TYPE     DATA   AGE
mysql-pass-2ffhd9htbk   Opaque   1      8m9s

# PVC가 Dynamic Provisioning 되는지 확인
kubectl get pvc, pv
NAME                                   STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/mysql-pv-claim   Bound    pvc-14773585-e62e-47df-884a-4415a1961d7a   4Gi        RWO            gp2            97s
persistentvolumeclaim/wp-pv-claim      Bound    pvc-1652c71f-ffd7-423a-8632-c59a2f3ec44c   4Gi        RWO            gp2            97s
NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                    STORAGECLASS   REASON   AGE
persistentvolume/pvc-14773585-e62e-47df-884a-4415a1961d7a   4Gi        RWO            Delete           Bound    default/mysql-pv-claim   gp2                     93s
persistentvolume/pvc-1652c71f-ffd7-423a-8632-c59a2f3ec44c   4Gi        RWO            Delete           Bound    default/wp-pv-claim      gp2                     93s

# Pod 정보 확인
kubectl get pods
NAME                               READY   STATUS    RESTARTS   AGE
wordpress-664bfdc845-nmh8p         1/1     Running   0          4m
wordpress-mysql-85648459b5-qzcwj   1/1     Running   0          4m

# Service 확인
kubectl get services wordpress
NAME        TYPE           CLUSTER-IP      EXTERNAL-IP                                                                         PORT(S)        AGE
wordpress   LoadBalancer   10.100.62.174   k8s-default-wordpres-d30dc22441-01684caff9c44457.elb.ap-northeast-2.amazonaws.com   80:30479/TCP   12m

# Service NLB을 Domain에 연결
kubectl annotate service wordpress -n default "external-dns.alpha.kubernetes.io/hostname=wptest.$MyDomain"
echo -e "Wordpress Test URL = http://wptest.$MyDomain"

설치 후 NLB가 연결 된 Domain을 호출하여 WordPress 설치페이지에 접속하였다.
이후 kustomization.yaml 에서 설정한 암호를 이용해서 wordpress 설치를 진행해봤다.
설치는 무리 없이 잘 설치되었고 이를 통해 mysql password 설정 또한 제대로 됐음을 알 수 있다. (mysql password 설정에 문제가 있다면 wordpress 설치가 정상적으로 되지 않기 때문에)

해당 실습이 종료됐으니 삭제를 진행한다.

# 삭제 진행
kubectl delete -k ./
secret "mysql-pass-2ffhd9htbk" deleted
service "wordpress" deleted
service "wordpress-mysql" deleted
persistentvolumeclaim "mysql-pv-claim" deleted
persistentvolumeclaim "wp-pv-claim" deleted
deployment.apps "wordpress" deleted
deployment.apps "wordpress-mysql" deleted

6. 정리

Storage의 경우 kOps 실습 때와 크게 다르지 않았지만 wordpress 배포를 통해 조금 더 상세한 내용을 테스트해볼 수 있어서 좋았다.
컨테이너로 db 등은 운영하지 않던 예전과 다르게 최근엔 db을 운영하기도 한다고 하니 앞으로 이 부분을 잘 공부해두면 좋을 것 같다.