[2주차] EKS Networking - #1
CloudNet@ 가시다님이 진행하는 AWS EKS Hands-on Study 내용 참고.
0. 실습 환경 배포
최종 구성도 : 2개의 VPC(EKS 배포, 운영용 구분), 운영서버 EC2 + EKS 제어부, EKS 데이터부(관리형 노드 그룹, AL2023 - 페도라 기반, dnf - Docs)
- myeks-vpc 에 각기 AZ를 사용하는 퍼블릭/프라이빗 서브넷 배치
- operator-vpc 에 AZ1를 사용하는 퍼블릭/프라이빗 서브넷 배치
- 내부 통신을 위한 VPC Peering 배치
실습 순서
- 자신의 PC에 실습을 위한 툴 및 설정 : Windows(WSL2 사용 권장)
- AWS CloudFormation 을 통해 기본 실습 환경 배포 : 2개의 VPC, 운영 서버 EC2 1대
- eksctl 을 통해 EKS 배포
- 관리형 노드 그룹(EC2) 접속
☞ 자신의 PC에 실습을 위한 툴 및 설정 : Windows (WSL2) + VSCODE 연동
1. Windows 에 WSL2 (Ubuntu 24.04 배포판) 설치 - Docs , 설명서
- Powershell 관리자 권한으로 실행
# DISM(배포 이미지 서비스 및 관리) 명령어로 Microsoft-Windows-Subsystem-Linux 기능을 활성화
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
# DISM 명령어로 VirtualMachinePlatform 기능을 활성화
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
# wsl 설치
wsl --install
# 기본값 WSL 2 버전 설정
wsl --set-default-version 2
# wsl 업데이트
wsl --update
- 윈도우 OS 재부팅
- Powershell 관리자 권한으로 실행 (윈도우즈 터미널을 설치하여 이용)
# 설치 가능한 배포판 확인
wsl --list --online
# Ubuntu 배포판 설치
wsl --install Ubuntu-24.04
...
Enter new UNIX username: <각자 Ubuntu 사용 계정>
New password: <해당 계정 암호>
Retype new password: <해당 계정 암호>
passwd: password updated successfully
Installation successful!
To run a command as administrator (user "root"), use "sudo <command>".
---------------------------------------
# 기본 정보 확인
hostnamectl
whoami
id
pwd
# apt 업데이트
sudo apt update
sudo apt install jq htop curl wget ca-certificates net-tools -y
ifconfig eth0
ping -c 1 8.8.8.8
# 빠져나오기
$ exit
---------------------------------------
# 설치된 배포판 확인
wsl -l -v
# Ubuntu 재진입
wsl
2. [WSL Ubuntu] 필수 툴 설치
# Install awscli
sudo snap install aws-cli --classic
aws --version
# Install eksctl
curl -sL "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_Linux_amd64.tar.gz" | tar xz -C ./
sudo mv ./eksctl /usr/local/bin
eksctl version
# Install kubectl
curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.31.2/2024-11-15/bin/linux/amd64/kubectl
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
kubectl version --client=true
# Install Helm
curl -s https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
helm version
# krew 툴 및 플러그인 설치
curl -sL https://github.com/kubernetes-sigs/krew/releases/download/v0.4.4/krew-linux_amd64.tar.gz | tar xz -C ./
./krew-linux_amd64 install krew
export PATH="$HOME/.krew/bin:$PATH"
echo 'export PATH="$HOME/.krew/bin:$PATH"' >> ~/.bashrc
kubectl krew version
kubectl krew install ctx ns neat get-all df-pv stern
kubectl krew list
# kubectl 단축 및 자동 완성 설정
echo 'source <(kubectl completion bash)' >> ~/.bashrc
echo 'alias k=kubectl' >> ~/.bashrc
echo 'complete -F __start_kubectl k' >> ~/.bashrc
# kube-ps1 설치
git clone https://github.com/jonmosco/kube-ps1.git $HOME/kube-ps1
cat <<"EOT" >> ~/.bashrc
source $HOME/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
# (옵션) kubecolor 설치 : 사용 시에는 kubectl(k) 대신 kubecolor 사용
curl -sL "https://github.com/kubecolor/kubecolor/releases/download/v0.5.0/kubecolor_0.5.0_linux_amd64.tar.gz" | tar xz -C ./
sudo mv ./kubecolor /usr/local/bin
kubecolor -h
3. [WSL Ubuntu] AWS Configure 자격 증명 설정 ⇒ 업무용 PC일 경우 aws profile 를 구별해서 사용을 권장
# 자격 구성 설정 없이 확인
aws ec2 describe-instances
# IAM User 자격 구성 : 실습 편리를 위해 administrator 권한을 가진 IAM User 의 자격 증명 입력
aws configure
AWS Access Key ID [None]: AKIA5...
AWS Secret Access Key [None]: CVNa2...
Default region name [None]: ap-northeast-2
Default output format [None]: json
# 자격 구성 적용 확인 : 노드 IP 확인
aws ec2 describe-instances
4. [WSL Ubuntu] 우분투 내부에 파일을 VSCODE 에서 변경 시 - Docs , Blog
# 사용자 기본 디렉터리에서 아래 입력
code .
☞ AWS CloudFormation 을 통해 기본 실습 환경 배포
# yaml 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/myeks-2week.yaml
# 배포
# aws cloudformation deploy --template-file myeks-1week.yaml --stack-name mykops --parameter-overrides KeyName=<My SSH Keyname> SgIngressSshCidr=<My Home Public IP Address>/32 --region <리전>
예시) aws cloudformation deploy --template-file myeks-2week.yaml \
--stack-name myeks --parameter-overrides KeyName=kp-gasida SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2
# CloudFormation 스택 배포 완료 후 운영서버 EC2 IP 출력
aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[*].OutputValue' --output text
예시) 3.36.89.204
# 운영서버 EC2 에 SSH 접속
예시) ssh ec2-user@3.36.89.204
ssh -i <ssh 키파일> ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
- 배포된 리소스 정보 확인 : 운영서버 EC2, VPC(DNS 설정 옵션), VPC Peering, Routing Table
☞ eksctl 을 통해 EKS 배포
- 배포할 YAML 파일 작성
#
export CLUSTER_NAME=myeks
# myeks-VPC/Subnet 정보 확인 및 변수 지정
export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" --query 'Vpcs[*].VpcId' --output text)
echo $VPCID
export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet3=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet3" --query "Subnets[0].[SubnetId]" --output text)
echo $PubSubnet1 $PubSubnet2 $PubSubnet3
# 출력된 내용 참고 : 아래 yaml 파일 참고해서 vpc/subnet id, ssh key 경로 수정
eksctl create cluster --name $CLUSTER_NAME --region=ap-northeast-2 --nodegroup-name=ng1 --node-type=t3.medium --nodes 3 --node-volume-size=30 --vpc-public-subnets "$PubSubnet1","$PubSubnet2","$PubSubnet3" --version 1.31 --with-oidc --external-dns-access --full-ecr-access --alb-ingress-access --node-ami-family AmazonLinux2023 --ssh-access --dry-run > myeks.yaml
-------------------------------------------
# 미사용
#export PrivateSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PrivateSubnet1" --query "Subnets[0].[SubnetId]" --output text)
#export PrivateSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PrivateSubnet2" --query "Subnets[0].[SubnetId]" --output text)
#export PrivateSubnet3=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PrivateSubnet3" --query "Subnets[0].[SubnetId]" --output text)
#echo $PrivateSubnet1 $PrivateSubnet2 $PrivateSubnet3
# ssh 퍼블릭 키 경로 지정
#SshPublic=<각자 자신의 ssh 퍼블릭 키 경로>
#SshPublic=~/.ssh/kp-gasida.pub
#echo $SshPublic
#eksctl create cluster --name $CLUSTER_NAME --region=ap-northeast-2 --nodegroup-name=ng1 --node-type=t3.medium --nodes 3 --node-volume-size=30 --vpc-public-subnets "$PubSubnet1","$PubSubnet2","$PubSubnet3" --version 1.31 --with-oidc --external-dns-access --full-ecr-access --alb-ingress-access --node-ami-family AmazonLinux2023 --ssh-access --ssh-public-key $SshPublic --dry-run > myeks.yaml
- myeks.yaml 파일 작성 : ssm 접속은 기본값 적용 (위에서 작성된 파일을 아래 내용을 참고하여 파일을 수정함)
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: myeks
region: ap-northeast-2
version: "1.31"
kubernetesNetworkConfig:
ipFamily: IPv4
iam:
vpcResourceControllerPolicy: true
withOIDC: true
accessConfig:
authenticationMode: API_AND_CONFIG_MAP
vpc:
autoAllocateIPv6: false
cidr: 192.168.0.0/16
clusterEndpoints:
privateAccess: true # if you only want to allow private access to the cluster
publicAccess: true # if you want to allow public access to the cluster
id: vpc-0cb2f40712c36fa60 # 각자 환경 정보로 수정
manageSharedNodeSecurityGroupRules: true # if you want to manage the rules of the shared node security group
nat:
gateway: Disable
subnets:
public:
ap-northeast-2a:
az: ap-northeast-2a
cidr: 192.168.1.0/24
id: subnet-0984a86959241f4f7 # 각자 환경 정보로 수정
ap-northeast-2b:
az: ap-northeast-2b
cidr: 192.168.2.0/24
id: subnet-0dcb19bbf91ec114a # 각자 환경 정보로 수정
ap-northeast-2c:
az: ap-northeast-2c
cidr: 192.168.3.0/24
id: subnet-04cb37ad06b1b0c6c # 각자 환경 정보로 수정
addons:
- name: vpc-cni # no version is specified so it deploys the default version
version: latest # auto discovers the latest available
attachPolicyARNs: # attach IAM policies to the add-on's service account
- arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
configurationValues: |-
enableNetworkPolicy: "true"
- name: kube-proxy
version: latest
- name: coredns
version: latest
- name: metrics-server
version: latest
privateCluster:
enabled: false
skipEndpointCreation: false
managedNodeGroups:
- amiFamily: AmazonLinux2023
desiredCapacity: 3
disableIMDSv1: true
disablePodIMDS: false
iam:
withAddonPolicies:
albIngress: false # Disable ALB Ingress Controller
appMesh: false
appMeshPreview: false
autoScaler: false
awsLoadBalancerController: true # Enable AWS Load Balancer Controller
certManager: true # Enable cert-manager
cloudWatch: false
ebs: false
efs: false
externalDNS: true # Enable ExternalDNS
fsx: false
imageBuilder: true
xRay: false
instanceSelector: {}
instanceType: t3.medium
preBootstrapCommands:
# install additional packages
- "dnf install nvme-cli links tree tcpdump sysstat ipvsadm ipset bind-utils htop -y"
# disable hyperthreading
- "for n in $(cat /sys/devices/system/cpu/cpu*/topology/thread_siblings_list | cut -s -d, -f2- | tr ',' '\n' | sort -un); do echo 0 > /sys/devices/system/cpu/cpu${n}/online; done"
labels:
alpha.eksctl.io/cluster-name: myeks
alpha.eksctl.io/nodegroup-name: ng1
maxSize: 3
minSize: 3
name: ng1
privateNetworking: false
releaseVersion: ""
securityGroups:
withLocal: null
withShared: null
ssh:
allow: true
publicKeyName: kp-gasida # 각자 환경 정보로 수정
tags:
alpha.eksctl.io/nodegroup-name: ng1
alpha.eksctl.io/nodegroup-type: managed
volumeIOPS: 3000
volumeSize: 30
volumeThroughput: 125
volumeType: gp3
- 최종 yaml 로 eks 배포
# kubeconfig 파일 경로 위치 지정 :
export KUBECONFIG=$HOME/kubeconfig
혹은 각자 편한 경로 위치에 파일 지정
export KUBECONFIG=~/Downloads/kubeconfig
# 배포
eksctl create cluster -f myeks.yaml --verbose 4
- 배포 후 기본 정보 확인
- EKS 관리 콘솔 확인
- Overview : API server endpoint, Open ID Connect provider URL기본 정보(oidc)
- Compute : Node groups 클릭 → AMI(AL2023)..
- Networking : access(public and private)..
- Add-ons : VPC CNI 클릭 → edit 후 권한 설정 확인(IRSA) ⇒ 해당 IAM Role 확인 ← 보안 진행 주차에서 상세히 소개
- Access : IAM access entries (설치 시 사용한 자격증명 username 확인) ← 보안 진행 주차에서 상세히 소개
- EKS 정보 확인
#
kubectl cluster-info
eksctl get cluster
# 네임스페이스 default 변경 적용
kubens default
#
kubectl ctx
cat $KUBECONFIG | grep current-context
kubectl config rename-context "<각자 자신의 IAM User>@myeks.ap-northeast-2.eksctl.io" "eksworkshop"
kubectl config rename-context "terraform.user@myeks.ap-northeast-2.eksctl.io" "eksworkshop"
cat $KUBECONFIG | grep current-context
#
kubectl get node --label-columns=node.kubernetes.io/instance-type,eks.amazonaws.com/capacityType,topology.kubernetes.io/zone
kubectl get node -v=6
#
kubectl get pod -A
kubectl get pdb -n kube-system
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
coredns N/A 1 1 28m
metrics-server N/A 1 1 28m
# 관리형 노드 그룹 확인
eksctl get nodegroup --cluster $CLUSTER_NAME
aws eks describe-nodegroup --cluster-name $CLUSTER_NAME --nodegroup-name ng1 | jq
# eks addon 확인
eksctl get addon --cluster $CLUSTER_NAME
NAME VERSION STATUS ISSUES IAMROLE UPDATE AVAILABLE CONFIGURATION VALUES POD IDENTITY ASSOCIATION ROLES
coredns v1.11.4-eksbuild.2 ACTIVE 0
kube-proxy v1.31.3-eksbuild.2 ACTIVE 0
metrics-server v0.7.2-eksbuild.2 ACTIVE 0
vpc-cni v1.19.2-eksbuild.5 ACTIVE 0 arn:aws:iam::339712784283:role/eksctl-myeks-addon-vpc-cni-Role1-hs0ftpt5kfYV enableNetworkPolicy: "true"
- EC2 관리 콘솔 확인 : type, az, IP, ec2 instance profile → iam role 확인
☞ 관리형 노드 그룹(EC2) 접속 및 노드 정보 확인
- 관리 콘솔 EC2 서비스 : 관리형 노드 그룹(EC2) 에 보안그룹 ID 확인
- 해당 보안그룹 inbound 에 자신의 집 공인 IP 추가 후 접속 확인
# 인스턴스 공인 IP 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{InstanceID:InstanceId, PublicIPAdd:PublicIpAddress, PrivateIPAdd:PrivateIpAddress, InstanceName:Tags[?Key=='Name']|[0].Value, Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
# 인스턴스 공인 IP 변수 지정
export N1=<az1 배치된 EC2 공인 IP>
export N2=<az2 배치된 EC2 공인 IP>
export N3=<az3 배치된 EC2 공인 IP>
export N1=3.38.210.206
export N2=43.201.41.106
export N3=3.39.22.93
echo $N1, $N2, $N3
# ping 테스트
ping -c 2 $N1
ping -c 2 $N2
# *nodegroup-ng1* 포함된 보안그룹 ID
export MNSGID=<각자 자신의 관리형 노드 그룹(EC2) 에 보안그룹 ID>
export MNSGID=sg-044e77f896ab525d4
# 해당 보안그룹 inbound 에 자신의 집 공인 IP 룰 추가
aws ec2 authorize-security-group-ingress --group-id $MNSGID --protocol '-1' --cidr $(curl -s ipinfo.io/ip)/32
# 해당 보안그룹 inbound 에 운영서버 내부 IP 룰 추가
aws ec2 authorize-security-group-ingress --group-id $MNSGID --protocol '-1' --cidr 172.20.1.100/32
# AWS EC2 관리 콘솔에서 EC2에 보안 그룹에 inbound rule 에 추가된 규칙 정보 확인
# ping 테스트
ping -c 2 $N1
ping -c 2 $N2
# 워커 노드 SSH 접속
ssh -i <SSH 키> -o StrictHostKeyChecking=no ec2-user@$N1 hostname
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh -o StrictHostKeyChecking=no ec2-user@$
i hostname; echo; done
ssh ec2-user@$N1
exit
ssh ec2-user@$N2
exit
ssh ec2-user@$N2
exit
------------------
# 운영서버 EC2에서 접속 시
## 인스턴스 공인 IP 변수 지정
export N1=<az1 배치된 EC2 내부 IP>
export N2=<az2 배치된 EC2 내부 IP>
export N3=<az3 배치된 EC2 내부 IP>
export N1=192.168.1.222
export N2=192.168.2.128
export N3=192.168.3.86
echo $N1, $N2, $N3
## ping 테스트
ping -c 2 $N1
ping -c 2 $N2
- (옵션) AWS EC2 System Manager - Session Manager 로 관리형 노드 그룹(EC2) 접속
LabGuide - AWS Systems Manager Session Manager
- 방안1 : 터미널에서 접속
# 인스턴스 ID 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{InstanceID:InstanceId, PublicIPAdd:PublicIpAddress, PrivateIPAdd:PrivateIpAddress, InstanceName:Tags[?Key=='Name']|[0].Value, Status:State.Name}" --filters Name=instance-state-name,Values=running --output text
# Session Manager 를 통한 접속
aws ssm start-session --target i-08de73b8e3d968f24
--------------------------------------------------
# 기본 사용자 정보 확인
whoami
pwd
# bash shell 적용
bash
whoami
pwd
# 기본 정보 확인
hostnamectl
# sudo 권한 사용 확인 >> 가능한 이유는? ChatGPT 등에 물어보시라!
sudo cat /etc/passwd
# 빠져나오기
exit
exit
--------------------------------------------------
- 방안2 : 관리 콘솔 AWS EC2 System Manager - Session Manager 에서 접속 - Link
- 세션 종료 후 로깅 확인해보기
- 노드 정보 확인
# 노드 정보 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i hostnamectl; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c addr; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo iptables -t nat -S; echo; done
# Node cgroup version : v1(tmpfs), v2(cgroup2fs) - Link
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i stat -fc %T /sys/fs/cgroup/; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i findmnt -t cgroup2; echo; done
#
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo systemctl status kubelet; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i ps axf |grep /usr/bin/containerd; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo tree /etc/kubernetes/kubelet/; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /etc/kubernetes/kubelet/config.json | jq; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo cat /etc/kubernetes/kubelet/config.json.d/00-nodeadm.conf | jq; echo; done
#
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i lsblk; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i df -hT /; echo; done
# 컨테이너 리스트 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ctr -n k8s.io container list; echo; done
CONTAINER IMAGE RUNTIME
28b6a15c475e32cd8777c1963ba684745573d0b6053f80d2d37add0ae841eb45 602401143452.dkr.ecr-fips.us-east-1.amazonaws.com/eks/pause:3.5 io.containerd.runc.v2
4f266ebcee45b133c527df96499e01ec0c020ea72785eb10ef63b20b5826cf7c 602401143452.dkr.ecr-fips.us-east-1.amazonaws.com/eks/pause:3.5 io.containerd.runc.v2
...
# 컨테이너 이미지 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ctr -n k8s.io image list --quiet; echo; done
...
# 태스크 리스트 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ctr -n k8s.io task list; echo; done
☞ 운영서버 EC2에서 eks 를 사용 할 수 있게 설정
# eks 설치한 iam 자격증명을 설정하기
aws configure
...
# get-caller-identity 확인
aws sts get-caller-identity --query Arn
# kubeconfig 생성
cat ~/.kube/config
aws eks update-kubeconfig --name myeks --user-alias <위 출력된 자격증명 사용자>
aws eks update-kubeconfig --name myeks --user-alias admin
# 추가된 kubeconfig 정보 확인
cat ~/.kube/config
# eks api dig 조회 : VPC 내부에서 질의하는데 왜 그럴까? private hosted zone 의 특징을 알아보자
APIDNS=$(aws eks describe-cluster --name myeks | jq -r .cluster.endpoint | cut -d '/' -f 3)
dig +short $APIDNS
#
kubectl cluster-info
kubectl ns default
kubectl get node -v6
☞ 실습에서 자주 사용하는 변수
#
export KUBECONFIG=~/.kube/kubeconfig
export CLUSTER_NAME=myeks
# myeks-VPC/Subnet 정보 확인 및 변수 지정
export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" --query 'Vpcs[*].VpcId' --output text)
echo $VPCID
export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet3=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet3" --query "Subnets[0].[SubnetId]" --output text)
echo $PubSubnet1 $PubSubnet2 $PubSubnet3
# 인스턴스 IP 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{InstanceID:InstanceId, PublicIPAdd:PublicIpAddress, PrivateIPAdd:PrivateIpAddress, InstanceName:Tags[?Key=='Name']|[0].Value, Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
# 인스턴스 공인 IP 변수 지정
#export N1=<az1 배치된 EC2 공인 IP>
#export N2=<az2 배치된 EC2 공인 IP>
#export N3=<az3 배치된 EC2 공인 IP>
export N1=3.38.210.206
export N2=43.201.41.106
export N3=3.39.22.93
echo $N1, $N2, $N3
# 노드 정보 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i hostnamectl; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c addr; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo iptables -t nat -S; echo; done
# 파드 이름 변수 지정
PODNAME1=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[0].metadata.name}')
PODNAME2=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[1].metadata.name}')
PODNAME3=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[2].metadata.name}')
# 파드 IP 변수 지정
PODIP1=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[0].status.podIP}')
PODIP2=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[1].status.podIP}')
PODIP3=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[2].status.podIP}')
# 자신의 도메인 변수 지정 : 소유하고 있는 자신의 도메인을 입력하시면 됩니다
MyDomain=<자신의 도메인>
MyDomain=gasida.link
MyDnzHostedZoneId=`aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text`
echo $MyDnzHostedZoneId
# A 레코드 값 반복 조회
while true; do aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq ; date ; echo ; sleep 1; done
1. AWS VPC CNI 소개
K8S CNI : Container Network Interface 는 k8s 네트워크 환경을 구성해준다 - 링크, 다양한 플러그인이 존재 - 링크
아래 링크에서 내용을 확인해 보세요.
☞ AWS VPC CNI : 파드의 IP를 할당해준다, 파드의 IP 네트워크 대역과 노드(워커)의 IP 대역이 같아서 직접 통신이 가능하다 - Github Proposal
- Amazon EKS는 VPC CNI라고도 하는 Amazon VPC 컨테이너 네트워크 인터페이스 플러그인을 통해 클러스터 네트워킹을 구현합니다. CNI 플러그인을 사용하면 Kubernetes 포드가 VPC 네트워크에서와 동일한 IP 주소를 가질 수 있습니다. 보다 구체적으로 말해서 포드 내의 모든 컨테이너는 네트워크 네임스페이스를 공유하며 로컬 포트를 사용하여 서로 통신할 수 있습니다.
- Amazon VPC CNI에는 두 가지 구성 요소가 있습니다.
- 포드 간 통신을 활성화Pod-to-Pod 네트워크를 설정하는 CNI 바이너리입니다. CNI 바이너리는 노드 루트 파일 시스템에서 실행되며 새 포드가에 추가되거나 노드에서 기존 포드가 제거될 때 kubelet에 의해 호출됩니다.
- 장기 실행 노드-로컬 IP 주소 관리(IPAM) 데몬인 ipamd는 다음을 담당합니다.
- 노드에서 ENIs 관리
- 사용 가능한 IP 주소 또는 접두사의 웜 풀 유지 관리
- K8S Calico CNI 와 AWS VPC CNI 차이
- 네트워크 통신의 최적화(성능, 지연)를 위해서 노드와 파드의 네트워크 대역을 동일하게 설정함
- 파드간 통신 시 일반적으로 K8S CNI는 오버레이(VXLAN, IP-IP 등) 통신을 하고, AWS VPC CNI는 동일 대역으로 직접 통신을 한다
☞ 워커 노드에 생성 가능한 최대 파드 갯수 : 여러가지 최대 파드 배치 제한 조건(cpu/mem 리소스, disk/pid 부족 등) 중 파드 IP 관련 살펴 보기 - 링크
1. Secondary IPv4 addresses : 인스턴스 유형에 최대 ENI 갯수와 할당 가능 IP 수를 조합하여 선정
2. IPv4 Prefix Delegation : IPv4 28bit 서브넷(prefix)를 위임하여 할당 가능 IP 수와 인스턴스 유형에 권장하는 최대 갯수로 선정
3. AWS VPC CNI Custom Networking : 노드와 파드 대역 분리, 파드에 별도 서브넷 부여 후 사용 - Docs
☞ [실습] 네트워크 기본 정보 확인
# CNI 정보 확인
kubectl describe daemonset aws-node --namespace kube-system | grep Image | cut -d "/" -f 2
# kube-proxy config 확인 : 모드 iptables 사용 >> ipvs 모드로 변경 해보자!
kubectl describe cm -n kube-system kube-proxy-config
...
mode: "iptables"
...
# 노드 IP 확인
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 확인
kubectl get pod -n kube-system -o=custom-columns=NAME:.metadata.name,IP:.status.podIP,STATUS:.status.phase
# 파드 이름 확인
kubectl get pod -A -o name
# 파드 갯수 확인
kubectl get pod -A -o name | wc -l
- 노드에 네트워크 정보 확인
# CNI 정보 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i tree /var/log/aws-routed-eni; echo; done
ssh ec2-user@$N1 sudo cat /var/log/aws-routed-eni/plugin.log | jq
ssh ec2-user@$N1 sudo cat /var/log/aws-routed-eni/ipamd.log | jq
ssh ec2-user@$N1 sudo cat /var/log/aws-routed-eni/egress-v6-plugin.log | jq
ssh ec2-user@$N1 sudo cat /var/log/aws-routed-eni/ebpf-sdk.log | jq
ssh ec2-user@$N1 sudo cat /var/log/aws-routed-eni/network-policy-agent.log | jq
# 네트워크 정보 확인 : eniY는 pod network 네임스페이스와 veth pair
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -br -c addr; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c addr; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
ssh ec2-user@$N1 sudo iptables -t nat -S
ssh ec2-user@$N1 sudo iptables -t nat -L -n -v
2. 노드에서 기본 네트워크 정보 확인
☞ 워커 노드1 기본 네트워크 구성 : 워커 노드2 는 구성이 유사하여 생략
- Network 네임스페이스는 호스트(Root)와 파드 별(Per Pod)로 구분된다
- 특정한 파드(kube-proxy, aws-node)는 호스트(Root)의 IP를 그대로 사용한다 ⇒ 파드의 Host Network 옵션
- [Kubernetes] Pod 관련 Host Network 옵션과 동작원리
- t3.medium 의 경우 ENI 마다 최대 6개의 IP를 가질 수 있다
- ENI0, ENI1 으로 2개의 ENI는 자신의 IP 이외에 추가적으로 5개의 보조 프라이빗 IP를 가질수 있다
- coredns 파드는 veth 으로 호스트에는 eniY@ifN 인터페이스와 파드에 eth0 과 연결되어 있다
워커 노드1 인스턴스의 네트워크 정보 확인 : 프라이빗 IP와 보조 프라이빗 IP 확인
- 네트워크인터페이스(ENI)에 설명 내용 확인해보자 : 주ENI와 추가ENI의 설명 차이점 확인 - Link
☞ [실습] 보조 IPv4 주소를 파드가 사용하는지 확인
# coredns 파드 IP 정보 확인
kubectl get pod -n kube-system -l k8s-app=kube-dns -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-6777fcd775-57k77 1/1 Running 0 70m 192.168.1.142 ip-192-168-1-251.ap-northeast-2.compute.internal <none> <none>
coredns-6777fcd775-cvqsb 1/1 Running 0 70m 192.168.2.75 ip-192-168-2-34.ap-northeast-2.compute.internal <none> <none>
# 노드의 라우팅 정보 확인 >> EC2 네트워크 정보의 '보조 프라이빗 IPv4 주소'와 비교해보자
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
☞ [실습] 테스트용 netshoot-pod 디플로이먼트 생성 - nicolaka/netshoot
# [터미널1~3] 노드 모니터링
ssh ec2-user@$N1
watch -d "ip link | egrep 'ens|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
ssh ec2-user@$N2
watch -d "ip link | egrep 'ens|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
ssh ec2-user@$N3
watch -d "ip link | egrep 'ens|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
# 테스트용 netshoot-pod 디플로이먼트 생성
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: netshoot-pod
spec:
replicas: 3
selector:
matchLabels:
app: netshoot-pod
template:
metadata:
labels:
app: netshoot-pod
spec:
containers:
- name: netshoot-pod
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOF
# 파드 이름 변수 지정
PODNAME1=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[0].metadata.name}')
PODNAME2=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[1].metadata.name}')
PODNAME3=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[2].metadata.name}')
# 파드 확인
kubectl get pod -o wide
kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP
# 노드에 라우팅 정보 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
- 파드가 생성되면, 워커 노드에 eniY@ifN 추가되고 라우팅 테이블에도 정보가 추가된다
- 테스트용 파드 eniY 정보 확인 - 워커 노드 EC2
# 노드3에서 네트워크 인터페이스 정보 확인
ssh ec2-user@$N3
----------------
ip -br -c addr show
ip -c link
ip -c addr
ip route # 혹은 route -n
# 네임스페이스 정보 출력 -t net(네트워크 타입)
sudo lsns -t net
# PID 정보로 파드 정보 확인
PID=<PID> # PID 높은 것 중 COMMAND가 pause 인것
sudo nsenter -t $PID -n ip -c addr
sudo nsenter -t $PID -n ip -c route
exit
----------------
- 테스트용 파드 접속(exec) 후 확인
# 테스트용 파드 접속(exec) 후 Shell 실행
kubectl exec -it $PODNAME1 -- zsh
# 아래부터는 pod-1 Shell 에서 실행 : 네트워크 정보 확인
----------------------------
ip -c addr
ip -c route
route -n
ping -c 1 <pod-2 IP>
ps
cat /etc/resolv.conf
exit
----------------------------
# 파드2 Shell 실행
kubectl exec -it $PODNAME2 -- ip -c addr
# 파드3 Shell 실행
kubectl exec -it $PODNAME3 -- ip -br -c addr
3. 노드 간 파드 통신
목표 : 파드간 통신 시 tcpdump 내용을 확인하고 통신 과정을 알아본다
파드간 통신 흐름 : AWS VPC CNI 경우 별도의 오버레이(Overlay) 통신 기술 없이, VPC Native 하게 파드간 직접 통신이 가능하다
- 파드간 통신 시 과정 참고
☞ [실습] 파드간 통신 테스트 및 확인 : 별도의 NAT 동작 없이 통신 가능!
# 파드 IP 변수 지정
PODIP1=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[0].status.podIP}')
PODIP2=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[1].status.podIP}')
PODIP3=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[2].status.podIP}')
# 파드1 Shell 에서 파드2로 ping 테스트
kubectl exec -it $PODNAME1 -- ping -c 2 $PODIP2
# 파드2 Shell 에서 파드3로 ping 테스트
kubectl exec -it $PODNAME2 -- ping -c 2 $PODIP3
# 파드3 Shell 에서 파드1로 ping 테스트
kubectl exec -it $PODNAME3 -- ping -c 2 $PODIP1
# 워커 노드 EC2 : TCPDUMP 확인
## For Pod to external (outside VPC) traffic, we will program iptables to SNAT using Primary IP address on the Primary ENI.
sudo tcpdump -i any -nn icmp
sudo tcpdump -i ens5 -nn icmp
sudo tcpdump -i ens6 -nn icmp
sudo tcpdump -i eniYYYYYYYY -nn icmp
[워커 노드1]
# routing policy database management 확인
ip rule
# routing table management 확인
ip route show table local
# 디폴트 네트워크 정보를 ens5 을 통해서 빠져나간다
ip route show table main
default via 192.168.1.1 dev ens5
...
4. 파드에서 외부 통신
파드에서 외부 통신 흐름 : iptable 에 SNAT 을 통하여 노드의 eth0(ens5) IP로 변경되어서 외부와 통신됨
VPC CNI 의 External source network address translation (SNAT) 설정에 따라, 외부(인터넷) 통신 시 SNAT 하거나 혹은 SNAT 없이 통신을 할 수 있다 - 링크
☞ [실습] 파드에서 외부 통신 테스트 및 확인
- 파드 shell 실행 후 외부로 ping 테스트 & 워커 노드에서 tcpdump 및 iptables 정보 확인
# pod-1 Shell 에서 외부로 ping
kubectl exec -it $PODNAME1 -- ping -c 1 www.google.com
kubectl exec -it $PODNAME1 -- ping -i 0.1 www.google.com
kubectl exec -it $PODNAME1 -- ping -i 0.1 8.8.8.8
# 워커 노드 EC2 : TCPDUMP 확인
sudo tcpdump -i any -nn icmp
sudo tcpdump -i ens5 -nn icmp
# 퍼블릭IP 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s ipinfo.io/ip; echo; echo; done
# 작업용 EC2 : pod-1 Shell 에서 외부 접속 확인 - 공인IP는 어떤 주소인가?
## The right way to check the weather - 링크
for i in $PODNAME1 $PODNAME2 $PODNAME3; do echo ">> Pod : $i <<"; kubectl exec -it $i -- curl -s ipinfo.io/ip; echo; echo; done
kubectl exec -it $PODNAME1 -- curl -s wttr.in/seoul
kubectl exec -it $PODNAME1 -- curl -s wttr.in/seoul?format=3
kubectl exec -it $PODNAME1 -- curl -s wttr.in/Moon
kubectl exec -it $PODNAME1 -- curl -s wttr.in/:help
# 워커 노드 EC2
## 출력된 결과를 보고 어떻게 빠져나가는지 고민해보자!
ip rule
ip route show table main
sudo iptables -L -n -v -t nat
sudo iptables -t nat -S
# 파드가 외부와 통신시에는 아래 처럼 'AWS-SNAT-CHAIN-0' 룰(rule)에 의해서 SNAT 되어서 외부와 통신!
# 참고로 뒤 IP는 eth0(ENI 첫번째)의 IP 주소이다
# --random-fully 동작 - 링크1 링크2
sudo iptables -t nat -S | grep 'A AWS-SNAT-CHAIN'
-A AWS-SNAT-CHAIN-0 -d 192.168.0.0/16 -m comment --comment "AWS SNAT CHAIN" -j RETURN
-A AWS-SNAT-CHAIN-0 ! -o vlan+ -m comment --comment "AWS, SNAT" -m addrtype ! --dst-type LOCAL -j SNAT --to-source 192.168.1.222 --random-fully
## 아래 'mark 0x4000/0x4000' 매칭되지 않아서 RETURN 됨!
sudo iptables -t nat -S | grep 'KUBE-POSTROUTING'
-N KUBE-POSTROUTING
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully
...
# 카운트 확인 시 AWS-SNAT-CHAIN-0에 매칭되어, 목적지가 192.168.0.0/16 아니고 외부 빠져나갈때 SNAT 192.168.1.251(EC2 노드1 IP) 변경되어 나간다!
sudo iptables -t filter --zero; sudo iptables -t nat --zero; sudo iptables -t mangle --zero; sudo iptables -t raw --zero
watch -d 'sudo iptables -v --numeric --table nat --list AWS-SNAT-CHAIN-0; echo ; sudo iptables -v --numeric --table nat --list KUBE-POSTROUTING; echo ; sudo iptables -v --numeric --table nat --list POSTROUTING'
# conntrack 확인 : EC2 메타데이터 주소(169.254.169.254) 제외 출력
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo conntrack -L -n |grep -v '169.254.169'; echo; done
conntrack v1.4.5 (conntrack-tools):
icmp 1 28 src=172.30.66.58 dst=8.8.8.8 type=8 code=0 id=34392 src=8.8.8.8 dst=172.30.85.242 type=0 code=0 id=50705 mark=128 use=1
tcp 6 23 TIME_WAIT src=172.30.66.58 dst=34.117.59.81 sport=58144 dport=80 src=34.117.59.81 dst=172.30.85.242 sport=80 dport=44768 [ASSURED] mark=128 use=1
☞ [실습] 파드 ↔ 운영서버 EC2 간 통신 확인
- 운영서버 EC2 → 파드 IP 통신 : 통신이 가능한 이유는? 통신 경로를 알아보자
# 운영서버 EC2 SSH 접속
ssh <운영서버 EC2 공인 IP>
ssh ec2-user@3.36.89.204
-----------------------
POD1IP=<파드1 IP 지정>
POD1IP=192.168.1.222
ping -c 1 $POD1IP
exit
-----------------------
# 워커노드1 에서 tcpdump 확인 : NAT 동작 적용 여유 확인
sudo tcpdump -i any -nn icmp
- 파드1 → 운영서버 EC2 통신 : 통신이 가능한 이유는? 통신 경로를 알아보자 - Docs
# vpc cni env 정보 확인
kubectl get ds aws-node -n kube-system -o json | jq '.spec.template.spec.containers[0].env'
...
{
"name": "AWS_VPC_K8S_CNI_EXTERNALSNAT",
"value": "false"
},
...
# 운영서버 EC2 SSH 접속
kubectl exec -it $PODNAME1 -- ping 172.20.1.100
# 파드1 배치 워커노드에서 tcpdump 확인 : NAT 동작 적용 여유 확인
sudo tcpdump -i any -nn icmp
# 운영서버 EC2 에서 tcpdump 확인 : NAT 동작 적용 여유 확인
sudo tcpdump -i any -nn icmp
-----------------------------------------------------
# 파드1 배치 워커노드 : NAT 적용 정책 확인
sudo iptables -t filter --zero; sudo iptables -t nat --zero; sudo iptables -t mangle --zero; sudo iptables -t raw --zero
watch -d 'sudo iptables -v --numeric --table nat --list AWS-SNAT-CHAIN-0; echo ; sudo iptables -v --numeric --table nat --list KUBE-POSTROUTING; echo ; sudo iptables -v --numeric --table nat --list POSTROUTING'
# 파드 상태 모니터링
# kubectl set env 명령어는 내부적으로 kubectl patch를 실행하여 PodSpec을 변경 → 이로 인해 aws-node 데몬셋이 자동으로 롤링 업데이트
watch -d kubectl get pod -n kube-system
# 파드1 배치 워커노드 iptables rule 모니터링 : iptables rule 추가됨
watch -d 'sudo iptables -v --numeric --table nat --list AWS-SNAT-CHAIN-0; echo ; sudo iptables -v --numeric --table nat --list KUBE-POSTROUTING; echo ; sudo iptables -v --numeric --table nat --list POSTROUTING'
# 사내 내부에 연결 확장된 네트워크 대역과 SNAT 없이 통신 가능하게 설정
kubectl set env daemonset aws-node -n kube-system AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS=172.20.0.0/16
#
kubectl get ds aws-node -n kube-system -o json | jq '.spec.template.spec.containers[0].env'
...
{
"name": "AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS",
"value": "172.20.0.0/16"
}
# 운영서버 EC2 SSH 접속
kubectl exec -it $PODNAME1 -- ping 172.20.1.100
# 파드1 배치 워커노드 : NAT 적용 정책 확인
sudo iptables -t filter --zero; sudo iptables -t nat --zero; sudo iptables -t mangle --zero; sudo iptables -t raw --zero
watch -d 'sudo iptables -v --numeric --table nat --list AWS-SNAT-CHAIN-0; echo ; sudo iptables -v --numeric --table nat --list KUBE-POSTROUTING; echo ; sudo iptables -v --numeric --table nat --list POSTROUTING'
Chain AWS-SNAT-CHAIN-0 (1 references)
pkts bytes target prot opt in out source destination
1 84 RETURN all -- * * 0.0.0.0/0 172.20.0.0/16 /* AWS SNAT CHAIN EXCLUSION */
0 0 RETURN all -- * * 0.0.0.0/0 192.168.0.0/16 /* AWS SNAT CHAIN */
10 696 SNAT all -- * !vlan+ 0.0.0.0/0 0.0.0.0/0 /* AWS, SNAT */ ADDRTYPE match dst-type !LOCAL to:192.168.1.222 random-fully
...
- env 에 설정을 영구 유지하려면 어떻게 해야 될까요? - Link
- 다음 실습을 위해서 디플로이먼트 삭제: kubectl delete deploy netshoot-pod
- [Sam.0] Netfilter and iptables - Link , A Deep Dive into Iptables and Netfilter Architecture - Link
- https://docs.google.com/presentation/d/1tXS3N0196WmdaWYa0ZLVpIMt7uDQdBO6PGdq25z0gvs/edit#slide=id.p 정독 추천!