블로그 불러오는 중...
문의 보내기
남겨주면 블로그 주인에게 바로 전달돼요.
가시다님 github 에서 받아 실습 환경을 구축한다.
# 코드 다운로드
git clone https://github.com/gasida/aews.git
cd aews
tree aews
# 작업 디렉터리 이동
cd 1w1. Terraform 변수 설정 (var.tf)
가장 먼저 살펴볼 파일은 var.tf이다. 테라폼 코드 내에서 사용할 다양한 변수들을 한곳에 모아 정의해 두는 파일이다. 클러스터 이름, 쿠버네티스 버전, 노드의 인스턴스 타입이나 개수 등을 변수화해 두었기 때문에, 추후 환경을 변경할 때 이 파일만 수정하면 되어 관리가 매우 용이하다.
variable "KeyName" {
# aws ec2 describe-key-pairs --query "KeyPairs[].KeyName" --output text
# export TF_VAR_KeyName=kp-gasida
description = "Name of an existing EC2 KeyPair to enable SSH access to the instances."
type = string
}
variable "ssh_access_cidr" {
# export TF_VAR_ssh_access_cidr=$(curl -s ipinfo.io/ip)/32
description = "Allowed CIDR for SSH access"
type = string
}
variable "ClusterBaseName" {
description = "Base name of the cluster."
type = string
default = "myeks"
}
variable "KubernetesVersion" {
description = "Kubernetes version for the EKS cluster."
type = string
default = "1.34"
}
variable "WorkerNodeInstanceType" {
description = "EC2 instance type for the worker nodes."
type = string
default = "t3.medium"
}
variable "WorkerNodeCount" {
description = "Number of worker nodes."
type = number
default = 2
}
variable "WorkerNodeVolumesize" {
description = "Volume size for worker nodes (in GiB)."
type = number
default = 30
}
variable "TargetRegion" {
description = "AWS region where the resources will be created."
type = string
default = "ap-northeast-2"
}
variable "availability_zones" {
description = "List of availability zones."
type = list(string)
default = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
}
variable "VpcBlock" {
description = "CIDR block for the VPC."
type = string
default = "192.168.0.0/16"
}
variable "public_subnet_blocks" {
description = "List of CIDR blocks for the public subnets."
type = list(string)
default = ["192.168.1.0/24", "192.168.2.0/24", "192.168.3.0/24"]
}이 var.tf 파일에 정의된 변수들은 앞으로 살펴볼 리소스 생성 파일(vpc.tf, eks.tf 등)들이 var.변수명 형태로 참조하여 사용하게 된다.
2. VPC 설정 (vpc.tf)
vpc.tf 파일은 AWS 공식 테라폼 모듈을 사용하여 퍼블릭/프라이빗 서브넷, 라우팅 테이블(Routing Table), 인터넷 게이트웨이(IGW) 등을 한 번에 프로비저닝한다.
########################
# VPC
########################
# VPC 모듈: 퍼블릭 및 프라이빗 서브넷을 포함하는 VPC를 생성
# https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/6.5.0
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~>6.5"
name = "${var.ClusterBaseName}-VPC"
cidr = var.VpcBlock
azs = var.availability_zones
enable_dns_support = true # DNS 서버 활성화
enable_dns_hostnames = true # 인스턴스에 DNS 이름 부여
public_subnets = var.public_subnet_blocks
enable_nat_gateway = false
manage_default_network_acl = false
map_public_ip_on_launch = true
igw_tags = {
"Name" = "${var.ClusterBaseName}-IGW"
}
public_subnet_tags = {
"Name" = "${var.ClusterBaseName}-PublicSubnet"
"kubernetes.io/role/elb" = "1"
}
tags = {
"Environment" = "cloudneta-lab"
}
}3. EKS 클러스터 및 워커 노드 설정 (eks.tf)
인프라의 핵심인 EKS 제어부(Control Plane)와 워커 노드(Node Group)를 정의하는 파일이다. 서울 리전(ap-northeast-2)을 타겟으로 하며, 앞서 생성한 VPC 모듈의 네트워크 정보를 참조하여 클러스터를 배포한다.
# AWS 공급자: 지정된 리전에서 AWS 리소스를 설정
provider "aws" {
region = var.TargetRegion
}
########################
# Security Group Setup #
########################
# 보안 그룹: EKS 워커 노드용 보안 그룹 생성
resource "aws_security_group" "node_group_sg" {
name = "${var.ClusterBaseName}-node-group-sg"
description = "Security group for EKS Node Group"
vpc_id = module.vpc.vpc_id
tags = {
Name = "${var.ClusterBaseName}-node-group-sg"
}
}
# 보안 그룹 규칙: 특정 IP에서 EKS 워커 노드로 SSH(22번 포트) 접속 허용
resource "aws_security_group_rule" "allow_ssh" {
type = "ingress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [
var.ssh_access_cidr,
"192.168.1.100/32"
]
security_group_id = aws_security_group.node_group_sg.id
}
########################
# EKS
########################
# https://registry.terraform.io/modules/terraform-aws-modules/eks/aws/latest
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 21.0"
name = var.ClusterBaseName
kubernetes_version = var.KubernetesVersion
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnets
endpoint_public_access = true
endpoint_private_access = false # true
# endpoint_public_access_cidrs = [
# var.ssh_access_cidr
# ]
# controlplane log
enabled_log_types = []
# Optional: Adds the current caller identity as an administrator via cluster access entry
enable_cluster_creator_admin_permissions = true
# EKS Managed Node Group(s)
eks_managed_node_groups = {
default = {
name = "${var.ClusterBaseName}-node-group"
use_name_prefix = false
instance_types = ["${var.WorkerNodeInstanceType}"]
desired_size = var.WorkerNodeCount
max_size = var.WorkerNodeCount + 2
min_size = var.WorkerNodeCount - 1
disk_size = var.WorkerNodeVolumesize
subnets = module.vpc.public_subnets
key_name = "${var.KeyName}"
vpc_security_group_ids = [aws_security_group.node_group_sg.id]
# AL2023 전용 userdata 주입
cloudinit_pre_nodeadm = [
{
content_type = "text/x-shellscript"
content = <<-EOT
#!/bin/bash
echo "Starting custom initialization..."
dnf update -y
dnf install -y tree bind-utils
echo "Custom initialization completed."
EOT
}
]
}
}
# add-on
addons = {
coredns = {
most_recent = true
}
kube-proxy = {
most_recent = true
}
vpc-cni = {
most_recent = true
before_compute = true
}
}
tags = {
Environment = "cloudneta-lab"
Terraform = "true"
}
}4. 테라폼 배포 실행
이제 준비된 테라폼 코드를 실제 AWS 환경에 배포할 차례이다. 배포를 실행하기 전, 내 PC의 공인 IP 정보를 가져와 보안 그룹에 추가하기 위해 환경 변수를 세팅하는 과정이 필요하다. 또한, EC2 워커 노드에 SSH로 접속하기 위한 키 페어(Key Pair)가 미리 준비되어 있어야 한다. (AWS EC2 키 페어 생성 링크에서 .pem 형식으로 키 페어를 생성해 주자.)
키 페어 준비가 끝났다면, 테라폼의 3단계 명령어(init -> plan -> apply)를 통해 인프라를 프로비저닝한다.
# 변수 지정
aws ec2 describe-key-pairs --query "KeyPairs[].KeyName" --output text
export TF_VAR_KeyName=$(aws ec2 describe-key-pairs --query "KeyPairs[].KeyName" --output text)
export TF_VAR_ssh_access_cidr=$(curl -s ipinfo.io/ip)/32
echo $TF_VAR_KeyName $TF_VAR_ssh_access_cidr
# 배포 : 12분 정도 소요
terraform init
terraform plan
nohup sh -c "terraform apply -auto-approve" > create.log 2>&1 &
tail -f create.log약 10분 정도 소요된다. 실제로 콘솔에서 생성 된 것을 볼 수 있다.
설치 이후에 자격증명 등을 셋팅한다.
# 자격증명 설정
aws eks update-kubeconfig --region ap-northeast-2 --name myeks
# k8s config 확인 및 rename context
cat ~/.kube/config
cat ~/.kube/config | grep current-context | awk '{print $2}'
kubectl config rename-context $(cat ~/.kube/config | grep current-context | awk '{print $2}') myeks
cat ~/.kube/config | grep current-context해당 설정 이후 정상적으로 kubernetes 노드가 적용 된 것을 볼 수 있다.

# eks 클러스터 정보 확인
kubectl cluster-info
# endpoint 확인
CLUSTER_NAME=myeks
aws eks describe-cluster --name $CLUSTER_NAME | jq
aws eks describe-cluster --name $CLUSTER_NAME | jq -r .cluster.endpoint
# eks 노드 그룹 정보 확인
aws eks describe-nodegroup --cluster-name $CLUSTER_NAME --nodegroup-name $CLUSTER_NAME-node-group | jq
# 노드 정보 확인 : OS와 컨테이너런타임 확인
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
kubectl get node --label-columns=node.kubernetes.io/instance-type
kubectl get node --label-columns=eks.amazonaws.com/capacityType # 노드의 capacityType 확인
kubectl get node
kubectl get node -owide
# 인증 정보 확인 : 자세한 정보는 보안에서 다룸
kubectl get node -v=6kubectl config view 를 이용한 API 로 접근하면 접속이 되는 것을 볼 수 있다.
예전엔 kubernetes 의 버전이 공개되었다고 하는데, 보안 취약점 으로 인해 사라졌다고 한다.

설치 직후에 pod 은 다음과 같다

아래 명령어를 실행해 보면서 기본 쿠버네티스 정보들을 확인 해 본다.
# 파드 정보 확인 : 온프레미스 쿠버네티스의 파드 배치와 다른점은? , 파드의 IP의 특징이 어떤가요? 자세한 네트워크는 2주차에서 다룸
kubectl get pod -n kube-system
kubectl get pod -n kube-system -o wide
kubectl get pod -A
# kube-system 네임스페이스에 모든 리소스 확인
kubectl get deploy,ds,pod,cm,secret,svc,ep,endpointslice,pdb,sa,role,rolebinding -n kube-system
# 모든 파드의 컨테이너 이미지 정보 확인 : dkr.ecr 저장소 확인!
kubectl get pods --all-namespaces -o jsonpath="{.items[*].spec.containers[*].image}" | tr -s '[[:space:]]' '\n' | sort | uniq -c
# kube-proxy : iptables mode, bind 0.0.0.0, conntrack 등
kubectl describe pod -n kube-system -l k8s-app=kube-proxy
kubectl get cm -n kube-system kube-proxy -o yaml
kubectl get cm -n kube-system kube-proxy-config -o yaml
# coredns
kubectl describe pod -n kube-system -l k8s-app=kube-dns
kubectl get cm -n kube-system coredns -o yaml
kubectl get pdb -n kube-system coredns -o jsonpath='{.spec}' | jq
# aws-node : 2개의 컨테이너 - aws-node(cni plugin), aws-eks-nodeagent(network policy agent)
kubectl describe pod -n kube-system -l k8s-app=aws-node
설치 된 add-on 도 확인 해 본다
aws eks list-addons --cluster-name myeks | jq
# 특정 Addon 상세 정보
aws eks describe-addon --cluster-name myeks --addon-name vpc-cni | jq
# 전체 Addon 상세 정보
aws eks list-addons --cluster-name myeks \
| jq -r '.addons[]' \
| xargs -I{} aws eks describe-addon \
--cluster-name myeks \
--addon-name {}설치된 add-on 정보를 aws cli 로 확인 해 본 모습이다.
아래는 aws web console 에서 확인 해 본 모습니다.

웹에서 워커노드를 확인 해 본다.
노드2개, 노드 그룹을 볼 수 있다.

aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
해당 ip 기준으로 ec2 를 접속해 본다.
NODE1=3.38.160.102
NODE2=54.180.104.75
# 노드 보안그룹 확인
aws ec2 describe-security-groups | jq
aws ec2 describe-security-groups --filters "Name=tag:Name,Values=myeks-node-group-sg" | jq
aws ec2 describe-security-groups --filters "Name=tag:Name,Values=myeks-node-group-sg" --query 'SecurityGroups[*].IpPermissions' --output textpem 키를 받아 -i 옵션을 통해 접근한다.
ssh -i ~/eks.pem -o StrictHostKeyChecking=no ec2-user@$NODE1 hostname
ssh -i ~/eks.pem -o StrictHostKeyChecking=no ec2-user@$NODE2 hostname접근이 되는 것을 확인할 수 있다. 접근이 되면 한번 설정을 확인 해 보자
나는 wsl 의 권한 문제때문에, 그냥 ssh 다른 데스크탑 앱으로 진행했다.
# 관리자 전환
sudo su -
whoami
# 호스트 정보 확인
hostnamectl
# SELinux 설정 : Kubernetes는 Permissive 권장
getenforce
sestatus
# Swap 비활성화
free -h
cat /etc/fstab
# cgroup 확인 : 버전2
stat -fc %T /sys/fs/cgroup/
cgroup2fs
# overlay 커널 모듈 로드 확인 : https://interlude-3.tistory.com/47
lsmod | grep overlay
# containerd 스냅샷 목록 보기
ctr -n k8s.io snapshots ls
ls -la /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/
tree /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/ -L 3
# 커널 파라미터 확인
tree /etc/sysctl.d/
cat /etc/sysctl.d/00-defaults.conf
cat /etc/sysctl.d/99-sysctl.conf
cat /etc/sysctl.d/99-amazon.conf
cat /etc/sysctl.d/99-kubernetes-cri.conf# 설정 확인
grep "^[^#]" /etc/chrony.conf
tree /run/chrony.d/
# time 서버 pool 확인
cat /usr/share/amazon-chrony-config/link-local-ipv4_unspecified.sources
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/set-time.html
server 169.254.169.123 prefer iburst minpoll 4 maxpoll 4
cat /usr/share/amazon-chrony-config/amazon-pool_aws.sources
# Use Amazon Public NTP leap-second smearing time sources
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/set-time.html#configure-time-sync
pool time.aws.com iburst
nslookup time.aws.com
# 상태 확인
timedatectl status
chronyc sources -v
# 기본 정보 확인
nerdctl info
# 동작 중인 컨테이너 확인
nerdctl ps
nerdctl images
nerdctl images | grep localhost
# 프로세스 확인
pstree -a
systemctl status containerd --no-pager -l
# 관련 설정 파일확인
tree /etc/containerd/
# 데몬 설정 확인
cat /etc/containerd/config.toml
# 컨테이너를 생성할 때 사용하는 기본 OCI runtime spec 확인
cat /etc/containerd/base-runtime-spec.json | jq
# containerd의 유닉스 도메인 소켓 확인 : kubelet에서 사용 , containerd client 3종(ctr, nerdctr, crictl)도 사용
containerd config dump | grep -n containerd.sock
ls -l /run/containerd/containerd.sock
ss -xl | grep containerd
ss -xnp | grep containerd
# 플러그인 확인
ctr --address /run/containerd/containerd.sock version
ctr plugins ls
ps afxuwww
systemctl status kubelet --no-pager
cat /etc/systemd/system/kubelet.service
# 관련 파일 확인
tree /etc/kubernetes/
/etc/kubernetes/
├── kubelet
│ ├── config.json
│ └── config.json.d
│ └── 40-nodeadm.conf
├── manifests # 별도 static pod 없음
└── pki
└── ca.crt
# k8s ca 인증서 확인 : 10년 유효 기간
cat /etc/kubernetes/pki/ca.crt | openssl x509 -text -noout
# kubelet 설정 파일 확인
cat /etc/kubernetes/kubelet/config.json | jq
# kubelet(client) -> eks api server(server) 호출 시
cat /var/lib/kubelet/kubeconfig
# eks api server(client) -> kubelet(server) 호출 시 : kubelet 이 https 서버 동작 시 TLS 서버 인증서
# 아래 SAN IP 'IP Address:16.184.28.160, IP Address:192.168.2.208' 는 해당 노드의 '사설, 공인' IP
curl ipinfo.io/ip ; echo
cat /var/lib/kubelet/pki/kubelet-server-current.pem | openssl x509 -text -noout
# cni 바이너리 확인
tree -pug /opt/cni/
/opt/cni/bin/aws-cni -h
cat /opt/cni/bin/aws-cni-support.sh
# aws vpc cni 플러그인 설정 확인
tree /etc/cni
cat /etc/cni/net.d/10-aws.conflist | jq# network 정보 확인
ip route
ip addr # eni 와 veth 확인
lsns -t net
# iptables 규칙 확인
iptables -t nat -S
iptables -t filter -S
iptables -t mangle -S
lsblk
df -hT
findmnt# cgroup 확인 : 버전2
stat -fc %T /sys/fs/cgroup/
findmnt |grep -i cgroup
# EKS node 전체 cgroup 구조
tree /sys/fs/cgroup/ -L 1
# 관련 툴
systemd-cgls
systemd-cgtop

Kubernetes 환경에서는 컨트롤 플레인과 워커 노드 간의 통신이 필수적이다. 예를 들어 kubectl logs나 exec 명령을 수행하려면 컨트롤 플레인이 워커 노드의 정보에 접근해야 한다.
하지만 EKS에서 컨트롤 플레인은 AWS가 관리하는 별도의 VPC에 위치하기 때문에, 기본적으로 고객의 VPC(워커 노드가 있는 곳)와 직접적인 통신 접점이 없다.
이를 해결하기 위해 위 그림처럼 EKS Owned ENI(AWS 소유의 교차 계정 네트워크 인터페이스)를 워커 노드의 서브넷에 생성하여 통신하게 된다. EKS의 API 서버 엔드포인트 액세스 방식은 크게 3가지로 나눌 수 있으며(아래 사진), 아키텍처는 아래 구성도를 첨부한다.

1. EKS Cluster Endpoint - Public

특징: EKS 클러스터를 생성할 때 기본적으로 설정되는 방식이다. API 서버의 엔드포인트가 인터넷을 통해 공개된다.
통신 흐름: 관리자가 외부 망에서 kubectl을 사용할 때뿐만 아니라, VPC 내부의 워커 노드가 컨트롤 플레인(API 서버)과 통신할 때도 인터넷 게이트웨이(IGW)를 거쳐 퍼블릭 네트워크를 통해 접근한다.

특징: 관리자의 편의성과 내부 네트워크의 보안성을 모두 챙길 수 있는 방식이다. 클러스터 외부에서는 퍼블릭하게, 내부에서는 프라이빗하게 접근한다.
통신 흐름:외부 접근: 관리자는 인터넷을 통해 퍼블릭 엔드포인트로 API 서버에 접속한다.
내부 접근: VPC 내부의 워커 노드나 파드(Pod)는 외부 인터넷으로 나가지 않고, EKS Owned ENI를 통해 AWS 내부망(프라이빗 네트워크)으로 API 서버와 안전하게 직접 통신한다.

특징: API 서버 엔드포인트에 대한 모든 퍼블릭 접근을 완전히 차단하는 가장 강력한 보안 설정이다. 사내 보안 규정이 엄격한 엔터프라이즈 환경에서 주로 사용한다.
통신 흐름: 워커 노드뿐만 아니라 관리자의 kubectl 명령 등 모든 API 요청이 반드시 VPC 내부망을 통해서만 이루어져야 한다.
현재 구축된 환경은 '퍼블릭(Public)' 액세스 모드이다. 엔드포인트가 외부로 열려 있어 네트워크상으로는 누구나 접근이 가능하다 (물론 자격 증명이 없으므로 실제 제어는 불가능하다).

실제로 생성된 EKS Owned ENI를 확인해 보았다. 내 AWS 계정 ID는 '9'로 시작하지만, 해당 ENI의 소유자는 내 계정이 아닌 AWS(EKS 서비스)로 되어 있는 것을 볼 수 있다.

워커 노드 내부에서 네트워크 통신 상태를 확인해 본다. 현재 퍼블릭 모드이므로 워커 노드가 API 서버의 공인 IP와 통신하고 있을 것이다.
워커 노드에서 실행해 본다.
sudo ss -tnp | grep -v ssh
이제 eks.tf 코드를 수정하여 EKS 액세스 엔드포인트를 **'퍼블릭 및 프라이빗'**으로 변경해 본다.
endpoint_public_access = true
endpoint_private_access = true
endpoint_public_access_cidrs = [
var.ssh_access_cidr
]코드를 수정한 뒤 terraform apply로 다시 배포를 진행한다. (엔드포인트 통신 방식을 변경하는 작업은 아키텍처가 바뀌는 것이라 시간이 꽤 오래 소요된다.)
업데이트가 완료되면 AWS 웹 콘솔에서도 API 서버 엔드포인트 액세스 설정이 퍼블릭 및 프라이빗으로 변경된 것을 확인할 수 있다.
이후 워커 노드(NODE1) 내부에서 API 서버의 DNS를 질의해 보자.
# NODE1 에서 eks 엔드포인트 DNS 질의 시, 공인 IP -> 내부 사설 IP로 변경!
# 동일 도메인에 대해서 각기 다른 응답값 리턴 원리 : 프라이빗/퍼블릭 DNS 이름 제공 기능 -> 이를 위해 vpc 에 'DNS resolution, DNS hostnames' 활성화 필요
while true; do ssh ec2-user@$NODE1 dig +short $APIDNS ; date ; echo "-----" ; sleep 1 ; done테스트 명령어를 실행해 보면, 기존에 공인 IP(Public IP)로 응답하던 API DNS가 내부 사설 IP(Private IP)로 변경되어 응답하는 것을 확인할 수 있다. 내 실습 환경에서는 즉각적인 변경 확인이 조금 늦어졌는데, 정상적으로 사설 IP로 전환된 화면을 첨부한다.

아래 사진과 같이 내부에서 api 서버 질의 시에는 내부 ip 를 가르키게 된다.
컨트롤 플레인에 직접 붙는게 아닌 워커노드에서 컨트롤 플레인 질의 시에 는 다음과 같이 된다.

자원을 제거한다.
rm -rf ~/.kube/config
nohup sh -c "terraform destroy -auto-approve" > delete.log 2>&1 &
tail -f delete.log해당 실습은 새로운 테라폼 파일로 실습한다.
1. versions.tf
terraform {
required_version = ">= 1.3"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.34, < 6.0"
}
}
# ## Used for end-to-end testing on project; update to suit your needs
# backend "s3" {
# bucket = "terraform-ssp-github-actions-state"
# region = "us-west-2"
# key = "e2e/fully-private-cluster/terraform.tfstate"
# }
}2. outputs.tf
output "configure_kubectl" {
description = "Configure kubectl: make sure you're logged in with the correct AWS profile and run the following command to update your kubeconfig"
value = "aws eks --region ${local.region} update-kubeconfig --name ${module.eks.cluster_name}"
}3.ec2.tf
variable "KeyName" {
# aws ec2 describe-key-pairs --query "KeyPairs[].KeyName" --output text
# export TF_VAR_KeyName=kp-gasida
description = "Name of an existing EC2 KeyPair to enable SSH access to the instances."
type = string
}
variable "ssh_access_cidr" {
# export TF_VAR_ssh_access_cidr=$(curl -s ipinfo.io/ip)/32
description = "Allowed CIDR for SSH access"
type = string
}
################################
# Security Group Configuration #
################################
# 보안 그룹: Bastion Host를 위한 보안 그룹을 생성
resource "aws_security_group" "eks_sec_group" {
vpc_id = module.vpc.vpc_id
name = "bastion-ec2-sg"
ingress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [var.ssh_access_cidr]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "bastion-ec2-sg"
}
}
######################
# EC2 Instance Setup #
######################
# 최신 Ubuntu 24.04 AMI ID를 AWS SSM Parameter Store에서 가져옴.
# aws ssm get-parameters-by-path --path "/aws/service/canonical/ubuntu/server/24.04" --recursive
data "aws_ssm_parameter" "ami" {
name = "/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id"
}
# EKS 클러스터 관리용 Bastion Host EC2 인스턴스를 생성.
resource "aws_instance" "eks_bastion" {
ami = data.aws_ssm_parameter.ami.value
instance_type = "t3.medium"
key_name = var.KeyName
subnet_id = module.vpc.public_subnets[0]
associate_public_ip_address = true
vpc_security_group_ids = [aws_security_group.eks_sec_group.id]
tags = {
Name = "bastion-ec2"
}
root_block_device {
volume_type = "gp3"
volume_size = 30
delete_on_termination = true
}
user_data = <<-EOF
#!/bin/bash
hostnamectl --static set-hostname "bastion-EC2"
# Config convenience
echo 'alias vi=vim' >> /etc/profile
echo "sudo su -" >> /home/ubuntu/.bashrc
timedatectl set-timezone Asia/Seoul
# Install Packages
apt update
apt install -y tree jq git htop unzip curl
# Install kubectl & helm
curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.34.2/2025-11-13/bin/linux/amd64/kubectl
install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
curl -s https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
# Install eksctl
curl -sL "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_Linux_amd64.tar.gz" | tar xz -C /tmp
mv /tmp/eksctl /usr/local/bin
# Install aws cli v2
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip >/dev/null 2>&1
./aws/install
complete -C '/usr/local/bin/aws_completer' aws
echo 'export AWS_PAGER=""' >> /etc/profile
# Install YAML Highlighter
snap install yq
# Install kube-ps1
echo 'source <(kubectl completion bash)' >> /root/.bashrc
echo 'alias k=kubectl' >> /root/.bashrc
echo 'complete -F __start_kubectl k' >> /root/.bashrc
git clone https://github.com/jonmosco/kube-ps1.git /root/kube-ps1
cat << "EOT" >> /root/.bashrc
source /root/kube-ps1/kube-ps1.sh
KUBE_PS1_SYMBOL_ENABLE=false
function get_cluster_short() {
echo "$1" | cut -d . -f1
}
KUBE_PS1_CLUSTER_FUNCTION=get_cluster_short
KUBE_PS1_SUFFIX=') '
PS1='$(kube_ps1)'$PS1
EOT
# Install kubectx & kubens
git clone https://github.com/ahmetb/kubectx /opt/kubectx >/dev/null 2>&1
ln -s /opt/kubectx/kubens /usr/local/bin/kubens
ln -s /opt/kubectx/kubectx /usr/local/bin/kubectx
EOF
user_data_replace_on_change = true
}
output "bastion_ec2-public_ip" {
value = aws_instance.eks_bastion.public_ip
description = "The public IP of the myeks-host EC2 instance."
}4. main.tf
provider "aws" {
region = local.region
}
data "aws_availability_zones" "available" {
# Do not include local zones
filter {
name = "opt-in-status"
values = ["opt-in-not-required"]
}
}
locals {
name = basename(path.cwd)
region = "ap-northeast-2"
vpc_cidr = "10.0.0.0/16"
azs = slice(data.aws_availability_zones.available.names, 0, 3)
tags = {
Blueprint = local.name
GithubRepo = "github.com/aws-ia/terraform-aws-eks-blueprints"
}
}
################################################################################
# Cluster
################################################################################
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.11"
cluster_name = local.name
cluster_version = "1.34"
# Optional: Adds the current caller identity as an administrator via cluster access entry
enable_cluster_creator_admin_permissions = true
# EKS Addons
cluster_addons = {
coredns = {}
kube-proxy = {}
vpc-cni = {}
}
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
eks_managed_node_groups = {
initial = {
instance_types = ["t3.medium"]
min_size = 1
max_size = 3
desired_size = 2
}
}
tags = local.tags
}
################################################################################
# Supporting Resources
################################################################################
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
manage_default_vpc = true
name = local.name
cidr = local.vpc_cidr
azs = local.azs
# Private Subnets: 인덱스 0, 1, 2 사용 (예: 10.0.0.0/20, 10.0.16.0/20...)
private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k)]
# Public Subnets: 인덱스 4, 5, 6 사용 (Private과 겹치지 않게 k + 4 처리)
public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k + 4)]
enable_nat_gateway = true
single_nat_gateway = true
private_subnet_tags = {
"kubernetes.io/role/internal-elb" = 1
}
public_subnet_tags = {
"kubernetes.io/role/elb" = 1
}
tags = local.tags
}
module "vpc_endpoints" {
source = "terraform-aws-modules/vpc/aws//modules/vpc-endpoints"
version = "~> 5.1"
vpc_id = module.vpc.vpc_id
# Security group
create_security_group = true
security_group_name_prefix = "${local.name}-vpc-endpoints-"
security_group_description = "VPC endpoint security group"
security_group_rules = {
ingress_https = {
description = "HTTPS from VPC"
cidr_blocks = [module.vpc.vpc_cidr_block]
}
}
endpoints = merge({
s3 = {
service = "s3"
service_type = "Gateway"
route_table_ids = module.vpc.private_route_table_ids
tags = {
Name = "${local.name}-s3"
}
}
},
{ for service in toset(["autoscaling", "ecr.api", "ecr.dkr", "ec2", "ec2messages", "elasticloadbalancing", "sts", "kms", "logs", "ssm", "ssmmessages"]) :
replace(service, ".", "_") =>
{
service = service
subnet_ids = module.vpc.private_subnets
private_dns_enabled = true
tags = { Name = "${local.name}-${service}" }
}
})
tags = local.tags
}폐쇄망이기 때문에 AWS 와 통신하기 위한 엔드포인트를 만든다.
폐쇄망이지만, vpc endpoint 를 이용해 aws 내부 서비스끼리는 통신을 위해 생성해야 한다.
추가로 전 실습과 다른 부분은 private subnet 에 위치한다.
5. 테라폼 배포
# 실습 디렉터리 진입
cd eks-private
# 변수 지정
aws ec2 describe-key-pairs --query "KeyPairs[].KeyName" --output text
export TF_VAR_KeyName=$(aws ec2 describe-key-pairs --query "KeyPairs[].KeyName" --output text)
export TF_VAR_ssh_access_cidr=$(curl -s ipinfo.io/ip)/32
echo $TF_VAR_KeyName $TF_VAR_ssh_access_cidr
# 초기화
terraform init
terraform plan
# vpc 배포 : 2분 소요 -> 이후 aws vpc 정보 확인!
terraform apply -target="module.vpc" -auto-approve
# eks 등 배포 : 14분 소요
terraform apply -auto-approve
# 자격증명 설정
aws eks --region ap-northeast-2 update-kubeconfig --name eks-private
kubectl config rename-context $(cat ~/.kube/config | grep current-context | awk '{print $2}') eks-private
# kubectl 조회 시도
kubectl get node -v=7
# 자격증명 삭제
rm -rf ~/.kube/config
private 기 때문에, 현재 서버에 접근되지 않는 것을 확인할 수 있다.
테라폼으로 생성한 bastion 서버로 접속한다.
# bastion ec2 접속
ssh -o StrictHostKeyChecking=no ubuntu@$(terraform output -raw bastion_ec2-public_ip)
# admin IAM User 자격 증명 설정
aws configure
# 자격증명 설정
aws eks --region ap-northeast-2 update-kubeconfig --name eks-private
kubectl config rename-context $(cat ~/.kube/config | grep current-context | awk '{print $2}') eks-private
# eks access endpoint 확인
APIDNS=$(aws eks describe-cluster --name eks-private | jq -r .cluster.endpoint | cut -d '/' -f 3)
echo $APIDNS
dig +short $APIDNS
# kubectl 조회 시도 : DNS Lookup resolved" host="4924E16AD07400AA0CA66718E179E887.gr7.ap-northeast-2.eks.amazonaws.com" address=[{"IP":"10.0.28.171","Zone":""},{"IP":"10.0.14.147","Zone":""}]
kubectl cluster-info
kubectl get node -v=9
통신이 되지 않는다. 인바운드 규칙에 해당 배스천 서버에서 오는 요청에 대한 rule 이 없기 때문에 추가를 해 줘야 한다.
보안 그룹 설정에 들어가서 클러스터 보안 그룹을 눌러 룰을 추가한다.

룰 추가 시 아래와 같이 소스를 배스쳔 서버의 security group 을 선택한다.

해당 룰 설정 이후에 아래와 같이 통신 가능한 것을 확인할 수 있다.

# 노드 정보 확인
kubectl get node -owide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
ip-10-0-36-91.ap-northeast-2.compute.internal Ready <none> 44m v1.34.4-eks-f69f56f 10.0.36.91 <none> Amazon Linux 2023.10.20260302 6.12.73-95.123.amzn2023.x86_64 containerd://2.1.5
ip-10-0-8-125.ap-northeast-2.compute.internal Ready <none> 44m v1.34.4-eks-f69f56f 10.0.8.125 <none> Amazon Linux 2023.10.20260302 6.12.73-95.123.amzn2023.x86_64 containerd://2.1.5
# node-shell 파드 배포
cat << EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: node-shell
spec:
containers:
- name: debug
image: public.ecr.aws/docker/library/alpine:latest
command: ["/bin/sh", "-c", "sleep 36000"]
securityContext:
privileged: true
volumeMounts:
- mountPath: /host
name: hostfs
hostNetwork: true
hostPID: true
hostIPC: true
volumes:
- name: hostfs
hostPath:
path: /
EOF
# 파드 내에서 chroot 실행으로 호스트 노드 진입!
kubectl exec -it node-shell -- chroot /host /bin/bash
-----------------------------------------------------
[root@ip-10-0-18-178 /]# hostnamectl
[root@ip-10-0-18-178 /]# id
[root@ip-10-0-18-178 /]# ip addr
[root@ip-10-0-18-178 /]# ss -tnp
exit
-----------------------------------------------------
위 사진을 보면 모든 ip 가 전부 내부로 잡혀 있는 것을 볼 수 있다.
해당 부분으로 실습을 마무리하고, 아까 보안 그룹 추가 했던 것을 지우고 테라폼으로 다시 삭제한다.
terraform destroy -auto-approve 명령어를 이용해 삭제 한다.
추가로 루트로 진행하셨던 분들은 access key를 비활성화 하거나 제거하기 바란다.