EC2 위의 K8s 배포 자동화 — Part 1: 아키텍처와 사전 준비
들어가며
CloudNativePG(CNPG)와 MySQL 오퍼레이터를 쿠버네티스 위에서 테스트하려면 클러스터가 먼저 필요합니다. EKS를 사용하면 편리하지만 클러스터 자체에 시간당 $0.10(월 약 $72)이 청구되고, 노드가 인터넷에 나가려면 NAT Gateway가 추가로 시간당 $0.045씩 더해집니다. 테스트 목적으로 띄웠다 내리는 환경이라면 부담입니다.
이 시리즈는 CloudFormation 명령 한 줄로 EC2 위에 kubeadm 기반 K8s 클러스터를 자동 구성하는 방법을 정리합니다. EKS 관리 비용이 없고, NAT Gateway도 사용하지 않습니다. 그 대신 EC2 인스턴스에 공인 IP를 직접 부여해 인터넷 접근을 처리합니다. 무엇보다 테스트가 끝난 후 스택 하나만 삭제하면 모든 리소스가 일괄 정리된다는 점이 개인적으로 가장 마음에 드는 부분입니다. 이 시리즈에서 소개하는 환경을 테스트 시 요긴하게 활용하고 있습니다.
이 환경은 테스트 / 검증 전용입니다. 프로덕션에서는 EKS, 멀티 AZ 배치, Private Subnet + NAT Gateway, 별도 보안 강화가 필요합니다.
결론부터
사전 조건이 모두 갖춰져 있다면 아래 순서로 진행합니다. 각 단계의 세부 사항은 이후 절에서 설명합니다.
사전 준비 — CloudFormation 템플릿 (로컬에만 저장)
bjh-k8s_modified_v2.yaml은 S3에 올리지 않습니다. CloudFormation 배포 시 로컬에서 직접 읽습니다.
1단계 — 스크립트 5개를 S3에 업로드
1
2
3
4
5
6
7
cd /로컬/스크립트/폴더
aws s3 cp init_v2.sh s3://my-k8s-scripts/init_v2.sh
aws s3 cp master_v2.sh s3://my-k8s-scripts/master_v2.sh
aws s3 cp worker_v2.sh s3://my-k8s-scripts/worker_v2.sh
aws s3 cp final_v2.sh s3://my-k8s-scripts/final_v2.sh
aws s3 cp kube-flannel.yml s3://my-k8s-scripts/kube-flannel.yml
2단계 — CloudFormation 배포 명령 한 줄이면 끝!
1
2
3
4
5
6
7
8
aws cloudformation deploy \
--template-file bjh-k8s_modified_v2.yaml \
--stack-name bjh-test-k8s \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
# 작성자 예시: bjh-test-bastion
KeyName=<YOUR-KEY-PAIR> \
SgIngressCidr=$(curl -s https://checkip.amazonaws.com)/32
약 25분 후 CREATE_COMPLETE = K8s Ready.
이 스크립트에서 고정으로 사용하는 값
bjh-test-k8s— CloudFormation 스택 이름. 원하는 이름으로 바꿔도 됩니다.my-k8s-scripts— S3 버킷명. IAM 정책에 하드코딩되어 있으므로 변경 시bjh-k8s_modified_v2.yaml도 함께 수정해야 합니다.bjh-k8s_modified_v2.yaml— 다운로드한 CFN 템플릿의 실제 파일명.
전체 아키텍처
리소스 구성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CloudFormation deploy (bjh-k8s_modified_v2.yaml)
└─ VPC (192.168.0.0/16) + Internet Gateway
├─ PublicSubnet1 AZ1 192.168.10.0/24
│ ├─ k8s-m Master .10.10 t3.large [항상]
│ ├─ k8s-w1 Worker1 .10.101 m5.xlarge [항상]
│ └─ k8s-w2 Worker2 .10.102 m5.xlarge [WorkerCount ≥ 2]
├─ PublicSubnet2 AZ2 192.168.20.0/24
│ └─ k8s-w3 Worker3 .20.103 m5.xlarge [WorkerCount = 3]
├─ PrivateSubnet1/2 (미사용 — 향후 확장 예약)
├─ EFS + MountTarget × 2 (NFS 공유 스토리지)
└─ IAM Role → S3 (my-k8s-scripts) 읽기/쓰기
S3 버킷 (my-k8s-scripts) ← 배포 전 수동 업로드
├── init_v2.sh 공통 노드 초기화
├── master_v2.sh 컨트롤플레인 설정
├── worker_v2.sh 워커 조인
├── final_v2.sh 클러스터 완성 (CNI, CSI, StorageClass)
└── kube-flannel.yml Flannel CNI 매니페스트
EC2 부팅 흐름
1
2
3
4
5
6
7
8
9
10
11
12
13
14
EC2 기동 → UserData 실행
├─ apt: awscli, cfn-bootstrap 설치
├─ on_err 트랩 설치 (실패 시 S3에 로그 + cfn-signal fail 즉시 전송)
├─ [Master] EFS DNS → /root/efs.txt 기록 + NFS 마운트
├─ aws s3 cp 스크립트 다운로드
├─ ./init.sh 공통: containerd / kubeadm / kubelet / kubectl
├─ [Master] ./master.sh
│ ├─ kubeadm init
│ ├─ admin.conf → S3 업로드 (워커에게 IAM으로 전달)
│ └─ ./final.sh Flannel / EBS CSI / StorageClass / NFS Provisioner
├─ [Worker] ./worker.sh
│ ├─ kubeadm join
│ └─ admin.conf ← S3 다운로드
└─ cfn-signal --success true → CloudFormation CREATE_COMPLETE
0. 빠른 체크리스트
배포 전 아래 항목을 모두 확인합니다. 하나라도 빠지면 Stack이 ROLLBACK됩니다.
| # | 항목 | 확인 명령 / 위치 |
|---|---|---|
| 1 | AWS CLI v2 설치 + 자격증명 등록 | aws sts get-caller-identity |
| 2 | 작업 Region이 us-east-1 (또는 파라미터로 변경) | aws configure get region |
| 3 | EC2 KeyPair <YOUR-KEY-PAIR> 존재 (us-east-1) | aws ec2 describe-key-pairs --key-names <YOUR-KEY-PAIR> |
| 4 | S3 버킷 my-k8s-scripts 존재 (us-east-1) | aws s3 ls s3://my-k8s-scripts/ |
| 5 | S3에 스크립트 5개 업로드 완료 | aws s3 ls s3://my-k8s-scripts/ \| grep -E 'init_v2\|master_v2\|worker_v2\|final_v2\|kube-flannel' |
| 6 | 본인 공인 IP 확인 (SgIngressCidr용) | curl -s https://checkip.amazonaws.com |
| 7 | EC2 vCPU 서비스 한도 충분 | aws service-quotas get-service-quota --service-code ec2 --quota-code L-1216C47A |
| 8 | IAM 권한: CFN/EC2/IAM/S3/EFS/VPC create | 아래 5. IAM 권한 참조 |
| 9 | 비용 알람 (선택) | AWS Budgets |
1. AWS CLI v2 + 자격증명
설치 (Windows)
AWS 공식 MSI 인스톨러를 다운로드해 설치합니다.
설치 후 PowerShell 또는 Git Bash를 재시작한 뒤 확인합니다.
1
2
aws --version
# aws-cli/2.x.x Python/3.x ...
자격증명 등록
1
2
3
4
5
aws configure
# AWS Access Key ID: [발급받은 키]
# AWS Secret Access Key: [발급받은 시크릿]
# Default region name: us-east-1
# Default output format: json
정상 등록 확인
1
2
aws sts get-caller-identity
# Account, UserId, Arn이 출력되면 정상입니다
2. EC2 KeyPair
PEM 파일이 로컬에 있더라도 AWS 측에 KeyPair가 등록되어 있어야 SSH 접속이 가능합니다.
존재 확인
1
aws ec2 describe-key-pairs --key-names <YOUR-KEY-PAIR> --region us-east-1
결과가 정상이면 통과합니다. InvalidKeyPair.NotFound 에러가 나오면 아래 §2-2를 수행합니다.
KeyPair 생성 (없을 때만)
1
2
3
4
5
6
7
8
9
10
aws ec2 create-key-pair \
# 작성자 예시: bjh-test-bastion
--key-name <YOUR-KEY-PAIR> \
--region us-east-1 \
--query 'KeyMaterial' --output text \
> "C:/경로/<YOUR-KEY-PAIR>.pem"
# Windows PowerShell에서 파일 권한 정리
icacls "<YOUR-KEY-PAIR>.pem" /inheritance:r
icacls "<YOUR-KEY-PAIR>.pem" /grant:r "$($env:USERNAME):(R)"
기존에 다른 PEM이 있으면 절대 덮어쓰지 마십시오. KeyPair가 이미 AWS에 등록된 경우 PEM을 재생성하면 SSH 접속이 깨집니다.
3. S3 버킷 + 스크립트 업로드
이 단계가 빠지면 CloudFormation 배포 자체는 성공해도 K8s가 절대 뜨지 않습니다. EC2 UserData가 부팅 시 S3에서 스크립트를 받아 실행하기 때문입니다.
CREATE_COMPLETE가 뜨더라도 K8s는 미설치 상태로 남습니다.
버킷 존재 확인
1
aws s3 ls s3://my-k8s-scripts/ --region us-east-1
버킷 생성 (없을 때만)
1
2
3
aws s3api create-bucket \
--bucket my-k8s-scripts \
--region us-east-1
버킷명은 반드시
my-k8s-scripts이어야 합니다. 템플릿 IAM Role이 이 버킷명에 대해서만 권한을 허용합니다. 다른 이름을 사용하려면 YAML도 함께 수정해야 합니다. 다른 Region에 버킷을 만들 때는--create-bucket-configuration LocationConstraint=<region>을 추가합니다.
업로드 대상 파일 (5개)
| 로컬 파일 | S3 경로 |
|---|---|
init_v2.sh | s3://my-k8s-scripts/init_v2.sh |
master_v2.sh | s3://my-k8s-scripts/master_v2.sh |
worker_v2.sh | s3://my-k8s-scripts/worker_v2.sh |
final_v2.sh | s3://my-k8s-scripts/final_v2.sh |
kube-flannel.yml | s3://my-k8s-scripts/kube-flannel.yml |
한 번에 업로드
1
2
3
4
5
6
7
cd "C:/경로/k8s"
aws s3 cp init_v2.sh s3://my-k8s-scripts/init_v2.sh
aws s3 cp master_v2.sh s3://my-k8s-scripts/master_v2.sh
aws s3 cp worker_v2.sh s3://my-k8s-scripts/worker_v2.sh
aws s3 cp final_v2.sh s3://my-k8s-scripts/final_v2.sh
aws s3 cp kube-flannel.yml s3://my-k8s-scripts/kube-flannel.yml
업로드 검증
1
2
aws s3 ls s3://my-k8s-scripts/
# 5개 파일이 모두 보여야 합니다
스크립트 내용을 수정했다면 반드시 S3에 다시 올린 후 스택을 재생성합니다. CloudFormation은 로컬 파일을 직접 참조하지 않습니다.
runtime/프리픽스(s3://my-k8s-scripts/runtime/<stack-name>/)는 Master가admin.conf를 워커에 전달하기 위한 임시 영역입니다.
미리 만들 필요 없이master_v2.sh가 자동으로 업로드합니다.
4. 본인 공인 IP 확인
배포 명령의 SgIngressCidr 파라미터에 본인 IP를 입력해야 SSH 접속이 허용됩니다.
1
2
curl -s https://checkip.amazonaws.com
# → 예: 165.225.228.251
위 결과를 <IP>/32 형태로 사용합니다. VPN 전환이나 공유기 재시작으로 IP가 바뀌면 SecurityGroup을 업데이트하거나 스택을 재배포해야 합니다.
5. IAM 권한
배포를 실행하는 IAM 주체(사용자/역할)가 다음 액션을 수행할 수 있어야 합니다.
| 서비스 | 주요 필요 액션 |
|---|---|
| CloudFormation | CreateStack, UpdateStack, DeleteStack, DescribeStacks, DescribeStackEvents, DescribeStackResources |
| EC2 | CreateVpc, CreateSubnet, RunInstances, CreateSecurityGroup, AuthorizeSecurityGroupIngress, Describe* 등 |
| EFS | CreateFileSystem, CreateMountTarget, DescribeFileSystems, Delete* |
| IAM | CreateRole, PutRolePolicy, CreateInstanceProfile, PassRole 등 (CAPABILITY_IAM 필요) |
| S3 | PutObject, GetObject, ListBucket |
| SSM | GetParameters (Ubuntu AMI ID 자동 조회) |
개인 계정이라면
AdministratorAccess정책으로 모두 통과됩니다. 회사 계정에서 권한이 제한된 경우 위 액션 단위로 정책을 검토하셔야 합니다.
6. 비용 사전 인지
| 옵션 | 구성 | 일 예상 비용 (on-demand, us-east-1, worker 2대 기준) |
|---|---|---|
| A — 검소 | master + worker 2대 모두 t3.large | 약 $6~8/일 |
| B — 권장 | master t3.large + worker 2대 m5.xlarge | 약 $16/일 (EC2 ~$11.2 + EBS ~$4.8) |
| C — 본격 | master m5.large + worker 2대 m5.2xlarge, EBS 12000 IOPS | 약 $33+/일 |
추가 비용 항목:
- EFS: 사용량 기반 (Standard $0.30/GB-월). 테스트 데이터가 적으면 무시할 수준입니다.
- NAT Gateway: 이 스택은 사용하지 않으므로 $0입니다.
- EBS Throughput: CFN EC2::Instance에서 직접 지정이 안 되므로, 기본값(125 MB/s)에서 변경이 필요하면 인스턴스 기동 후 별도로 적용합니다.
1
2
3
4
5
6
7
8
9
10
# 인스턴스 기동 후 EBS Throughput 일괄 변경 예시
for ID in $(aws ec2 describe-instances \
--filters "Name=tag:aws:cloudformation:stack-name,Values=bjh-test-k8s" \
"Name=instance-state-name,Values=running" \
--query 'Reservations[].Instances[].InstanceId' --output text); do
VOL=$(aws ec2 describe-instances --instance-ids $ID \
--query 'Reservations[0].Instances[0].BlockDeviceMappings[?DeviceName==`/dev/sda1`].Ebs.VolumeId' \
--output text)
aws ec2 modify-volume --volume-id $VOL --throughput 500
done
테스트가 끝나면
aws cloudformation delete-stack --stack-name <스택이름>으로 정리하면 됩니다.
(선택) AWS Budgets 알람
일 $20 초과 시 이메일 알림을 설정합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
aws budgets create-budget \
--account-id $(aws sts get-caller-identity --query Account --output text) \
--budget '{
"BudgetName": "bjh-test-k8s-daily",
"BudgetLimit": {"Amount": "20", "Unit": "USD"},
"TimeUnit": "DAILY",
"BudgetType": "COST"
}' \
--notifications-with-subscribers '[{
"Notification": {
"NotificationType": "ACTUAL",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 100
},
"Subscribers": [{
"SubscriptionType": "EMAIL",
"Address": "[email protected]"
}]
}]'
7. 배포 직전 최종 점검 + 실행
(1) 사전 자원 한 번에 확인
1
2
3
4
5
6
7
aws sts get-caller-identity --query 'Arn' --output text
aws ec2 describe-key-pairs --key-names <YOUR-KEY-PAIR> --region us-east-1 \
--query 'KeyPairs[0].KeyName' --output text
aws s3 ls s3://my-k8s-scripts/
aws service-quotas get-service-quota \
--service-code ec2 --quota-code L-1216C47A \
--region us-east-1 --query 'Quota.Value' --output text
(2) 공인 IP 확인
1
2
MY_IP=$(curl -s https://checkip.amazonaws.com)/32
echo "SgIngressCidr will be: $MY_IP"
(3) 템플릿 사전 검증
1
2
3
4
5
6
7
# CFN 서버 사이드 문법 검증
aws cloudformation validate-template \
--template-body file://bjh-k8s_modified_v2.yaml \
--query '{Params:Parameters[].ParameterKey,Caps:Capabilities}' --output table
# (선택) 로컬 lint — pip install cfn-lint 후
cfn-lint bjh-k8s_modified_v2.yaml
검증 실패 시 배포하지 마십시오. 즉시 ROLLBACK됩니다.
(4) 배포 실행
기본값(master 1 + worker 2, t3.large + m5.xlarge, EBS 200 GB / 6000 IOPS):
1
2
3
4
5
6
7
8
aws cloudformation deploy \
--template-file bjh-k8s_modified_v2.yaml \
--stack-name bjh-test-k8s \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
# 작성자 예시: bjh-test-bastion
KeyName=<YOUR-KEY-PAIR> \
SgIngressCidr=$MY_IP
HA 테스트용 worker 3대:
1
2
3
4
5
6
7
aws cloudformation deploy --template-file bjh-k8s_modified_v2.yaml \
--stack-name bjh-test-k8s --capabilities CAPABILITY_IAM \
--parameter-overrides \
# 작성자 예시: bjh-test-bastion
KeyName=<YOUR-KEY-PAIR> \
SgIngressCidr=$MY_IP \
WorkerCount=3
CREATE_COMPLETE는 cfn-signal까지 통과한 상태, 즉 K8s가 실제로 Ready된 것을 의미합니다. 최대 약 25분이 소요됩니다.
(5) 완료 확인 + Master 접속
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 배포 완료 대기 (블로킹)
aws cloudformation wait stack-create-complete --stack-name bjh-test-k8s && echo "DONE"
# 모든 Output 조회
aws cloudformation describe-stacks --stack-name bjh-test-k8s \
--query 'Stacks[0].Outputs' --output table
# Master 공인 IP 조회 + SSH 접속
MASTER=$(aws cloudformation describe-stacks --stack-name bjh-test-k8s \
--query "Stacks[0].Outputs[?OutputKey=='MasterNodeIP'].OutputValue" --output text)
ssh -i <YOUR-KEY-PAIR>.pem ubuntu@$MASTER # 작성자 예시: bjh-test-bastion.pem
# 노드 상태 확인
kubectl get nodes -o wide
노드 N+1대가 모두 Ready이면 다음 단계(CNPG Operator 설치)로 진행합니다.
8. 정리 (테스트 종료 시)
1
2
3
4
5
6
7
8
9
10
11
12
# 스택 삭제 시작 (EC2 / EBS / EFS / VPC 모두 정리)
aws cloudformation delete-stack --stack-name bjh-test-k8s
# 삭제 완료 대기 (최대 ~10분)
aws cloudformation wait stack-delete-complete --stack-name bjh-test-k8s && echo "DELETED"
# admin.conf S3 잔여물 수동 정리 (delete-stack은 S3 객체를 삭제하지 않음)
aws s3 rm s3://my-k8s-scripts/runtime/bjh-test-k8s/ --recursive
# 삭제 확인
aws cloudformation describe-stacks --stack-name bjh-test-k8s 2>&1 \
| grep -q "does not exist" && echo "Stack fully deleted"
다음 편
Part 2에서는 CloudFormation 템플릿(bjh-k8s_modified_v2.yaml)의 각 리소스를 해설합니다. IAM Role 설계, NAT Gateway 없는 네트워크 구성, cfn-signal 동작 원리, WorkerCount 조건 분기, 그리고 전체 CFN 명령어 레퍼런스를 다룹니다.