쿠버네티스를 운영하면서 사용하는 kubectl 버전은 운영하는 Kubernetes 클러스터 버전과 호환되어야 한다. 이 내용은 Kubernetes 공식 문서 내 버전 차이 정책(Version Skew Policy)에서 확인할 수 있으며, Amazon EKS 문서에도 명시적으로 언급되어 있다.

kubectl은 kube-apiserver의 한 단계 마이너 버전(이전 또는 최신) 내에서 지원한다.

예:

  • kube-apiserver은 1.32 이다.
  • kubectl은 1.33, 1.32  1.31 을 지원한다.

참고:

HA 클러스터의 kube-apiserver 인스턴스 간에 버전 차이가 있으면 지원되는 kubectl 버전의 범위도 줄어든다.

예:

  • kube-apiserver 인스턴스는 1.32  1.31 이다.
  • kubectl은 1.32  1.31 에서 지원한다(다른 버전은 kube-apiserver 인스턴스 중에 한 단계 이상의 마이너 버전 차이가 난다).

https://kubernetes.io/ko/releases/version-skew-policy/#kubectl

 

버전 차이(skew) 정책

다양한 쿠버네티스 구성 요소 간에 지원되는 최대 버전 차이

kubernetes.io

 

참고

Amazon EKS 클러스터 제어 영역과 마이너 버전이 하나 다른 kubectl 버전을 사용해야 합니다. 예를 들어, 1.30 kubectl 클라이언트는 Kubernetes 1.29, 1.30, 1.31 클러스터와 함께 작동합니다.

https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/install-kubectl.html#kubectl-install-update

 

kubectl 및 eksctl 설정 - Amazon EKS

Amazon EKS 클러스터 제어 영역과 마이너 버전이 하나 다른 kubectl 버전을 사용해야 합니다. 예를 들어, 1.30 kubectl 클라이언트는 Kubernetes 1.29, 1.30, 1.31 클러스터와 함께 작동합니다.

docs.aws.amazon.com

 

그런데 Amazon EKS를 클러스터를 사용하다보면 kubectl 명령어를 실행할 때, `v1.29.0-eks-5e0fdde`와 같은 형식의 버전 문자열을 볼 수 있다 (참고: https://aws.amazon.com/ko/blogs/tech/how-to-upgrade-amazon-eks-worker-nodes-with-karpenter-drift/ ). Amazon EKS 사용자 가이드 - kubectl 및 eksctl 설정 문서에 따르면 이 바이너리는 업스트림 커뮤니티 버전과 동일하다고 언급되어 있다. 이와 같이 동일한 바이너리를 사용하더라도 버전 스트링을 커스터마이징하는 경우가 있는데, 클라우드를 제공하는 프로바이더 등에서 직접 바이너리를 빌드했음을 알려 신뢰성을 확보하거나, 또는 내부 버전 관리 목적을 위해 (예: 특정 환경용 빌드 구분 - `v1.32.0-internal`, 테스트 환경 구분 - `v1.32.0-test`, 개발 단계 구분 - `v1.29.0-dev`) 사용할 수 있겠다.

 

Kubernetes 소스를 직접 활용하여 버전 문자열을 커스터마이징해보자.

 

사전 요구사항 

  • Go 개발 환경: 아래 과정을 사용하면 원하는 kubectl 버전에 맞추어 go를 직접 다운로드하여 바이너리를 빌드하는 과정을 거친다. 해당 Go가 잘 실행될 수 있도록 https://go.dev/wiki/MinimumRequirements 홈페이지에서 언급하는 go 최소 요구 사항을 확인하여 Go 개발 환경을 갖추도록 한다.
  • Git: 이미 git 명령어가 실행되는 환경이라면 추가로 고려하지 않아도 된다. 그렇지 않다면 https://git-scm.com/ 등을 참고하여 git를 설치하도록 한다.
  • Make 도구: 역사가 오래된 도구로 (링크: https://www.gnu.org/software/make/manual/make.html), 소스 코드 내 Makefile 파일을 읽어와 바이너리를 빌드하는 도구이다. "make" 명령어가 실행되지 않는다면 개발 환경 운영체제 및 배포판에 따라 설치하는 가이드를 검색하여 준비한다.

빌드 과정

1. 소스코드 준비

Kubernetes - GitHub 저장소에서 소스를 가져와 해당 디렉토리로 이동한다.

$ git clone https://github.com/kubernetes/kubernetes
$ cd kubernetes

 

2. 기존 버전 태그 확인

아래 명령어를 사용하여 최신 릴리스된 Kubernetes 버전부터 이전 버전까지 목록을 확인할 수 있다. 명령어가 종료되지 않고 : 마크가 앞에 나오는 것을 볼 수 있는데, 스페이스바를 누르면 계속 목록을 볼 수 있으며, q 문자를 누르면 빠져나온다.

$ git tag --sort=-taggerdate

 

 

3. 버전 선택 및 체크아웃

빌드하고자 하는 버전 태그를 로컬 환경 변수에 저장하고, 해당 버전에 대한 소스로 변경하기 위해 "git checkout" 커맨드를 사용한다. 여기서는 TAG라는 이름으로 환경 변수를 사용하여 값을 저장한 다음 $TAG 형태로 해당 값을 사용하여 해당 버전에 대한 소스를 체크아웃하였다.

$ TAG=v1.31.2  # 반드시 git tag 목록에 있는 버전을 사용
$ git checkout $TAG

 

4. 커스텀 태그 설정

Kubernetes 에서 제공하는 빌드 방식은 소스에 대한 Git 저장소에 기존 태그가 이미 저장되어 있어 해당 태그 이름을 우선적으로 활용하여 빌드하도록 구성이 되어 있다. 여기서는 해당 방식을 변경하기보다는, 기존 태그 목록을 제거한 후, 현재 체크아웃한 소스에 대해 커스텀 태그를 설정하는 방식을 적용해보자. 이 때, 임의의 문자열을 사용할 수는 없으며 "v{SEMVER}-{CUSTOM}" 형태를 사용한다. SEMVER는 https://semver.org/lang/ko/ 에서 설명하는 버전 번호에 해당하며, {CUSTOM} 부분에 위에서 설명한 목적에 따라 간단한 문자열을 추가해보는 식으로 설정한다.

# 기존 태그 제거
$ git tag | xargs git tag -d
 
# 원격 태그 가져오기 비활성화
$ git config --local fetch.tags false
 
# 새로운 커스텀 태그 생성
$ git tag $TAG-aewstest

 

5. 태그 적용 확인

아래 명령어를 사용하여 태그가 잘 적용되었는지 확인한다.

 

$ git describe --tags --match='v*'

 

6. kubectl 빌드

이제 make 명령어를 사용하여 빌드해보도록 하자. 아래 명령어를 실행한다.

$ make all WHAT=cmd/kubectl GOFLAGS=-v

 

(kubectl 빌드 시작)

 

(kubectl 빌드 끝!)

 

7. 빌드 결과 확인

빌드된 바이너리는 _output 폴더에 생성된다. 빌드한 kubectl 바이너리를 한 번 실행해보자.

$ _output/bin/kubectl version

 

유의 사항

위에서 설명하였듯이, kubectl 버전은 클러스터 버전과 한 단계 차이까지를 허용한다. 테스트 환경에서 사용할 때에는 충분히 검증 후 사용하며, 프로덕션 환경에서는 공식 배포하는 kubectl 바이너리 사용을 권장한다.

 

 

도구 설치

컴퓨터에서 쿠버네티스 도구를 설정한다.

kubernetes.io

마치며

이와 같이 빌드한 kubectl은 원하는 버전 커스팀 문자열을 가질 수 있으며, 해당 문자열을 활용한다면 CI/CD에서 kubectl 버전을 확인하여 클러스터 관리에 도움을 주는 방향으로 활용하는 것도 가능하겠다. 실제 운영 환경에서는 공식 배포판을 사용하는 것이 안전하며, 커스텀 빌드는 테스트나 개발 환경에서 사용하는 것을 권장한다.

 

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

 

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

 

Windows 11 환경에서 "프로그램 및 기능" 메뉴에서 "AWS" 키워드로 검색한 결과, 현재 설치된 AWS CLI 버전은 2.15.9.0임을 확인할 수 있었다. AWS CLI는 AWS 서비스를 명령어를 통해 관리하도록 해주는 CLI 도구로, `aws configure`를 실행하여 Access Key ID, Secret Access Key, 리전 이름 정도만 설정하면 AWS 서비스를 손쉽게 제어할 수가 있다. 설치할 당시에는 공식 홈페이지 (https://aws.amazon.com/ko/cli/)에서 다운로드를 하여 직접 설치를 진행하였는데, 시간이 지나면서 설치된 버전이 최신인지 확인하고, 최신 버전으로 업그레이드하는 과정이 필자에게는 다소 번거롭게 느껴졌다.

Ubuntu에서 사용하던 "sudo apt update && sudo apt upgrade"와 같이 손쉽게 할 수 있다면 얼마나 좋을까?

 

문득 갑자기 이런 궁금증이 들었고, winget이라는 도구가 있다는 잊고 있던 생각이 갑자기 났다. 이를 통해 AWS CLI 버전을 최신으로 업그레이드를 진행한 경험을 블로그를 통해 적고자 한다.

 

1. winget 소개 & 설치

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

 

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

 

2. winget 기본 명령어

winget을 사용하는 기본적인 방법은 apt와 많이 유사하다. 필자는 여전히 "sudo apt update && sudo apt upgrade" 명령어가 익숙한데, winget에서는 update와 upgrade 명령어가 동일하다는 점을 참고하자 (정확히는 update가 upgrade에 대한 alias로 구성되어 있다). 자세한 도움말은 https://aka.ms/winget-command-help 를 통해 확인할 수 있으며, 또는 국내 Microsoft 권순만 MVP께서 정리하신 블로그 글 (URL: https://blog.naver.com/hakunamata2/223366445380 )을 참고해도 좋겠다.

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

 

3. AWS CLI 설치하기

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

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

 

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

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

 

> winget install Amazon.AWSCLI

 

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

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

 

또 다른 방법으로는 Ubuntu에서 "apt list | grep AWS"와 같이 실행하는 것과 같이 PowerShell에서는 다음과 같은 명령어를 통해 확인할 수도 있겠다. 이 방법은 현재 설치된 버전이 2.15.9.0이며, 최신 설치 가능한 버전이 2.16.4라는 정보까지를 같이 알 수가 있다는 장점이 있다.

> winget list | Select-String -Pattern AWS

Freedom Scientific JAWS 2024            FreedomScientific.JAWS.2024              2024.2310.70.400     2024.240??winget
Freedom Scientific JAWS Training Table??{AE1E7553-752E-4D04-9695-EE1FB83C54AE}   25.0.2005.0
AWS Command Line Interface v2           Amazon.AWSCLI                            2.15.9.0             2.16.4    winget

 

최신 버전으로 업데이트하기 위해서는 다음과 같이 명령어를 실행한다.

> winget upgrade Amazon.AWSCLI

 

 

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

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

 

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

> winget uninstall Amazon.AWSCLI

 

이렇게 특정 패키지를 winget 명령어를 통해 설치, 업그레이드, 제거까지 손쉽게 하는 방법에 대해 알아보았다.

5. winget 명령어로 여러 패키지 업그레이드하기

사실 원래는 AWS CLI 툴 자체를 업그레이드하는 목적보다는 이전에 https://github.com/ianychoi/aws-ecs-fargate-dotnet-module3-supplementary 와 같은 내용을 준비하면서 "Build Tools for Visual Studio 2019"가 설치되었던 것 같은데, 해당 패키지를 포함해 AWS CLI 등 여러 버전들을 각각 다운로드받아 최신 버전으로 설치하기가 번거롭다보니 winget을 사용하면 어떨까하는 생각이 들었고 "winget upgrade" 명령어를 통해 손쉽게 최신 버전으로 업데이트를 진행할 수 있었다. "winget upgrade" 를 실행하면 업그레이드 가능한 모든 프로그램들이 나온다. 필자의 경우 이전에 영상 자막을 직접 관리하고자 사용했던 Vrew 프로그램이 오래 되었고, Bandizip, GIMP 등도 업그레이드하지 않던 상황을 손쉽게 확인할 수 있었다. 모든 것을 업그레이드하려면 "winget upgrade --all" 명령어를 사용하면 되겠지만, 꼭 모든 소프트웨어를 업그이드할 필요는 없겠다. 필자의 경우 GIMP를 굳이 최신으로 꼭 업그레이드가 필요한가 싶어 다음과 같이 3개 소프트웨어만 업데이트를 진행하였다.

"winget upgrade"를 실행한 결과 (일부)
3개 패키지에 대해 명시적으로 업그레이드하기

 

 

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

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

 

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

 

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

 

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

 

0. 실습 환경 배포 & 소개

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

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

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

 

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

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

# Linux
sudo apt install -y tree jq

 

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

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

 

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

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

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

 

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

 

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

1. Terraform 기본 사용

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

 

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

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

 

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

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

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

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

 

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

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

 

 

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

 

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

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

 

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

 

# 초기화
terraform init
ls -al
tree .terraform

# plan 확인
terraform plan

# apply 실행
terraform apply
 Enter a value: yes 입력

# ec2 생성 확인 : aws 웹 관리 콘솔에서도 확인 - 서울 리전 선택
export AWS_PAGER=""
aws ec2 describe-instances --output table

# 테라폼 정보 확인
terraform state list
terraform show
terraform show aws_instance.example

 

 

이번에는 태그 정보를 수정하고 적용해보자.

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

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

  tags = {
    Name = "aews-study"
  }

}
EOT

 

# plan 실행 시 아래와 같은 정보가 출력
terraform plan
# aws_instance.example will be updated in-place
  ~ resource "aws_instance" "example" {
        id                                   = "i-0fe5acad038030055"
      ~ tags                                 = {
          + "Name" = "aews-study"
        }
      ~ tags_all                             = {
          + "Name" = "aews-study"
        }
        # (38 unchanged attributes hidden)

        # (8 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

# apply 실행
terraform apply
 Enter a value: yes 입력

# 모니터링 : [터미널1]에 Name 확인

 

 

실습을 완료하였으면 리소스를 삭제하자.

 

# 리소스 삭제
terraform destroy
 Enter a value: yes 입력

혹은
terraform destroy -auto-approve

 

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

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

 

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

 

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

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

 

 

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

 

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

 

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

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

 

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

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

 

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

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

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

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

 

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

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

 

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

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

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

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

 

배포 이전

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

 

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

 

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

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

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

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

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

  availability_zone = "ap-northeast-2a"

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

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

  availability_zone = "ap-northeast-2c"

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

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

 

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

terraform state show aws_subnet.mysubnet1

terraform output
terraform output aws_vpc_id
terraform output -raw aws_vpc_id

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

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

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

 

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

 

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

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

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

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

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

  availability_zone = "ap-northeast-2a"

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

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

  availability_zone = "ap-northeast-2c"

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


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

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

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

 

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

 

그 다음에는 vpc.tf 코드를 수정하여 IGW 인터넷 게이트웨이로 전달하는 디폴트 라우팅 정보를 추가하자.

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

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

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

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

  availability_zone = "ap-northeast-2a"

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

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

  availability_zone = "ap-northeast-2c"

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


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

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

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

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

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

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

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

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

 

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

terraform state show aws_route.mydefaultroute

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

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

 

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

 

- sg.tf 파일 내용:

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

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

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

 

# 배포
ls *.tf
terraform plan && terraform apply -auto-approve
terraform state list
aws_security_group.mysg
aws_security_group_rule.mysginbound
aws_security_group_rule.mysgoutbound
...

terraform state show aws_security_group.mysg
terraform state show aws_security_group_rule.mysginbound

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

 

- ec2.tf 파일 내용:

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

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

  owners = ["amazon"]
}

resource "aws_instance" "myec2" {

  depends_on = [
    aws_internet_gateway.myigw
  ]

  ami                         = data.aws_ami.my_amazonlinux2.id
  associate_public_ip_address = true
  instance_type               = "t2.micro"
  vpc_security_group_ids      = ["${aws_security_group.mysg.id}"]
  subnet_id                   = aws_subnet.mysubnet1.id

  user_data = <<-EOF
              #!/bin/bash
              wget https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-x86_64
              mv busybox-x86_64 busybox
              chmod +x busybox
              echo "Web Server</h1>" > index.html
              nohup ./busybox httpd -f -p 80 &
              EOF

  user_data_replace_on_change = true

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

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

 

# 
ls *.tf
terraform plan && terraform apply -auto-approve
terraform state list
data.aws_ami.my_amazonlinux2
aws_instance.myec2
...

terraform state show data.aws_ami.my_amazonlinux2
terraform state show aws_instance.myec2

# 데이터소스 값 확인
terraform console
> 
data.aws_ami.my_amazonlinux2.id
"ami-0972fbae82d8513f6"
data.aws_ami.my_amazonlinux2.image_id
data.aws_ami.my_amazonlinux2.name
data.aws_ami.my_amazonlinux2.owners
data.aws_ami.my_amazonlinux2.platform_details
data.aws_ami.my_amazonlinux2.hypervisor
data.aws_ami.my_amazonlinux2.architecture
exit

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

# 출력된 EC2 퍼블릭IP로 cul 접속 확인
terraform output -raw myec2_public_ip
43.203.247.218

MYIP=$(terraform output -raw myec2_public_ip)
while true; do curl --connect-timeout 1  http://$MYIP/ ; echo "------------------------------"; date; sleep 1; done

 

 

AWS 리소스에 대한 의존성은 다음과 같다.

 

이와 같이 AWS EC2 리소스와 Security Group, 그리고 관련한 네트워크 리소스에 대한 전반적인 의존성을 확보하면서 Terraform으로 생성해보는 실습을 진행하였다. 실습을 다 진행한 후에는 리소스 삭제를 꼭 하자.

 

terraform destroy -auto-approve

 

1.4. 프로바이더 활용하기 - Tier

Terraform는 프로바이더에 대한 Tier (Official, Partner, Community) 가 있다. https://registry.terraform.io/browse/providers 에서는 Official은 Hashicorp에서 직접 유지보수를 하는 Tier, Partner는 기술 파트너를 통해 유지보수가 이루어지는 Tier, Community는 Hashicorp 커뮤니티 생태계를 통해 퍼블리싱 및 유지보수가 이루어진다고 한다.

 

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

 

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

 

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

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

 

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

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

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

 

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

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

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

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

 

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

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

# 삭제
terraform destroy -auto-approve

 

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

1.5. 모듈

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

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

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

 

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

 

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

resource "random_password" "password" {
  length           = var.isDB ? 16 : 10
  special          = var.isDB ? true : false
  override_special = "!#$%*?"
}
# variable.tf
variable "isDB" {
  type        = bool
  default     = false
  description = "패스워드 대상의 DB 여부"
}
# output.tf
output "id" {
  value = random_pet.name.id
}

output "pw" {
  value = nonsensitive(random_password.password.result) 
}

 

#
cd 06-module-traning/modules/terraform-random-pwgen

#
ls *.tf
terraform init && terraform plan

# 테스트를 위해 apply 시 변수 지정
terraform apply -auto-approve -var=isDB=true
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
id = "knowing-aardvark"
pw = "Y5eeP0i2KLLE9gBa"

# 확인
terraform state list
terraform state show random_pet.name
terraform state show random_password.password

# graph 확인
terraform graph > graph.dot

 

 

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

 

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

 

# 06-01-basic/main.tf

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

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

output "mypw1" {
  value  = module.mypw1
}

output "mypw2" {
  value  = module.mypw2
}

 

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

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

# 확인
terraform state list

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

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

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

# graph 확인
terraform graph > graph.dot

 

2. Terraform으로 EKS 배포

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

# 코드 가져오기
git clone https://github.com/gasida/aews-cicd.git
cd aews-cicd/4

# terraform 환경 변수 저장
export TF_VAR_KeyName=[각자 ssh keypair]
export TF_VAR_KeyName='kp-ian'
echo $TF_VAR_KeyName

# 
terraform init
terraform plan

# 10분 후 배포 완료
terraform apply -auto-approve

 

 

약 10분이 지나 배포가 완료되면 배포 정보를 확인한다. kubectl 명령어를 실행 가능하도록  인증 정보를 가져오는 단계가 필요하다.

#
kubectl get node -v=6

# EKS 클러스터 인증 정보 업데이트
CLUSTER_NAME=myeks
aws eks update-kubeconfig --region ap-northeast-2 --name $CLUSTER_NAME
kubectl config rename-context "arn:aws:eks:ap-northeast-2:$(aws sts get-caller-identity --query 'Account' --output text):cluster/$CLUSTER_NAME" "Aews-Labs"

#
kubectl cluster-info
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
kubectl get pod -A

 

 

기타 설치도 진행하자.

 

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

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

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

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

 

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

 

그 다음 코드를 재사용하여 두 번째 클러스터를 배포해보자. 다른 클러스터에 접근하므로 서로 다른 kubeconfig 가 필요하다. myeks 클러스터에 접근할 때는 디폴트 config를, myeks2 클러스터에 접근할 때는 myeks2config 파일을 참조하는 것으로 확인을 하였다.

#
cd ..
mkdir 5
cd 5
cp ../4/*.tf .
ls

#
terraform init
terraform apply -auto-approve -var=ClusterBaseName=myeks2 -var=KubernetesVersion="1.28"

# EKS 클러스터 인증 정보 가져오기
CLUSTER_NAME2=myeks2
aws eks update-kubeconfig --region ap-northeast-2 --name $CLUSTER_NAME2 --kubeconfig ./myeks2config

# EKS 클러스터 정보 확인
kubectl --kubeconfig ./myeks2config get node 
kubectl --kubeconfig ./myeks2config get pod -A

 

 

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

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

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

 

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

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

 

 

 

 

+ Recent posts