Karpenter란
Karpenter란 쿠버네티스 클러스터에서 노드를 자동으로 프로비저닝하고 관리하기 위한 오픈 소스 도구입니다. 기존의 Cluster Autoscaler와는 다르게, Karpenter는 보다 유연하고 효율적인 노드 관리를 제공합니다.
카펜터를 프로젝트에 적용한 이유는 시나리오 상 트래픽 폭증에 대비해 사전에 스케일 아웃을 해두고 시간이 지난 후 스케일 인을 하는 방식으로 운영할 계획이나, 사전에 인지하지 못하는 경우나 예상보다 트래픽이 더 많이 증가하는 경우를 대비하기 위해 스케일 인/아웃이 빠르고 비용 효율적인 운영이 가능한 오토스케일링 전략이 필요했기 때문이었습니다.
Pod Auto scaling의 경우, HPA를 이용해 metric server가 수집한 metric 기반으로 스케일 인, 아웃되도록 구성하였고, 노드 스케일링의 경우 cluster autoscaler와 Karpenter를 비교한 후 다음과 같은 이유로 Karpenter를 선택하게 되었습니다.
Cluster Autoscaler: K8s의 리소스인 Cluster Autoscaler가 직접 노드를 프로비저닝 하는 것이 아니라 AWS의 ASG을 거쳐 노드를 프로비저닝하므로 노드 생성 속도가 느리고, 이를 ASG의 Warm pool을 함께 사용하여 보완하더라도 노드의 타입이 하나로 제한되고 동기화 문제가 발생할 수 있다는 단점이 있습니다.
Karpenter: 노드 프로비저닝과 Pod의 노드 바인딩을 직접 처리하기 때문에, 노드 스케일링 속도가 빠르면서도 파드의 요구사항에 맞는 최적의 노드를 프로비저닝하여 비용을 절감할 수 있다는 장점이 있습니다. 또한 동기화 문제가 발생하지 않기에 불필요한 노드의 삭제가 빠르고 정확하고, 비효율적인 노드 사용(하나의 노드에 작은 Pod 하나만 떠있는 상황)을 방지할 수 있습니다.
위의 장점 때문에 추가적인 학습을 거치면서까지 노드의 오토스케일링에 카펜터를 활용하였고, Pod가 unscheduled 되었을 때 노드 생성 속도를 테스트해 본 결과 30초 이내에 노드가 준비 상태(Ready)가 되는 것을 확인할 수 있었습니다.
Karpenter 설치 및 적용
클러스터 구축과 기본 설정을 마친 후 Karpenter를 node autoscailing에 사용하기 위해 다음과 같은 설정이 필요합니다.
Karpenter 공식 문서
kubectl version을 통해 eks version을 확인할 수 있습니다.
1. 먼저 cluster name과 k8s version을 수정한 뒤, 다음 명령어들을 순서대로 실행하여 환경변수를 설정합니다.
#
# 변수 설정
#
KARPENTER_NAMESPACE=kube-system
CLUSTER_NAME=<your cluster name>
K8S_VERSION=<your eks cluster version>
AWS_PARTITION="aws" # if you are not using standard partitions, you may need to configure to aws-cn / aws-us-gov
AWS_REGION="$(aws configure list | grep region | tr -s " " | cut -d" " -f3)"
OIDC_ENDPOINT="$(aws eks describe-cluster --name "${CLUSTER_NAME}" \
--query "cluster.identity.oidc.issuer" --output text)"
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' \
--output text)
ARM_AMI_ID="$(aws ssm get-parameter --name /aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2-arm64/recommended/image_id --query Parameter.Value --output text)"
AMD_AMI_ID="$(aws ssm get-parameter --name /aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2/recommended/image_id --query Parameter.Value --output text)"
GPU_AMI_ID="$(aws ssm get-parameter --name /aws/service/eks/optimized-ami/${K8S_VERSION}/amazon-linux-2-gpu/recommended/image_id --query Parameter.Value --output text)"
2. Karpenter가 provisioning할 노드들의 IAM role을 생성하고 필요한 정책들을 attach 합니다.
#
# Create IAM roles for nodes provisioned with Karpenter
#
echo '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}' > node-trust-policy.json
# attach required policies to IAM role
aws iam create-role --role-name "KarpenterNodeRole-${CLUSTER_NAME}" \
--assume-role-policy-document file://node-trust-policy.json
aws iam attach-role-policy --role-name "KarpenterNodeRole-${CLUSTER_NAME}" \
--policy-arn "arn:${AWS_PARTITION}:iam::aws:policy/AmazonEKSWorkerNodePolicy"
aws iam attach-role-policy --role-name "KarpenterNodeRole-${CLUSTER_NAME}" \
--policy-arn "arn:${AWS_PARTITION}:iam::aws:policy/AmazonEKS_CNI_Policy"
aws iam attach-role-policy --role-name "KarpenterNodeRole-${CLUSTER_NAME}" \
--policy-arn "arn:${AWS_PARTITION}:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
aws iam attach-role-policy --role-name "KarpenterNodeRole-${CLUSTER_NAME}" \
--policy-arn "arn:${AWS_PARTITION}:iam::aws:policy/AmazonSSMManagedInstanceCore"
3. Karpenter controller의 IAM role을 생성하고 필요한 정책들을 attach 합니다.
#
# create IAM role for Karpenter controller
#
cat << EOF > controller-trust-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:oidc-provider/${OIDC_ENDPOINT#*//}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"${OIDC_ENDPOINT#*//}:aud": "sts.amazonaws.com",
"${OIDC_ENDPOINT#*//}:sub": "system:serviceaccount:${KARPENTER_NAMESPACE}:karpenter"
}
}
}
]
}
EOF
aws iam create-role --role-name "KarpenterControllerRole-${CLUSTER_NAME}" \
--assume-role-policy-document file://controller-trust-policy.json
cat << EOF > controller-policy.json
{
"Statement": [
{
"Action": [
"ssm:GetParameter",
"ec2:DescribeImages",
"ec2:RunInstances",
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:DescribeLaunchTemplates",
"ec2:DescribeInstances",
"ec2:DescribeInstanceTypes",
"ec2:DescribeInstanceTypeOfferings",
"ec2:DescribeAvailabilityZones",
"ec2:DeleteLaunchTemplate",
"ec2:CreateTags",
"ec2:CreateLaunchTemplate",
"ec2:CreateFleet",
"ec2:DescribeSpotPriceHistory",
"pricing:GetProducts"
],
"Effect": "Allow",
"Resource": "*",
"Sid": "Karpenter"
},
{
"Action": "ec2:TerminateInstances",
"Condition": {
"StringLike": {
"ec2:ResourceTag/karpenter.sh/nodepool": "*"
}
},
"Effect": "Allow",
"Resource": "*",
"Sid": "ConditionalEC2Termination"
},
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}",
"Sid": "PassNodeIAMRole"
},
{
"Effect": "Allow",
"Action": "eks:DescribeCluster",
"Resource": "arn:${AWS_PARTITION}:eks:${AWS_REGION}:${AWS_ACCOUNT_ID}:cluster/${CLUSTER_NAME}",
"Sid": "EKSClusterEndpointLookup"
},
{
"Sid": "AllowScopedInstanceProfileCreationActions",
"Effect": "Allow",
"Resource": "*",
"Action": [
"iam:CreateInstanceProfile"
],
"Condition": {
"StringEquals": {
"aws:RequestTag/kubernetes.io/cluster/${CLUSTER_NAME}": "owned",
"aws:RequestTag/topology.kubernetes.io/region": "${AWS_REGION}"
},
"StringLike": {
"aws:RequestTag/karpenter.k8s.aws/ec2nodeclass": "*"
}
}
},
{
"Sid": "AllowScopedInstanceProfileTagActions",
"Effect": "Allow",
"Resource": "*",
"Action": [
"iam:TagInstanceProfile"
],
"Condition": {
"StringEquals": {
"aws:ResourceTag/kubernetes.io/cluster/${CLUSTER_NAME}": "owned",
"aws:ResourceTag/topology.kubernetes.io/region": "${AWS_REGION}",
"aws:RequestTag/kubernetes.io/cluster/${CLUSTER_NAME}": "owned",
"aws:RequestTag/topology.kubernetes.io/region": "${AWS_REGION}"
},
"StringLike": {
"aws:ResourceTag/karpenter.k8s.aws/ec2nodeclass": "*",
"aws:RequestTag/karpenter.k8s.aws/ec2nodeclass": "*"
}
}
},
{
"Sid": "AllowScopedInstanceProfileActions",
"Effect": "Allow",
"Resource": "*",
"Action": [
"iam:AddRoleToInstanceProfile",
"iam:RemoveRoleFromInstanceProfile",
"iam:DeleteInstanceProfile"
],
"Condition": {
"StringEquals": {
"aws:ResourceTag/kubernetes.io/cluster/${CLUSTER_NAME}": "owned",
"aws:ResourceTag/topology.kubernetes.io/region": "${AWS_REGION}"
},
"StringLike": {
"aws:ResourceTag/karpenter.k8s.aws/ec2nodeclass": "*"
}
}
},
{
"Sid": "AllowInstanceProfileReadActions",
"Effect": "Allow",
"Resource": "*",
"Action": "iam:GetInstanceProfile"
}
],
"Version": "2012-10-17"
}
EOF
aws iam put-role-policy --role-name "KarpenterControllerRole-${CLUSTER_NAME}" \
--policy-name "KarpenterControllerPolicy-${CLUSTER_NAME}" \
--policy-document file://controller-policy.json
4. Karpenter가 생성할 노드들이 위치할 subnet과, Karpenter가 생성할 노드들이 속할 보안그룹에 태그를 추가합니다.
#
# Add tags to subnet and security groups
# 클러스터의 첫번째 node group에만 적용되는 것에 유의해야 함.
#
for NODEGROUP in $(aws eks list-nodegroups --cluster-name "${CLUSTER_NAME}" --query 'nodegroups' --output text); do
aws ec2 create-tags \
--tags "Key=karpenter.sh/discovery,Value=${CLUSTER_NAME}" \
--resources $(aws eks describe-nodegroup --cluster-name "${CLUSTER_NAME}" \
--nodegroup-name "${NODEGROUP}" --query 'nodegroup.subnets' --output text )
done
NODEGROUP=$(aws eks list-nodegroups --cluster-name "${CLUSTER_NAME}" \
--query 'nodegroups[0]' --output text)
LAUNCH_TEMPLATE=$(aws eks describe-nodegroup --cluster-name "${CLUSTER_NAME}" \
--nodegroup-name "${NODEGROUP}" --query 'nodegroup.launchTemplate.{id:id,version:version}' \
--output text | tr -s "\t" ",")
# 1. If your EKS setup is configured to use only Cluster security group, then please execute -
SECURITY_GROUPS=$(aws eks describe-cluster \
--name "${CLUSTER_NAME}" --query "cluster.resourcesVpcConfig.clusterSecurityGroupId" --output text)
# 2. If your setup uses the security groups in the Launch template of a managed node group, then :
SECURITY_GROUPS="$(aws ec2 describe-launch-template-versions \
--launch-template-id "${LAUNCH_TEMPLATE%,*}" --versions "${LAUNCH_TEMPLATE#*,}" \
--query 'LaunchTemplateVersions[0].LaunchTemplateData.[NetworkInterfaces[0].Groups||SecurityGroupIds]' \
--output text)"
aws ec2 create-tags \
--tags "Key=karpenter.sh/discovery,Value=${CLUSTER_NAME}" \
--resources "${SECURITY_GROUPS}"
위 코드에서 중요한 점은 cluster의 기본 security group만 노드그룹에 사용하는 경우(eksctl이나 수동으로 cluster 생성 시)와 기본 보안 그룹 이외에 Managed node group의 Launch template에 적용되는 보안 그룹이 노드 그룹에 적용되는 경우(terraform에 추가로 보안 그룹 설정 후 띄운 경우), 각각 다른 명령어를 입력해야 한다는 점입니다.
5. aws-auth에 아래와 같은 설정을 추가합니다.
#
# Update aws-auth ConfigMap
#
kubectl edit configmap aws-auth -n kube-system
# 위 명령 실행 후 vi editor에서 아래와 같은 부분을 찾고, 아래의 코드에서 ${AWS_PARTITION}, ${AWS_ACCOUNT_ID}, ${CLUSTER_NAME}을 수정하여 추가해주면 됨.
# AWS_PARTITION은 일반적으로 aws이고, AWS_ACCOUNT_ID와 CLUSTER_NAME은 AWS 웹 콘솔에서 쉽게 찾을 수 있습니다.
- groups:
- system:bootstrappers
- system:nodes
rolearn: arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterNodeRole-${CLUSTER_NAME}
username: system:node:{{EC2PrivateDNSName}}
aws-auth은 AWS IAM Authenticator for Kubernetes가 AWS IAM credentials를 Kubernetes Cluster 인증에 사용할 수 있도록 IAM RBAC와 Kubernetes RBAC의 매핑 정보를 설정하는 ConfigMap입니다.
따라서 aws-auth ConfigMap에 해당 설정을 추가하는 것의 의미는 2번 과정에서 생성했던 AWS IAM Role을 arn을 이용해 Kubernetes Group에 연결하고, 해당 IAM Role을 가진 EC2 인스턴스가 cluster에서 사용할 Kubernetes username을 지정하는 것으로 볼 수 있습니다.
위 설정을 추가함으로써 Karpenter가 생성하는 EC2 인스턴스가 Kubernetes Cluster에서 노드에 대한 기본적인 권한을 부여하는 데 사용되는 system:bootstrappers와 system:nodes 그룹에 속하게 되어, Kubernetes API를 통해 클러스터 리소스에 접근할 수 있습니다. 또한 {{EC2PrivateDNSName}}는 Karpenter가 생성하는 각 노드의 private DNS name으로 자동으로 대체되어 각 노드가 Kubernetes Cluster에서 고유하게 식별될 수 있습니다.
6. Karpenter를 배포합니다.
#
# 카펜터 배포에 사용할 karpenter.yaml 파일 생성
#
# set the Karpenter release you want to deploy.
export KARPENTER_VERSION="1.0.0"
# generate a full Karpenter deployment yaml from the Helm chart.
helm template karpenter oci://public.ecr.aws/karpenter/karpenter --version "${KARPENTER_VERSION}" --namespace "${KARPENTER_NAMESPACE}" \
--set "settings.clusterName=${CLUSTER_NAME}" \
--set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=arn:${AWS_PARTITION}:iam::${AWS_ACCOUNT_ID}:role/KarpenterControllerRole-${CLUSTER_NAME}" \
--set controller.resources.requests.cpu=1 \
--set controller.resources.requests.memory=1Gi \
--set controller.resources.limits.cpu=1 \
--set controller.resources.limits.memory=1Gi > karpenter.yaml
위 명령어를 실행하면 working directory에 karpenter.yaml 파일이 생성됩니다.
vi editor로 해당 파일을 수정해야 합니다.
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: karpenter.sh/nodepool
operator: DoesNotExist
- key: eks.amazonaws.com/nodegroup
operator: In
values:
- ${NODEGROUP}
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: "kubernetes.io/hostname"
위 코드 중, 실제로 karpenter.yaml 파일에 추가해야 할 부분은 아래의 코드입니다. 이외의 코드들은 이미 작성되어 있으니 yaml 형식에 맞게 잘 수정해야 합니다.
(이 때, matchExpressions에 karpenter.sh/nodepool이라는 label이 존재하지 않는 노드에만 Karpenter가 뜨도록 기본적으로 설정되어 있다는 것을 확인할 수 있습니다. Karpenter가 생성하는 노드에는 karpenter.sh/nodepool: default와 같은 label이 자동으로 붙는데, 이러한 설정 덕분에 Karpenter Pod는 자기 자신이 관리하는 노드에 뜨지 않을 수 있습니다.)
- key: eks.amazonaws.com/nodegroup
operator: In
values:
- ${NODEGROUP}
${NODEGROUP}에 cluster의 노드그룹 이름을 적은 뒤, 위의 코드를 아래 예시처럼 spec.template.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.matchExpressions에 추가합니다.
이후 아래의 명령을 수행해 Karpenter를 배포합니다.
kubectl create namespace "${KARPENTER_NAMESPACE}" || true
kubectl create -f \
"https://raw.githubusercontent.com/aws/karpenter-provider-aws/v${KARPENTER_VERSION}/pkg/apis/crds/karpenter.sh_nodepools.yaml"
kubectl create -f \
"https://raw.githubusercontent.com/aws/karpenter-provider-aws/v${KARPENTER_VERSION}/pkg/apis/crds/karpenter.k8s.aws_ec2nodeclasses.yaml"
kubectl create -f \
"https://raw.githubusercontent.com/aws/karpenter-provider-aws/v${KARPENTER_VERSION}/pkg/apis/crds/karpenter.sh_nodeclaims.yaml"
kubectl apply -f karpenter.yaml
7. 정상적으로 Karpenter가 배포되었다면, default NodePool과 EC2NodeClass를 생성하여 Pod가 unscheduled 되었을 때 어떤 노드 타입을 생성할 지 정해줍니다.
cat <<EOF | envsubst | kubectl apply -f -
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: kubernetes.io/os
operator: In
values: ["linux"]
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["2"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
expireAfter: 720h # 30 * 24h = 720h
limits:
cpu: 1000
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
amiFamily: AL2 # Amazon Linux 2
role: "KarpenterNodeRole-${CLUSTER_NAME}" # replace with your cluster name
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: "${CLUSTER_NAME}" # replace with your cluster name
amiSelectorTerms:
- id: "${ARM_AMI_ID}"
- id: "${AMD_AMI_ID}"
# - id: "${GPU_AMI_ID}" # <- GPU Optimized AMD AMI
# - name: "amazon-eks-node-${K8S_VERSION}-*" # <- automatically upgrade when a new AL2 EKS Optimized AMI is released. This is unsafe for production workloads. Validate AMIs in lower environments before deploying them to production.
EOF
위의 예시는 노드 증설에 spot instance를 사용하므로 아래와 같은 설정을 추가하거나 capacity-type을 spot에서 on-demand로 바꿔줘야 정상적으로 노드가 생성됩니다.
aws iam create-service-linked-role --aws-service-name spot.amazonaws.com || true
이후, pod가 unscheduled 상태가 되면 node가 자동으로 스케일 아웃되는 것을 확인할 수 있습니다.
+ 추가로 coredns나 metric-server와 같이 중요한 기능을 하는 워크로드들의 경우, Karpenter가 관리하는 노드에 뜨게 되면 Karpenter가 노드 사용을 최적화하기 위해 프로비저닝된 노드를 줄이고 늘리는 과정에서 워크로드가 종료될 수 있기 때문에 추가 설정을 통해 Karpenter가 관리하지 않는 노드에 뜨도록 해주는 것이 좋습니다.
(Karpenter의 경우 이미 위의 설정들을 통해 자기 자신은 자기가 관리하는 노드에 뜨지 않습니다.)
kubectl edit deploy {coredns or metric-server name}을 통해 아래 코드를 적절히 추가해주면 됩니다.
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: eks.amazonaws.com/nodegroup
operator: In
values:
- ${NODEGROUP}
또한, nodePool에 다음과 같은 설정을 추가하여 Karpenter가 provisioning할 노드의 core 수를 제한할 수도 있습니다.
(+ 추가로, Karpenter를 적용하기 전에 생성되었던 노드에는 Karpenter가 관리하기 위해 필요한 label이 설정되지 않았기 때문에 Karpenter가 관리하지 않습니다. 따라서 다음과 같은 명령어를 통해 노드의 수를 조절해야 합니다.
# 카펜터가 관리하지 않는 Node의 개수를 2개로 설정
for NODEGROUP in $(aws eks list-nodegroups --cluster-name "${CLUSTER_NAME}" \
--query 'nodegroups' --output text); do aws eks update-nodegroup-config --cluster-name "${CLUSTER_NAME}" \
--nodegroup-name "${NODEGROUP}" --scaling-config "minSize=2,maxSize=2,desiredSize=2"; \
done
또한, Karpenter나 coredns와 같은 워크로드들은 Karpenter가 관리하지 않는 해당 노드에 뜨기 때문에, 어느정도의 스펙을 갖춘 노드를 사용하도록 기본 노드 그룹을 설정해야합니다.)
(자세한 내용은 공식문서 참고)
Overprovisioning
카펜터는 Pod가 스케줄링 되지 않아야 새로운 노드를 띄우기 때문에 ASG을 사용할 때처럼 미리 노드의 개수를 정해 증설할 수 없다는 문제가 있었습니다.
위의 예시처럼 Pod Anti Affinity를 가지고, 리소스를 거의 사용하지 않는 overprovision용 Pod를 생성하는 방법으로 Pod 수만큼 노드를 확장하여 파드가 스케줄링될 여유 공간을 미리 확보할 수 있습니다.
트러블 슈팅
eksctl이나 수동으로 cluster 생성한 경우 다음과 같은 오류가 발생할 수 있습니다.
1. "ec2 api connectivity check failed,error:NoCredentialProviders: no valid providers in chain"
이 때는 eks-pod-identity-agent를 addon으로 추가해주니 해결되었습니다.
2. "WebIdentityErr: failed to retrieve credentials caused by: InvalidIdentityToken: No OpenIDConnect provider found"
-> AWS 콘솔을 사용하거나 수동으로 EKS 클러스터를 생성한 경우, IAM provider 목록에서 EKS의 OIDC 공급자를 활성화해야 합니다.
아래 명령을 수행하면 이 문제를 해결할 수 있습니다.
eksctl utils associate-iam-oidc-provider --cluster "${CLUSTER_NAME}" --approve
3. 추가로, "4. Karpenter가 생성할 노드들이 위치할 subnet과, Karpenter가 생성할 노드들이 속할 보안그룹에 태그를 추가합니다." 부분에서 2번째 명령의 결과가 다음과 같다면 1번으로 수정하여 문제를 해결할 수 있었습니다.
4. 위의 1, 2, 3번을 모두 반영했다면, 다시 처음부터 설정을 진행하면 문제가 해결됩니다.
'Cloud engineering' 카테고리의 다른 글
RHCSA 9 후기 (5) | 2024.09.07 |
---|---|
부하테스트를 통한 상품 조회 서비스 1.5배 성능 개선(ElastiCache, HPA) (0) | 2024.09.01 |
Argo Rollouts 설치 및 활용 (0) | 2024.09.01 |
ArgoCD 설치 및 설정 (1) | 2024.09.01 |
Terraform 이용해 EKS Cluster 구축하기 (0) | 2024.09.01 |