안녕하세요?
EKS를 다루는 주제로 CloudNeta팀 가시다님의 스터디에 합류하게 되었습니다.
이미 CloudNeta팀에서 한 번 했었던 주제이지만 시간이 지났으니 바뀐 내용들을 습득할 필요가 있습니다.
첫 주차 과제로서 ECR Pull Through Cache에 대해서 다뤄보겠습니다.
TL; DR
EKS 환경 구축
익히 알려져있다시피 EKS 는 AWS의 관리형 제품이기 때문에 Control Plane을 관리할 필요가 없다는 장점이 있습니다.
프로덕션 환경에서는 Terraform 등의 IaC 툴을 사용하여 많이 운영하고 있을 것입니다만
스터디 복습용이기 때문에 AWS CloudFormation 템플릿을 사용해보겠습니다.
주어진 템플릿을 다운받습니다.
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/myeks-1week.yaml
CloudFormation의 템플릿은 JSON 또는 YAML 형식의 텍스트 파일입니다.
CloudFormation을 사용할 경우 스택이라는 하나의 단위로 관련 리소스를 관리합니다.
스택을 생성, 업데이트, 삭제하여 리소스 모음을 생성, 업데이트 및 삭제합니다.
위와 같은 구조로 AWS 리소스를 생성합니다.
그러면 다운받은 템플릿에 어떤 내용이 들어있는지 볼까요?
제일 직관적인 방법은 CloudFormation에 들어가서 다운받은 템플릿 파일을 InfraStructure Composer로 로드해서 보는 방법입니다.
예컨대 이런식으로 파일을 확인해볼 수 있습니다.
그러면 아래와 같이 정돈된 모습으로 시각화해서 볼 수 있습니다.
EKS 클러스터에 필요한
VPC, Subnet, Internet Gateway, EC2, Security Group, Route Table 들이 생성될 예정이란 사실을 알 수 있습니다.
EC2는 그냥 올라갈까요? 아닙니다.
EC2 항목을 선택해서 세부 내용을 보면
위와 같이 EC2가 올라가면서 아래와 같은 커맨드가 실행되어 실습환경을 준비한다는 사실을 알 수 있습니다.
이런 내용을 UserData 항목에 추가할 수 있습니다.
테스트에 필요한 유틸들이 잘 들어가 있군요.
UserData: !Base64
Fn::Sub: |
#!/bin/bash
hostnamectl --static set-hostname "${ClusterBaseName}-host"
# Config Root account
echo 'root:qwe123' | chpasswd
sed -i "s/^#PermitRootLogin yes/PermitRootLogin yes/g" /etc/ssh/sshd_config
sed -i "s/^PasswordAuthentication no/PasswordAuthentication yes/g" /etc/ssh/sshd_config
rm -rf /root/.ssh/authorized_keys
systemctl restart sshd
# Config convenience
echo 'alias vi=vim' >> /etc/profile
echo "sudo su -" >> /home/ec2-user/.bashrc
sed -i "s/UTC/Asia\/Seoul/g" /etc/sysconfig/clock
ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
# Install Packages
yum -y install tree jq git htop
# Install YAML Highlighter
wget https://github.com/andreazorzetto/yh/releases/download/v0.4.0/yh-linux-amd64.zip
unzip yh-linux-amd64.zip
mv yh /usr/local/bin/
# Install kubectl & helm
cd /root
curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.31.2/2024-11-15/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
export AWS_DEFAULT_REGION=${AWS::Region}
echo "export AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION" >> /etc/profile
# Install krew
curl -L https://github.com/kubernetes-sigs/krew/releases/download/v0.4.4/krew-linux_amd64.tar.gz -o /root/krew-linux_amd64.tar.gz
tar zxvf krew-linux_amd64.tar.gz
./krew-linux_amd64 install krew
export PATH="$PATH:/root/.krew/bin"
echo 'export PATH="$PATH:/root/.krew/bin"' >> /etc/profile
# Install kube-ps1
echo 'source <(kubectl completion bash)' >> /etc/profile
echo 'alias k=kubectl' >> /etc/profile
echo 'complete -F __start_kubectl k' >> /etc/profile
git clone https://github.com/jonmosco/kube-ps1.git /root/kube-ps1
cat <<"EOT" >> /root/.bash_profile
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
# CLUSTER_NAME
export CLUSTER_NAME=${ClusterBaseName}
echo "export CLUSTER_NAME=$CLUSTER_NAME" >> /etc/profile
# Create SSH Keypair
ssh-keygen -t rsa -N "" -f /root/.ssh/id_rsa
# Install krew plugin
kubectl krew install ctx ns get-all neat # ktop df-pv mtail tree
# Install Docker
amazon-linux-extras install docker -y
systemctl start docker && systemctl enable docker
# Install Kubecolor
wget https://github.com/kubecolor/kubecolor/releases/download/v0.5.0/kubecolor_0.5.0_linux_amd64.tar.gz
tar -zxvf kubecolor_0.5.0_linux_amd64.tar.gz
mv kubecolor /usr/local/bin/
그 다음 화면으로 넘어가겠습니다.
생성에 필요한 파라미터들을 입력합니다.
Cluster 이름 , 사용될 key , 사용될 EC2는 아무데서나 접근하면 안되니까 여긴 내 환경의 IP를 입력해주어야 합니다.
그리고 CloudFormation 설계도대로 리소스들이 생성되기를 기다립니다.
# EC2 접근
ssh -i docker-test.pem ec2-user@$(aws cloudformation describe-stacks --stack-name test-eks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
# aws cli에 대한 권한 구성
aws configure
# CloudFormation으로 구성된 VPC 정보 확인
aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" | jq
aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" | jq Vpcs[]
aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" | jq Vpcs[].VpcId
aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" | jq -r .Vpcs[].VpcId
export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" | jq -r .Vpcs[].VpcId)
echo "export VPCID=$VPCID" >> /etc/profile
echo $VPCID
# EKS 배포할 VPC에 속한 Subnet 정보 확인
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPCID" --output json | jq
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPCID" --output yaml
## 퍼블릭 서브넷 ID 확인
aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet1" | jq
aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text
export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
echo "export PubSubnet1=$PubSubnet1" >> /etc/profile
echo "export PubSubnet2=$PubSubnet2" >> /etc/profile
echo $PubSubnet1
echo $PubSubnet2
배포를 위한 정보들을 확인했습니다.
이제 EKS 배포를 시작합니다.
필요한 변수들은 아까 export 해두었습니다.
echo $AWS_DEFAULT_REGION
echo $CLUSTER_NAME
echo $VPCID
echo $PubSubnet1,$PubSubnet2
eksctl create cluster --name $CLUSTER_NAME --region=$AWS_DEFAULT_REGION --nodegroup-name=$CLUSTER_NAME-nodegroup --node-type=t3.medium \ --node-volume-size=30 --vpc-public-subnets "$PubSubnet1,$PubSubnet2" --version 1.31 --ssh-access --external-dns-access --verbose 4
ECR Pull Through Cache
AWS ECR이 뭔가요?
ECR에 대한 설명
Amazon ECR 프라이빗 레지스트리는 가용성과 확장성이 뛰어난 아키텍처에서 컨테이너 이미지를 호스팅합니다. 프라이빗 레지스트리를 사용하여 Docker 및 Open Container Initiative(OCI) 이미지와 아티팩트로 구성된 프라이빗 이미지 리포지토리를 관리할 수 있습니다.
ECR(Elastic Container Registry)은 컨테이너 이미지를 보관할 수 있는 레지스트리입니다.
회사의 코드가 들어간 컨테이너 이미지를 아무나 접근할 수 있는 컨테이너 레지스트리에 올릴 수도 없습니다.
그러니 EKS를 사용하는 상황이라면 아무나 접근할 수 없는 Private ECR을 사용하는 것이 좋은 선택지이고 EKS에서 private ecr에 올라간 이미지를 Pulling 하여 사용하게 될 것 같습니다.
아래와 같은 형태가 될 수 있습니다.
Git Repo에 코드 업데이트를 하면 Kaniko 등의 방법으로 컨테이너가 빌드되고 Private ECR에 Push가 될겁니다.
그리고 GitOps 형태라면 이 업데이트가 트리거가 될 수 있을 것이고 ArgoCD 등에서 반영이 되어 ECR에 새로 올라간 업데이트된 애플리케이션이 새로 배포가 될 겁니다.
ECR Pull Through Cache가 뭔가요?
위와 같은 경우라면 직접 빌드를 해서 컨테이너를 Private ECR에 매번 푸시할 수 있습니다.
하지만 남이 만든 컨테이너를 사용해야 하는 경우는 어떨까요?
external-dns 같은 툴의 컨테이너 이미지를 사용하는 상황이라고 해보겠습니다.
누구나 받을 수 있도록 하기 위해 이런 컨테이너는 퍼블릭 레지스트리에 올라가 있을 것입니다.
그런데 갑자기 대규모의 업데이트가 필요해서 (그런 일은 없겠지만) 동시에 그 퍼블릭 레지스트리에서 이미지들이 풀링이 된다면 아마 풀링 제한이 걸릴겁니다.
이런 저런 이유로 우리가 사용하는 컨테이너들을 복제해와서 Private ECR에 보관하고 싶은 생각이 들 수 있습니다.
그런데 이 일은 어떻게 해야 할까요?
직접 pull 해와서 일일이 push하기는 너무 귀찮습니다.
새 버젼을 업데이트하기는 더 귀찮습니다.
이럴 때 사용할 수 있는 기능이 ECR Pull Through Cache 입니다.
공식문서는 Amazon ECR 프라이빗 레지스트리와 업스트림 레지스트리 동기화 - Amazon ECR를 참고해주시기 바랍니다.
ECR Pull Through Cache의 구조
어떤 동작을 하는지는 제가 간단하게 그려둔 도식을 확인해주시기 바랍니다.
ECR Pull Through Cache 구현
편리성을 이유로 Terraform을 기준으로 설명하겠습니다.
인증
다른 업스트림 레지스트리는 괜찮은데 DockerHub 같은건 별도의 인증을 요구합니다.
그렇기 때문에 AWS Secret Manager를 통해 DockerHub에 대한 인증을 미리 넣어줄 필요가 있습니다.
업스트림 레지스트리의 보안 인증 정보 저장은 해당 문서를 참고해주시기 바랍니다.
업스트림 리포지토리 보안 인증 정보를 AWS Secrets Manager 보안 암호로 저장 - Amazon ECR
업스트림 리포지토리 보안 인증 정보를 AWS Secrets Manager 보안 암호로 저장 - Amazon ECR
보안 암호를 암호화하려면 기본 aws/secretsmanager 암호화 키를 사용해야 합니다. Amazon ECR은 이를 위한 고객 관리형 키(CMK) 사용을 지원하지 않습니다.
docs.aws.amazon.com
이런 형태로 ecr-pullthroughcache 라는 접두사를 꼭 넣어주어야 합니다.
Terraform 코드 생성
그리고 cache-image.yaml 파일을 만듭니다.
여기에는 필요로 하는 이미지들이 위치하는 업스트림 레지스트리의 정보가 들어갑니다.
아래와 같은 형태입니다.
external-dns를 예로 들어보겠습니다.
# cache-image.yaml
- docker.io/bitnami/external-dns:0.14.2-debian-12-r4
...
그리고 ECR Pull Through Cache Rule은 아래와 같은 형태로 들어갈 수 있습니다.
# ecr_pull_through_cache_rule.tf
resource "aws_ecr_pull_through_cache_rule" "this" {
for_each = local.ecr_ptcr
ecr_repository_prefix = each.key
upstream_registry_url = each.value.registry_url
credential_arn = each.value.secrets_manager_arn
}
실제로 pulling을 하는 파일은 이렇게 만들 수 있습니다.
# main.tf
locals {
account_id = {your_account_id}
region = {your_region}
ecr_registry = "${local.account_id}.dkr.ecr.${local.region}.amazonaws.com"
docker_secret_arn = "arn:aws:secretsmanager:${local.region}:${local.account_id}:secret:ecr-pullthroughcache/{your-name}"
cache_images_info = yamldecode(file("./cache_images.yaml"))
ecr_ptcr = {
docker-hub = {
secrets_manager_arn = local.docker_secret_arn
registry_url = "registry-1.docker.io"
pull_images = [
for i in local.cache_images_info : trimprefix(i, "registry-1.docker.io/")
if startswith(i, "registry-1.docker.io")
]
}
quay = {
secrets_manager_arn = null
registry_url = "quay.io"
pull_images = [
for i in local.cache_images_info : trimprefix(i, "quay.io/")
if startswith(i, "quay.io")
]
}
}
- ...
ecr_repos = flatten([
for k, v in local.ecr_ptcr : distinct([
for image in v.pull_images : "${k}/${split(":", image)[0]}"
])
])
docker_images = [
for k, v in local.ecr_ptcr : {
for image in v.pull_images : "${k}_${replace(image, ":", "_")}" => {
ptcr = k
image = image
}
}
]
merged_docker_images = zipmap(
flatten([for item in local.docker_images : keys(item)]),
flatten([for item in local.docker_images : values(item)])
)
}
그리고 배포합니다.
실행 결과
그러면 이제 dockerhub에 있는 container image를 갖고와보도록 하겠습니다.
위 image 입니다.
저 image를 갖고오는 Pod의 imagePullPolicy를 Always로 하고 재시작하면 이미지를 갖고오게 될 것입니다.
그러면 아래와 같이 ECR에 해당 레지스트리가 생성됩니다.
그리고 external-dns는 private ecr에서 갖고와서 배포가 되게 됩니다.
'Kubernetes' 카테고리의 다른 글
AWS VPC CNI 공부하기 (1) | 2024.11.02 |
---|---|
Cilium CNI 공부하기 (3) | 2024.10.25 |
Istio sidecar mode와 Kubeflow (1) | 2024.10.20 |
Operator로 배포하는 Gateway API 형태의 Kong API Gateway (4) | 2024.10.09 |
Iptables와 Kubernetes ClusterIP의 Iptables (2) | 2024.10.08 |