01/引言
对于实施多云或混合云策略的企业来说,kOps 是一个理想的 Kubernetes 集群管理工具。它通过统一的配置文件(YAML 或 JSON)实现跨多个云环境(如 AWS、GCP、Azure)或本地数据中心的集群管理。
kOps 提供了丰富的自定义选项,包括控制面节点和工作节点的操作系统、网络插件(如 Calico、Cilium)、存储解决方案等,满足企业在复杂场景下的灵活部署需求。
对于希望进一步优化 Kubernetes 资源效率的企业,开源的 Kubernetes 集群自动扩展工具 Karpenter 则是一个强大的选择。
Karpenter 能根据 Pod 的资源需求动态配置节点,支持多种实例类型,并可以智能调度 AWS Spot 实例,大幅降低运行成本。其无节点池的设计,使得资源调度更加灵活,特别适合需要高弹性和高性价比的场景。
然而,kOps 与 Karpenter 的官方集成已停止更新,仅支持较旧的版本。这意味着在最新版本的 kOps 上,用户需要通过手动配置的方式实现与 Karpenter 的集成。
为了解决这一问题,本文将介绍如何在 kOps 部署的 AWS Kubernetes 集群上部署 Karpenter,通过具体步骤和技术细节,帮助用户在现有架构中实现动态扩展能力,同时最大化利用 Karpenter 的优势,提升集群的弹性与资源利用效率。
为什么选择 kOps
kOps 是一个开源项目,可以创建、销毁、升级和维护一个高可用的生产级 Kubernetes 集群。该项目的开发者将其描述为"为集群打造的 Kubectl"。
目前,kOps 主要用于部署 AWS 和 GCE Kubernetes 集群,对 Azure 的支持处于 alpha 阶段。以下是这款工具的主要特性:
- 自动配置高可用 Kubernetes 集群
- 支持集群滚动更新
- 在命令行中自动补齐命令
- 生成 Terraform 配置
- 以状态同步模型为基础,实现 dry-runs 和自动幂等性(idempotency)
- 创建实例组以支持异构集群
相较其他产品 ,kOps更灵活。通过采用 kOps,企业可以获得 K8s 集群配置的较大控制权,并且能够自定义 K8s 环境以满足特定需求。还能根据自己的偏好配置云环境,并可以完全访问 master 节点以进行微调和故障排除。
此外,对于小型或临时集群,kOps 往往比其他选择更具成本效益。 作为一种开源和免费的解决方案,企业只需支付底层基础设施的费用。
为什么选择 Karpenter
Karpenter 是当前业界最流行的开源 Kubernetes 集群自动扩展工具之一,最初由 AWS 开源,目前已捐献给 CNCF。当前,Karpenter 支持 AWS、Azure及阿里云,针对 GKE 的支持正在紧锣密鼓地开发中。
与传统的自动扩缩不同,Karpenter 能够动态地在实时环境中为集群工作负载提供所需的计算资源。它通过观察未调度 pod 的资源请求总量,智能决策并启动精准匹配需求的新节点。
Karpenter 项目地址: github.com/kubernetes-...
02/前期准备
- 具有 IAM 权限的 AWS 账户, 以创建 EC2 Instance.
- 安装并配置 AWS CLI.
- 安装 Kubernetes CLI (kubectl)
- 安装 Helm(Kubernetes 的包管理工具)
- 安装 kOps
03/通过 kOps 创建集群
配置集群
在创建集群前,您需要配置集群所在的 Region,以及集群的名称。为了简化部署流程,我们会创建基于 Gossip DNS 的集群。 kops.sigs.k8s.io/gossip/
如果您希望使用自己的域名创建集群,您可以参阅下文: kops.sigs.k8s.io/getting_sta...
ini
export DEPLOY_REGION="us-west-1"
export CLUSTER_NAME="demo1"
export DEPLOY_ZONE="us-west-1a"
export NAME=${CLUSTER_NAME}.k8s.loca
创建 kOps IAM User
为了在 AWS 中创建集群,我们将使用 AWS CLI 为 kOps 创建一个专用的 IAM 用户 kops
。
sql
aws iam create-group --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonEC2FullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonRoute53FullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/IAMFullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonVPCFullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonSQSFullAccess --group-name kops
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonEventBridgeFullAccess --group-name kops
aws iam create-user --user-name kops
aws iam add-user-to-group --user-name kops --group-name kops
aws iam create-access-key --user-name kops
导出 AWS AccessKey/SecretKey
我们需要使用 AccessKey 和 SecretKey,需要将其导出。为了简化部署,本教程中没有切换用户,您可以手动通过 aws configure
切换到 kops
用户。
arduino
export AWS_ACCESS_KEY_ID=$(aws configure get aws_access_key_id)
export AWS_SECRET_ACCESS_KEY=$(aws configure get aws_secret_access_key)
创建集群状态存储 Bucket
为了保存集群的状态信息以及描述集群的配置信息,我们需要创建一个专用的 S3 存储桶供 kOps 使用。这个存储桶将成为保存集群配置的唯一可信来源。
ini
export KOPS_STATE_STORE_NAME=kops-state-store-${CLUSTER_NAME}
export KOPS_OIDC_STORE_NAME=kops-oidc-store-${CLUSTER_NAME}
export KOPS_STATE_STORE=s3://${KOPS_STATE_STORE_NAME}
aws s3api create-bucket \
--bucket ${KOPS_STATE_STORE_NAME} \
--region ${DEPLOY_REGION} \
--create-bucket-configuration LocationConstraint=${DEPLOY_REGION}
aws s3api create-bucket \
--bucket ${KOPS_OIDC_STORE_NAME} \
--region ${DEPLOY_REGION} \
--create-bucket-configuration LocationConstraint=${DEPLOY_REGION} \
--object-ownership BucketOwnerPreferred
aws s3api put-public-access-block \
--bucket ${KOPS_OIDC_STORE_NAME} \
--public-access-block-configuration BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false
aws s3api put-bucket-acl \
--bucket ${KOPS_OIDC_STORE_NAME} \
--acl public-read
创建集群
以下是创建集群的命令。我们将使用最基本的示例,以下命令将生成集群配置,但不会开始构建它。
ini
kops create cluster \
--name=${NAME} \
--cloud=aws \
--node-count=1 \
--control-plane-count=1 \
--zones=${DEPLOY_ZONE} \
--discovery-store=s3://${KOPS_OIDC_STORE_NAME}/${NAME}/discovery
现在我们进入实际构建集群的最后一步,这需要一段时间。完成后,您将需要等待更长时间,直到启动的实例完成下载 Kubernetes 组件并达到"就绪"状态。
bash
kops update cluster --name ${NAME} --yes --admin
kops export kubeconfig
# 等待集群状态 Ready
kops validate cluster --wait 10m --name ${NAME}
04/部署 Karpenter
准备必要的内容
我们需要这些环境变量来部署 Karpenter 和创建 NodePool/NodeClass。
通过 AWS CLI 获取 OIDC Provider 的信息、Issuer 地址和 AWS 账户 ID,确保后续部署正常进行。
ini
export OIDC_PROVIDER_ID=$(aws iam list-open-id-connect-providers \
--query "OpenIDConnectProviderList[?contains(Arn, '${NAME}')].Arn" \
--output text | awk -F'/' '{print $NF}')
export OIDC_ISSUER=${KOPS_OIDC_STORE_NAME}.s3.${DEPLOY_REGION}.amazonaws.com/${NAME}/discovery/${NAME}
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' \
--output text)
export AWS_INSTANCE_PROFILE_NAME=nodes.${NAME}
export KARPENTER_ROLE_NAME=karpenter.kube-system.sa.${NAME}
export CLUSTER_ENDPOINT=$(kubectl config view -o jsonpath="{.clusters[?(@.name=='${NAME}')].cluster.server}")
# 储存后续需要的临时文件
export TMP_DIR=$(mktemp -d)
创建 Karpenter IAM Role
我们需要为 Karpenter 创建和配置专用的 IAM Role 和 Policy,允许 Karpenter 通过 OIDC 身份验证该角色,并为该角色添加必要的权限,使其能够动态创建、管理和删除AWS资源(如EC2实例),以满足 Kubernetes 工作负载的需求。
swift
aws iam create-role \
--role-name ${KARPENTER_ROLE_NAME} \
--assume-role-policy-document "{
\"Version\": \"2012-10-17\",
\"Statement\": [
{
\"Effect\": \"Allow\",
\"Principal\": {
\"Federated\": \"arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/oidc.eks.${DEPLOY_REGION}.amazonaws.com/id/${OIDC_PROVIDER_ID}\"
},
\"Action\": \"sts:AssumeRoleWithWebIdentity\",
\"Condition\": {
\"StringEquals\": {
\"oidc.eks.${DEPLOY_REGION}.amazonaws.com/id/${OIDC_PROVIDER_ID}:sub\": \"system:serviceaccount:kube-system:karpenter\"
}
}
}
]
}"
aws iam create-role \
--role-name ${KARPENTER_ROLE_NAME} \
--assume-role-policy-document "{
\"Version\": \"2012-10-17\",
\"Statement\": [
{
\"Effect\": \"Allow\",
\"Principal\": {
\"Federated\": \"arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/${KOPS_OIDC_STORE_NAME}.s3.us-west-1.amazonaws.com/${NAME}/discovery/${NAME}\"
},
\"Action\": \"sts:AssumeRoleWithWebIdentity\",
\"Condition\": {
\"StringEquals\": {
\"${OIDC_ISSUER}:sub\": \"system:serviceaccount:kube-system:karpenter\"
}
}
}
]
}"
aws iam put-role-policy \
--role-name ${KARPENTER_ROLE_NAME} \
--policy-name InlineKarpenterPolicy \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:CreateFleet",
"ec2:CreateTags",
"ec2:DescribeAvailabilityZones",
"ec2:DescribeImages",
"ec2:DescribeInstanceTypeOfferings",
"ec2:DescribeInstanceTypes",
"ec2:DescribeInstances",
"ec2:DescribeLaunchTemplates",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSpotPriceHistory",
"ec2:DescribeSubnets",
"ec2:RunInstances",
"ec2:TerminateInstances",
"iam:PassRole",
"pricing:GetProducts",
"ssm:GetParameter",
"ec2:CreateLaunchTemplate",
"ec2:DeleteLaunchTemplate",
"sts:AssumeRoleWithWebIdentity"
],
"Resource": "*"
}
]
}'
部署 Karpenter
首先,我们需要配置一些额外的内容从而限制 Karpenter 仅在控制面运行,并且绑定和传入我们之前配置的内容,如 clusterEndpoint
/ clusterName
以及最重要的 IAM Role。
yaml
cat <<EOF > ${TMP_DIR}/values.yaml
serviceAccount:
annotations:
"eks.amazonaws.com/role-arn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/${KARPENTER_ROLE_NAME}"
replicas: 1
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: Exists
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: "kubernetes.io/hostname"
tolerations:
- key: CriticalAddonsOnly
operator: Exists
- key: node-role.kubernetes.io/master
operator: Exists
- key: node-role.kubernetes.io/control-plane
operator: Exists
- effect: NoExecute
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300
extraVolumes:
- name: token-amazonaws-com
projected:
defaultMode: 420
sources:
- serviceAccountToken:
audience: amazonaws.com
expirationSeconds: 86400
path: token
controller:
containerName: controller
image:
repository: docker.io/vacanttt/kops-karpenter-provider-aws
tag: latest
digest: sha256:24ef24de6b5565df91539b7782f3ca0e4f899001020f4c528a910cefb3b1c031
env:
- name: AWS_REGION
value: us-west-1
- name: AWS_DEFAULT_REGION
value: us-west-1
- name: AWS_ROLE_ARN
value: arn:aws:iam::${AWS_ACCOUNT_ID}:role/${KARPENTER_ROLE_NAME}
- name: AWS_WEB_IDENTITY_TOKEN_FILE
value: /var/run/secrets/amazonaws.com/token
extraVolumeMounts:
- mountPath: /var/run/secrets/amazonaws.com/
name: token-amazonaws-com
readOnly: true
logLevel: debug
settings:
clusterName: ${NAME}
clusterEndpoint: ${CLUSTER_ENDPOINT}
featureGates:
spotToSpotConsolidation: true
nodeRepair: false
EOF
通过 Helm 部署 Karpenter 到 kube-system
Namespace。
bash
export KARPENTER_NAMESPACE="kube-system"
helm upgrade --install karpenter \
oci://public.ecr.aws/karpenter/karpenter \
--namespace "${KARPENTER_NAMESPACE}" --create-namespace \
--wait -f $TMP_DIR/values.yaml
创建 Nodepool/NodeClass
我们需要获取 kOps 管理的 LaunchTemplate,并且使用它的 userData
来作为Karpenter EC2NodeClass 的 userData
,从而注册新节点到我们的集群中。
ini
export NODE_INSTANCE_GROUP=$(kops get instancegroups --name ${NAME} | grep Node | awk '{print $1}')
export NODE_LAUNCH_TEMPLATE_NAME=${NODE_INSTANCE_GROUP}.${NAME}
export USER_DATA=$(aws ec2 describe-launch-templates --region ${DEPLOY_REGION} --filters Name=launch-template-name,Values=${NODE_LAUNCH_TEMPLATE_NAME} \
--query "LaunchTemplates[].LaunchTemplateId" --output text | \
xargs -I {} aws ec2 describe-launch-template-versions --launch-template-id {} --region ${DEPLOY_REGION} \
--query "LaunchTemplateVersions[].LaunchTemplateData.UserData" --output text | base64 --decode)
将 NodeClass 以及 NodePool 暂存,您可以在应用前检查或是配置其他需要的内容。
yaml
cat <<EOF > ${TMP_DIR}/nodeclass.yaml
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
associatePublicIPAddress: true
amiFamily: AL2
tags:
kops.k8s.io/instancegroup: ${NODE_INSTANCE_GROUP}
KubernetesCluster: ${NAME}
k8s.io/role/node: "1"
aws-node-termination-handler/managed: ""
k8s.io/cluster-autoscaler/node-template/label/node-role.kubernetes.io/node: ""
subnetSelectorTerms:
- tags:
KubernetesCluster: ${NAME}
securityGroupSelectorTerms:
- tags:
Name: nodes.${NAME}
KubernetesCluster: ${NAME}
amiSelectorTerms:
- name: "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20241211"
instanceProfile: nodes.${NAME}
userData: |
$(echo "$USER_DATA" | sed 's/^/ /')
EOF
cat <<EOF > ${TMP_DIR}/nodepool.yaml
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: ["on-demand", "spot"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
expireAfter: 720h
limits:
cpu: 4
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
EOF
应用 NodeClass 和 NodePool 到集群中。
bash
kubectl apply -f ${TMP_DIR}/nodeclass.yaml
kubectl apply -f ${TMP_DIR}/nodepool.yaml
05/后续
创建 Workload 测试自动扩缩容
创建一个 4 Replica,并且请求一定资源的 Workload。在预期的情况下,会有 2 个 Replica 因为资源不足而 Pending。
yaml
cat <<EOF > ${TMP_DIR}/workload.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: workload
namespace: default
labels:
app: workload
spec:
replicas: 4
selector:
matchLabels:
app: workload
template:
metadata:
labels:
app: workload
spec:
containers:
- name: pause
image: public.ecr.aws/eks-distro/kubernetes/pause:3.7
resources:
requests:
cpu: "550m"
memory: "128Mi"
EOF
bash
kubectl apply -f ${TMP_DIR}/workload.yaml
您可以检查是否有 NodeClaim 被创建,在 NodeClaim 被创建约 70 秒后,新节点将注册到集群中。
删除集群
在 AWS 中运行 Kubernetes 集群显然需要持续投入成本,因此如果您完成实验后可能需要删除集群。 当您确定要删除集群时,请输入带有--yes
标志的删除命令。
bash
kops delete cluster --name ${NAME} --yes
kOps + Karpenter 的优势和局限
kOps 和 Karpenter 的结合为 Kubernetes 集群的自动化管理带来了强大的功能,但同时也存在一些局限性。
在优势方面,Karpenter 能够根据 Pod 的实际需求动态配置节点,这不仅提高了资源利用率,还能快速响应工作负载的变化,避免资源浪费或不足。
同时,支持多样化的实例类型,用户可以根据不同的使用场景选择最佳的实例类型,从而进一步优化性能和成本。
然而,这种组合也有局限性。
首先,由于无法使用 EKS 的 bootstrap.sh
脚本,Kubelet 的配置受限于 kOps 的控制,这意味着无法在 NodeClass 中自定义与 Kubelet 相关的参数。
其次,集群的控制面节点必须使用 ASG(自动扩展组)而非 Karpenter,这在一定程度上限制了对控制面节点的弹性管理。
此外,Karpenter 的正常运行需要指定至少一个 InstanceGroup,未设置时节点将无法成功注册到集群,这增加了配置的复杂性。
尽管如此,kOps 和 Karpenter 的结合仍然是一个强有力的工具组合,适合需要动态扩展和多实例支持的场景,但在实施时需要注意这些局限性并做好相关规划。