들어가며
bjh-k8s_modified_v2.yaml 한 파일로 어떻게 VPC부터 K8s Ready 상태까지 자동으로 올라오는지를 분석합니다. 각 설계 결정의 이유와 함께 전체 CloudFormation 명령어 레퍼런스를 정리합니다.
템플릿 전체 구조
1
2
3
4
5
6
7
8
9
10
11
12
13
| bjh-k8s_modified_v2.yaml
├── Parameters 사용자 입력 (KeyName, WorkerCount, InstanceType, EBS 설정 등)
├── Conditions CreateWorker2 / CreateWorker3 (WorkerCount 기반 조건 분기)
├── Resources
│ ├── EC2Role + EC2InstanceProfile IAM
│ ├── MyVPC + Subnet × 4 + IGW 네트워크
│ ├── EC2SG + EFSSG Security Group
│ ├── ElasticFileSystem + MountTarget × 2 EFS
│ ├── MEC2 (Master) + Instance1ENI1
│ ├── W1EC2 (Worker1) + Instance2ENI1 [항상]
│ ├── W2EC2 (Worker2) + Instance3ENI1 [Condition: CreateWorker2]
│ └── W3EC2 (Worker3) + Instance4ENI1 [Condition: CreateWorker3]
└── Outputs MasterNodeIP, WorkerNodeIP × N, EfsDnsName, S3RuntimePrefix
|
파라미터 치트시트
| 파라미터 | 기본값 | 비고 |
|---|
KeyName | (필수) | 등록된 EC2 KeyPair 이름 |
SgIngressCidr | 0.0.0.0/0 | 본인 공인 IP/32 권장 |
WorkerCount | 2 | 1 / 2 / 3 |
MasterNodeInstanceType | t3.large | t2/t3/m5/m6i 시리즈 |
WorkerNodeInstanceType | m5.xlarge | t2/t3/m5/m6i 시리즈 |
Ec2EbsVolumeSize | 200 | GiB (gp3) |
Ec2EbsVolumeIops | 6000 | gp3: 3000 ~ 16000 |
KubernetesVersion | 1.28.0 | 1.23.x ~ 1.30.0 |
OwnerTag | bjh | EC2 태그 owner |
ExpiryDate | 2026-12-31 | 태그 expiry-date (YYYY-MM-DD) |
TargetRegion | us-east-1 | 다른 Region 사용 시 AZ도 변경 |
AvailabilityZone1/2 | us-east-1a/b | Region 변경 시 동기화 필수 |
아래 파라미터를 변경하면 인스턴스가 재생성됩니다 (서비스 중단): MasterNodeInstanceType · WorkerNodeInstanceType · KubernetesVersion · LatestAmiId (Ubuntu AMI 변경)
EBS 사이즈/IOPS 변경은 보통 무중단이지만 인스턴스 재시작이 필요할 수 있습니다.
IAM Role 설계
EC2 인스턴스는 하나의 IAM Role(EC2Role)을 공유합니다. 이 Role에는 세 가지 권한이 담겨 있습니다.
1. AmazonEBSCSIDriverPolicy (관리형 정책)
1
2
| ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy
|
final_v2.sh에서 설치하는 AWS EBS CSI Driver가 볼륨 생성(ec2:CreateVolume), 연결(ec2:AttachVolume), 스냅샷 등의 API를 호출하는 데 필요합니다.
2. S3ScriptsAccess (인라인 정책)
1
2
3
4
5
6
7
8
| - Effect: Allow
Action: s3:GetObject
Resource: "arn:aws:s3:::my-k8s-scripts/*"
- Effect: Allow
Action: [s3:PutObject, s3:GetObject, s3:ListBucket]
Resource:
- "arn:aws:s3:::my-k8s-scripts"
- "arn:aws:s3:::my-k8s-scripts/runtime/*"
|
- GetObject (
/*): 모든 노드가 init_v2.sh, master_v2.sh 등 스크립트를 다운로드할 때 사용합니다. - PutObject/GetObject/ListBucket (
runtime/*): Master가 kubeadm init 후 생성된 admin.conf를 S3에 업로드하고, 워커가 그것을 받아가는 데 사용합니다. sshpass나 root SSH 패스워드 없이 IAM 기반으로 파일을 전달하는 구조입니다.
3. EFSDescribe (인라인 정책)
1
2
3
| - Effect: Allow
Action: [elasticfilesystem:DescribeMountTargets, elasticfilesystem:DescribeFileSystems]
Resource: "*"
|
Master의 UserData에서 EFS 마운트 타깃 상태(LifeCycleState: available)를 폴링할 때 사용합니다.
네트워크 설계
Public Subnet Only — NAT Gateway 없음
1
2
| PublicSubnet1:
MapPublicIpOnLaunch: true # 각 EC2에 공인 IP 자동 부여
|
모든 노드가 PublicSubnet에 위치하고 각자 공인 IP를 가집니다. 인터넷 아웃바운드 트래픽이 Internet Gateway를 직접 통과하므로 NAT Gateway 비용($0.045/h)이 발생하지 않습니다. 대신 EC2가 인터넷에 직접 노출되므로, SgIngressCidr를 본인 IP/32로 제한하는 것이 중요합니다.
고정 Private IP
1
2
3
4
5
6
7
8
| Instance1ENI1:
PrivateIpAddress: 192.168.10.10 # Master
Instance2ENI1:
PrivateIpAddress: 192.168.10.101 # Worker1
Instance3ENI1:
PrivateIpAddress: 192.168.10.102 # Worker2
Instance4ENI1:
PrivateIpAddress: 192.168.20.103 # Worker3 (AZ2)
|
master_v2.sh의 kubeadm init --apiserver-advertise-address=192.168.10.10과 worker_v2.sh의 kubeadm join 192.168.10.10:6443이 이 값에 의존합니다.
IP를 동적으로 받으면 스크립트를 수정해야 하므로 ENI에 고정합니다.
SourceDestCheck: false
K8s는 파드 트래픽을 노드 IP가 아닌 파드 IP로 라우팅합니다. AWS 기본값인 SourceDestCheck: true가 켜져 있으면 출발지/목적지 IP 검증에 걸려 파드 간 통신이 차단됩니다.
Security Group
1
2
3
4
5
6
7
8
9
| SecurityGroupIngress:
- IpProtocol: '-1'
CidrIp: !Ref SgIngressCidr # 본인 PC → SSH/kubectl
- IpProtocol: '-1'
CidrIp: !Ref VpcBlock # 노드 간 (192.168.0.0/16)
- IpProtocol: '-1'
CidrIp: 172.16.0.0/16 # Flannel Pod CIDR
- IpProtocol: '-1'
CidrIp: 10.200.10.0/24 # K8s Service CIDR
|
노드 간 통신, Pod 네트워크(Flannel VXLAN), Service 네트워크를 모두 허용합니다. -1 프로토콜은 all traffic을 의미합니다.
EFS 구성
1
2
3
4
5
6
7
8
| ElasticFileSystem:
Type: AWS::EFS::FileSystem
ElasticFileSystemMountTarget0:
SubnetId: !Ref PublicSubnet1 # AZ1
ElasticFileSystemMountTarget1:
SubnetId: !Ref PublicSubnet2 # AZ2
|
NFS 공유 스토리지로 사용합니다. final_v2.sh에서 설치하는 nfs-subdir-external-provisioner가 이 EFS를 백엔드로 사용해 PVC를 프로비저닝합니다.
1
2
3
4
| MEC2:
DependsOn:
- ElasticFileSystemMountTarget0
- ElasticFileSystemMountTarget1
|
DependsOn으로 마운트 타깃 2개가 모두 생성된 후 Master EC2가 기동되도록 순서를 강제합니다. 이것만으로는 available 상태까지 보장되지 않으므로, UserData에서 추가로 폴링합니다.
1
2
3
4
5
6
| # UserData 내부 — EFS MountTarget이 available이 될 때까지 최대 5분 대기
for i in $(seq 1 30); do
STATE=$(aws efs describe-mount-targets ... --query 'MountTargets[0].LifeCycleState' --output text)
if [ "$STATE" = "available" ]; then break; fi
sleep 10
done
|
EC2 UserData 흐름 (Master 기준 상세)
UserData는 EC2가 처음 부팅될 때 root 권한으로 실행됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| # 1. cfn-bootstrap 설치 — cfn-signal을 사용하기 위한 전제
apt-get install -y awscli python3-pip
pip3 install ... aws-cfn-bootstrap-py3-latest.tar.gz
# 2. 실패 트랩 설치
on_err() {
# S3에 실패 원인 파일 + 로그 업로드
aws s3 cp /var/log/userdata.log s3://my-k8s-scripts/runtime/<stack>/MEC2-userdata.log
# cfn-signal로 즉시 실패 신호 — 25분 타임아웃 기다리지 않고 바로 ROLLBACK
cfn-signal --success false --reason "line=$1 rc=$rc" --stack ... --resource MEC2 ...
}
trap 'on_err $LINENO "$BASH_COMMAND"' ERR
set -eE # 에러 즉시 종료 + trap 전파
# 3. EFS DNS를 파일로 기록 (master.sh보다 먼저)
echo "$EFS_DNS" > /root/efs.txt
# 4. EFS 마운트
mount -t nfs4 $EFS_DNS:/ /nfs4-share
# 5. S3에서 스크립트 다운로드 + 실행
aws s3 cp s3://my-k8s-scripts/init_v2.sh /root/init.sh
aws s3 cp s3://my-k8s-scripts/master_v2.sh /root/master.sh
/root/init.sh > /root/init.log 2>&1
/root/master.sh > /root/master.log 2>&1
# 6. 성공 신호
cfn-signal --success true --stack ... --resource MEC2 ...
|
cfn-signal과 CreationPolicy
1
2
3
4
| MEC2:
CreationPolicy:
ResourceSignal:
Timeout: PT25M
|
CloudFormation은 MEC2 리소스가 생성된 후 최대 25분 동안 cfn-signal을 기다립니다. 이 신호가 없으면 타임아웃 후 ROLLBACK합니다. 신호가 --success false이면 즉시 ROLLBACK합니다.
CREATE_COMPLETE 상태는 cfn-signal을 받은 시점이므로, K8s가 실제로 Ready인 상태를 의미합니다. EC2만 올라온 상태가 아닙니다.
WorkerCount 조건 분기
1
2
3
4
5
| Conditions:
CreateWorker2: !Or
- !Equals [ !Ref WorkerCount, "2" ]
- !Equals [ !Ref WorkerCount, "3" ]
CreateWorker3: !Equals [ !Ref WorkerCount, "3" ]
|
WorkerCount=1이면 W1만, =2이면 W1+W2, =3이면 W1+W2+W3이 생성됩니다. Worker3는 PublicSubnet2(AZ2)에 배치되어 AZ 분산 테스트가 가능합니다.
CFN 명령어 전체 레퍼런스
템플릿 검증 (deploy 전 필수)
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
|
배포
기본값 (master 1 + worker 2):
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
|
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=$(curl -s https://checkip.amazonaws.com)/32 \
WorkerCount=3
|
성능 본격 (옵션 C):
1
2
3
4
5
6
7
8
9
10
| 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 \
MasterNodeInstanceType=m5.large \
WorkerNodeInstanceType=m5.2xlarge \
Ec2EbsVolumeIops=12000 \
WorkerCount=3
|
배포 전 Change-Set 미리보기 (비용 없음)
1
2
3
4
5
6
7
8
9
10
| aws cloudformation deploy \
--template-file bjh-k8s_modified_v2.yaml \
--stack-name bjh-test-k8s \
--capabilities CAPABILITY_IAM \
# 작성자 예시: KeyName=bjh-test-bastion
--parameter-overrides KeyName=<YOUR-KEY-PAIR> SgIngressCidr=$(curl -s https://checkip.amazonaws.com)/32 \
--no-execute-changeset
# 출력된 change-set 이름으로 상세 조회
aws cloudformation describe-change-set --stack-name bjh-test-k8s --change-set-name <이름>
|
배포 상태 모니터링
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| # (a) 한 줄 상태
aws cloudformation describe-stacks --stack-name bjh-test-k8s \
--query "Stacks[0].StackStatus" --output text
# (b) 이벤트 타임라인 (최근 50개)
aws cloudformation describe-stack-events --stack-name bjh-test-k8s \
--query 'StackEvents[*].[Timestamp,LogicalResourceId,ResourceStatus,ResourceStatusReason]' \
--output table | head -60
# (c) 10초 간격 실시간 watch (Ctrl+C 종료)
while :; do
clear
aws cloudformation describe-stacks --stack-name bjh-test-k8s \
--query "Stacks[0].StackStatus" --output text
aws cloudformation describe-stack-events --stack-name bjh-test-k8s \
--query 'StackEvents[0:10].[Timestamp,LogicalResourceId,ResourceStatus]' --output table
sleep 10
done
# (d) 리소스별 진행 상태
aws cloudformation describe-stack-resources --stack-name bjh-test-k8s \
--query 'StackResources[*].[LogicalResourceId,ResourceType,ResourceStatus]' \
--output table
# (e) 완료 대기 (블로킹, 최대 ~25분)
aws cloudformation wait stack-create-complete --stack-name bjh-test-k8s && echo "DONE"
|
Output 조회
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| # 모든 Output 한눈에
aws cloudformation describe-stacks --stack-name bjh-test-k8s \
--query 'Stacks[0].Outputs' --output table
# Master 공인 IP
aws cloudformation describe-stacks --stack-name bjh-test-k8s \
--query "Stacks[0].Outputs[?OutputKey=='MasterNodeIP'].OutputValue" --output text
# Master Private IP (kubeadm advertise — 보통 192.168.10.10)
aws cloudformation describe-stacks --stack-name bjh-test-k8s \
--query "Stacks[0].Outputs[?OutputKey=='MasterNodePrivateIP'].OutputValue" --output text
# Worker 공인 IP (있는 만큼만 출력)
for k in WorkerNode1IP WorkerNode2IP WorkerNode3IP; do
ip=$(aws cloudformation describe-stacks --stack-name bjh-test-k8s \
--query "Stacks[0].Outputs[?OutputKey=='$k'].OutputValue" --output text)
[ -n "$ip" ] && echo "$k = $ip"
done
# EFS DNS
aws cloudformation describe-stacks --stack-name bjh-test-k8s \
--query "Stacks[0].Outputs[?OutputKey=='EfsDnsName'].OutputValue" --output text
# admin.conf S3 위치
aws cloudformation describe-stacks --stack-name bjh-test-k8s \
--query "Stacks[0].Outputs[?OutputKey=='S3RuntimePrefix'].OutputValue" --output text
|
스택 업데이트
같은 --stack-name으로 deploy를 재실행하면 차이만큼만 업데이트됩니다. InstanceType 변경 등 인스턴스 재생성이 필요한 경우 admin.conf 재업로드가 필요합니다.
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 \
WorkerNodeInstanceType=m5.2xlarge
|
스택 삭제
1
2
3
4
5
6
7
8
9
10
11
12
| # 삭제 시작 (비동기)
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 잔여물 수동 정리 (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"
|
ROLLBACK / 실패 처리
| 상태 | 의미 | 조치 |
|---|
ROLLBACK_IN_PROGRESS | 자동 롤백 중 | 기다림 (wait stack-rollback-complete) |
ROLLBACK_COMPLETE | 첫 생성 실패 후 롤백 완료 | update 불가 — delete-stack 후 재생성 |
UPDATE_ROLLBACK_FAILED | update 중 롤백도 실패 | continue-update-rollback 으로 강제 진행 |
CREATE_FAILED | 특정 자원 생성 실패 | 이벤트 로그에서 원인 자원 확인 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # 실패 자원과 원인 확인
aws cloudformation describe-stack-events --stack-name bjh-test-k8s \
--query "StackEvents[?ResourceStatus=='CREATE_FAILED'].[Timestamp,LogicalResourceId,ResourceStatusReason]" \
--output table
# ROLLBACK_COMPLETE 후 재시도
aws cloudformation delete-stack --stack-name bjh-test-k8s
aws cloudformation wait stack-delete-complete --stack-name bjh-test-k8s
aws cloudformation deploy ... # 수정 후 재배포
# 실패 자원을 디버깅용으로 남기고 싶을 때 (--on-failure DO_NOTHING)
aws cloudformation create-stack \
--template-body file://bjh-k8s_modified_v2.yaml \
--stack-name bjh-test-k8s --capabilities CAPABILITY_IAM \
--on-failure DO_NOTHING \
# 작성자 예시: ParameterValue=bjh-test-bastion
--parameters ParameterKey=KeyName,ParameterValue=<YOUR-KEY-PAIR> \
ParameterKey=SgIngressCidr,ParameterValue=$(curl -s https://checkip.amazonaws.com)/32
# 디버깅 후 수동 delete-stack
|
PowerShell 등가 명령어 (Windows 사용자)
| Bash | PowerShell |
|---|
MASTER=$(aws ...) | $MASTER = aws ... |
\ 줄바꿈 | ` (백틱) 또는 한 줄로 |
&& | ; if ($?) { ... } (PS 5.1) 또는 && (PS 7+) |
head -50 | \| Select-Object -First 50 |
grep -q ... | \| Select-String ... |
PowerShell 5.1 watch 루프 예시:
1
2
3
4
5
6
7
8
| while ($true) {
Clear-Host
aws cloudformation describe-stacks --stack-name bjh-test-k8s `
--query "Stacks[0].StackStatus" --output text
aws cloudformation describe-stack-events --stack-name bjh-test-k8s `
--query "StackEvents[0:10].[Timestamp,LogicalResourceId,ResourceStatus]" --output table
Start-Sleep -Seconds 10
}
|
자주 쓰는 명령어
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # 배포 + 완료 대기 + SSH 명령 출력 (한 번에)
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 \
&& aws cloudformation describe-stacks --stack-name bjh-test-k8s \
--query "Stacks[0].Outputs[?OutputKey=='MasterNodeIP'].OutputValue" --output text \
| xargs -I {} echo "ssh -i <YOUR-KEY-PAIR>.pem ubuntu@{}" # 작성자 예시: bjh-test-bastion.pem
# 삭제 + 대기 + S3 정리 (한 번에)
aws cloudformation delete-stack --stack-name bjh-test-k8s \
&& aws cloudformation wait stack-delete-complete --stack-name bjh-test-k8s \
&& aws s3 rm s3://my-k8s-scripts/runtime/bjh-test-k8s/ --recursive \
&& echo "All cleaned up"
|
(선택) Region 변경
기본값은 us-east-1입니다. 다른 Region으로 배포하려면 TargetRegion과 AvailabilityZone1/2를 함께 변경합니다.
1
2
3
4
5
6
7
8
9
10
| aws cloudformation deploy --template-file bjh-k8s_modified_v2.yaml \
--stack-name bjh-test-k8s --capabilities CAPABILITY_IAM \
--region ap-northeast-2 \
--parameter-overrides \
# 작성자 예시: bjh-test-bastion
KeyName=<YOUR-KEY-PAIR> \
SgIngressCidr=165.225.228.251/32 \
TargetRegion=ap-northeast-2 \
AvailabilityZone1=ap-northeast-2a \
AvailabilityZone2=ap-northeast-2c
|
S3 버킷도 해당 Region에 있어야 IAM Role의 s3:GetObject가 정상 동작합니다. Cross-Region도 가능하지만 레이턴시가 증가합니다.
다음 편
Part 3에서는 init_v2.sh, master_v2.sh, worker_v2.sh, final_v2.sh 네 스크립트의 각 단계를 해설합니다. kubeadm init 파라미터 의미, admin.conf를 S3로 전달하는 이유, EBS CSI Driver와 StorageClass 구성, Flannel CNI 매니페스트를 오프라인으로 관리하는 이유까지 다룹니다.