kOps + Karpenter 集成实践:实现 K8s 集群的动态扩展

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 的结合仍然是一个强有力的工具组合,适合需要动态扩展和多实例支持的场景,但在实施时需要注意这些局限性并做好相关规划。

推荐阅读

咨询公司 CEO 暴论:AWS 转售是个坑,早该凉了!

CA 不够用了?Azure 推 Karpenter + Spot,让 AKS 便宜 80%!

Prometheus v2.47+Karpenter:轻松月省4万云成本

相关推荐
小刘爱喇石( ˝ᗢ̈˝ )17 分钟前
k8s中service概述(一)ClusterIP
云原生·容器·kubernetes
有梦想的攻城狮23 分钟前
【一起来学kubernetes】21、Secret使用详解
云原生·eureka·kubernetes·secret
神奇的海马体9 小时前
Kubeasz工具快速部署K8Sv1.27版本集群(二进制方式)
docker·容器·kubernetes·kubeasz
海鸥8113 小时前
在K8S中挂载 Secret 到 Pod
云原生·容器·kubernetes
忍界英雄14 小时前
k8s中的service解析
java·容器·kubernetes
海鸥8114 小时前
K8S中若要挂载其他命名空间中的 Secret
云原生·容器·kubernetes
云上艺旅16 小时前
K8S学习之基础三十六:node-exporter部署
学习·云原生·贪心算法·kubernetes·prometheus
rocksun16 小时前
Kubernetes中Argo CD ApplicationSet Generators的艺术
kubernetes
郁大锤18 小时前
Docker Compose 和 Kubernetes(K8s)对比
docker·容器·kubernetes·k8s