AWS 中国区部署 Kubernetes 1.9.3

733 阅读9分钟
原文链接: zhuanlan.zhihu.com
本文档初版写于 2018 年 6 月, kops release 最新版本为 1.9.1
本文没有覆盖 Kubernetes 的基础知识,不清楚的应该去官网跑一遍 minikube 的 demo

0. 目标和背景

即刻技术团队一直在实践 DevOps 文化,部署 Kubernetes 集群的目标是 Production Ready,要求集群稳定性必须达到生产标准。

即刻到目前为止已经在生产环境使用过两套 Kubernetes 部署方案,这两套方案都有稳定性以及其他方面的问题。

第一套方案:juju

juju 使用很方便,但是社区不活跃,可定制性太弱,而且后来遇到了 ubuntu 实例的 kernel panic 问题。

第二套方案:kubespray

选择第二套方案的时候 kops 还没有集成 gossip,无法使用于 AWS 中国区,当时选择了可定制性极强的 kubespray。kubespray 本身没有什么大问题,其问题反而是太过灵活,缺乏最佳实践。结果遇到了这些问题:

  • 从过去的经验出发我当时选择了 CentOS 作为操作系统,但是后来频繁遇到 docker issue 5618,没有太好的解决方案,这个 bug 在 CentOS / RedHat 上更容易触发;
  • launch configuration 和 autoscaling group 需要自己写脚本管理,很容易出错;
  • 缩容的时候需要做一些繁琐的事情,包括把它从 autoscaling group 中移出,drain 它,删除 node,如果使用了 calico 还需要把它从 calico 配置中移走,最后才能关闭机器。

在选择第三套方案的时候我们吸取了前两次的经验,需要在易用性和可定制性之间找到一个平衡,而同时 kops 已经集成了 gossip 部署方案,看起来是一个成熟的解决方案。但是在实施过程中我们还是遇到了很多问题,以至于切换集群的半个多月中产生了若干次的服务故障。本文后面会覆盖到我们踩过的主要的坑,但也难免有所遗漏,欢迎大家提问和补充。


1. 平台、工具选择

  • 部署工具:kops,因为它不需要很多定制就可以直接集成以下功能
    • EBS -> PV
    • LoadBalancer
    • flannel vxlan
    • AutoScalingGroup
  • 操作系统:debian jessie,虽然功能落后于 ubuntu,但是稳定

2. 难题和解决方案

AWS 中国区没有 Route53 这个 DNS 服务

自从 kops 1.7 以后,就支持 gossip 了,只要 cluster 名字是以 k8s.local 结尾,就不用做任何 DNS 配置。

缺少基础 AMI

中国区的 market-place 没有 k8s-1.8-debian-jessie-amd64-hvm-ebs-YYYY-mm-dd 这样的基础镜像,需要自己 build 一个
build 出来的镜像 kernel 是定制的,内核参数也已经做过调优,可以直接投入生产使用。

flannel 的坑

想使用 flannel,还需要加载一个内核模块 br_netfilter,否则会遇到这个 kube-dns 的问题。比较方便的办法是制作 AMI 之前把它写到 /etc/modules 文件中。

有些基础组件还是放在下载困难的地方

原本 kops 支持配置 containerRegistry 的,但是目前的版本似乎有 bug,这个选项无法生效,为此需要做一些额外的工作,主要包括:

  • offline mode 离线模式
  • 拉一些别的镜像再生成新的 AMI:用 build 出来的 AMI 创建一个 EC2 实例,然后设置好代理,拉好以下镜像:
gcr.io/google_containers/cluster-proportional-autoscaler-amd64:1.1.2-r2
gcr.io/google_containers/etcd:2.2.1
gcr.io/google_containers/k8s-dns-dnsmasq-nanny-amd64:1.14.10
gcr.io/google_containers/k8s-dns-kube-dns-amd64:1.14.10
gcr.io/google_containers/k8s-dns-sidecar-amd64:1.14.10
gcr.io/google_containers/pause-amd64:3.0
gcr.io/google_containers/kube-apiserver:v1.9.3
gcr.io/google_containers/kube-controller-manager:v1.9.3
gcr.io/google_containers/kube-proxy:v1.9.3
gcr.io/google_containers/kube-scheduler:v1.9.3
提示:假如不确定还需要什么镜像,可以在尝试实施部署,设置好全局的 HTTP 代理,可以顺利部署完,然后就可以看到镜像列表了

提示的提示:但是部署了 HTTP 代理的集群不应该投入生产环境使用

3. 开始部署

3.0 准备工作

  • 创建一个 IAM user,其中 Route53 的部分跳过
  • 想一个以 .k8s.local 结尾的集群名字,比如 jike-test.k8s.local

3.1 配置 AWS 环境

下面的脚本中,所有以 your- 开头的名字都是随便取的,请按实际情况修改。

# 安装 awscli
pip install awscli

# 生成 awscli 使用的本地配置,输入上一步骤拿到的 key 和 secret,还有 region,北京区那就是 cn-north-1。如果已经配置过则可以跳过
aws configure

export AWS_REGION=$(aws configure get region)
export AWS_AZ=a  # 可用区
export NAME=your-cluster.k8s.local

# 开一个 s3 bucket 存放 kops state
export KOPS_STATE_STORE=your-kops-store
aws s3api create-bucket --bucket $KOPS_STATE_STORE --create-bucket-configuration LocationConstraint=$AWS_REGION

# 从 key-pair 生成一个 public key
export SSH_PUBLIC_KEY=your-public-key.pub  # 这个 public key 将会被 copy 到集群中的所有节点上,方便 ssh 登陆
ssh-keygen -f pemfile.pem -y >${SSH_PUBLIC_KEY}

# 在真正部署之前所有需要的环境变量还有这些没配置过
export VPC_ID=your-vpc  # 打算放到哪个 VPC
export VPC_NETWORK_CIDR=your-vpc-network-cidr  # VPC 的 CIDR
export SUBNETS=your-vpc-subnet  # VPC 的哪个子网
export UTILITY_SUBNETS=your-vpc-utility-subnet  # 暂时没用上,如果需要部署 bastion host,会放在这个子网中
export AMI=your-ami  # AMI 的 source,注意前面有串数字
export KUBERNETES_VERSION="v1.9.3"  # 虽然 kops 1.9.1 发布的时候 kubernetes 已经发布了 1.9.7,但是推荐的版本还是 1.9.3
export KOPS_VERSION="1.9.1"
export ASSET_BUCKET="kops-asset"  # offline mode 需要
export KOPS_BASE_URL="https://s3.cn-north-1.amazonaws.com.cn/$ASSET_BUCKET/kops/$KOPS_VERSION/"  # 同上
export CNI_VERSION_URL="https://s3.cn-north-1.amazonaws.com.cn/$ASSET_BUCKET/kubernetes/network-plugins/cni-plugins-amd64-v0.6.0.tgz"  # 需要的 CNI
export CNI_ASSET_HASH_STRING="d595d3ded6499a64e8dac02466e2f5f2ce257c9f"  # CNI 的 sha1 hash
export SSH_ACCESS=your-cidr1,your-cidr2  # 逗号分隔的 CIDR,用于限制 ssh 登陆的来源 IP,会写进安全组里

3.2 创建集群

kops create cluster \
    --zones ${AWS_REGION}${AWS_AZ} \
    --vpc ${VPC_ID} \
    --network-cidr ${VPC_NETWORK_CIDR} \
    --image ${AMI} \
    --associate-public-ip=true \
    --api-loadbalancer-type public \  # 如果要在公网使用 kubectl 控制集群,就设置为 public,否则设置为 internal
    --topology public \  # 节点放在 public 子网
    --networking flannel \  # 默认 backend 就是 VXLAN
    --kubernetes-version https://s3.cn-north-1.amazonaws.com.cn/$ASSET_BUCKET/kubernetes/release/$KUBERNETES_VERSION \
    --ssh-public-key ${SSH_PUBLIC_KEY} \
    --subnets ${SUBNETS} \
    --utility-subnets ${UTILITY_SUBNETS} \  # bastion 会使用
    --master-count 3 \  # master 节点数量,建议配置为至少 3,后面不好变更
    --master-size m4.xlarge \  # 参看这里:https://kubernetes.io/docs/admin/cluster-large/
    --node-count 1 \
    --node-volume-size 200 \
    ${NAME}

3.3 实施前的设置

到这里,我们已经在 state store 中生成了一份集群的配置,但是还没有实施部署。

看一下目前的集群配置

kops get clusters $NAME -o yaml 

看一下所有所有的配置,包括 instance groups(后面简称为 IG)

kops get --name $NAME -o yaml

列出所有的 IG

kops get ig --name $NAME

目前能看到两种 ROLE:master 和 node

默认配置需要做一些调整以保证其正常工作,配置主要包含集群配置和 IG 配置。

  • 修改集群配置 kops edit cluster $NAME
spec:
  docker:
    registryMirrors:
    - https://xxxx  # docker registry mirror 地址,加速 docker.io 上的镜像的下载速度
  kubelet:  # 一些 kubelet cgroup 相关的配置
    kubeletCgroups: "/systemd/system.slice"  # 见 https://github.com/kubernetes/kops/issues/4049
    runtimeCgroups: "/systemd/system.slice"
    imageGCHighThresholdPercent: 70  # 旧镜像回收
    imageGCLowThresholdPercent: 50  # 同上
  masterKubelet:  # 一些 master 上的 kubelet cgroup 相关的配置
    kubeletCgroups: "/systemd/system.slice"
    runtimeCgroups: "/systemd/system.slice"
  • 修改 IG 配置 kops edit ig your-ig-name --name $NAME
spec:
  rootVolumeOptimization: true  # 打开 EBS 优化
  rootVolumeSize: 200  # volume 大小
  nodeLabels: [...]  # 一些自定义的标签
  taints: [...]  # 一些 taint

3.4 实施部署

kops update cluster $NAME --yes

等几分钟,然后

kops validate cluster

如果显示为 Ready,那就一切就绪了
假如显示 NotReady,并且提示说 kube-dns 没有启动,有可能是给 Node 加的 taint 使得 kube-dns 找不到可以分配的 node,可以通过修改 toleration 去解决

3.5 部署以后

  • 增加、修改、删除 IG
kops create ig $IG_NAME --name $NAME --edit

注意修改配置,尤其是 spec.image,默认的镜像在中国区是找不到的。

  • 安装 kubernetes-dashboard
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/recommended/kubernetes-dashboard.yaml

确认相关 pod 已经起来以后

kubectl proxy

这时使用浏览器打开

http://localhost:8001/ui

会发现需要输入一些东西才能登陆,和以前不一样了。
注意,我们创建集群时生成的 config 文件是不能直接当作登陆凭证的。具体方法见文档

  • 给 Master / Node 增加 IAM Statement

kops 支持对的特定 role 增加 IAM Statement。

注意是 role 而不是 IG。所以如果要给一个 node 增加一个 Statement,那么所有的 node 都会有这个 Statement。

先看一下原来的 Node 有哪些策略:

aws iam list-role-policies --role-name nodes.${NAME}

结果是

{
    "PolicyNames": [
        "nodes.${NAME}"
    ]
}

注意这里的 role name 和 policy name 用了同一个名字。这是一个 Inline 的 policy,看看里面有什么 Statement,运行

aws iam get-role-policy --role-name nodes.${NAME} --policy-name nodes.${NAME}

结果是

{
    "RoleName": "nodes.${NAME}",
    "PolicyName": "nodes.${NAME}",
    "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "kopsK8sEC2NodePerms",
                "Effect": "Allow",
                "Action": [
                    "ec2:DescribeInstances",
                    "ec2:DescribeRegions"
                ],
                "Resource": [
                    "*"
                ]
            },
            {
                "Sid": "kopsK8sS3GetListBucket",
                "Effect": "Allow",
                "Action": [
                    "s3:GetBucketLocation",
                    "s3:ListBucket"
                ],
                "Resource": [
                    "arn:aws-cn:s3:::kops-k8s-v1-state-store"
                ]
            },
            {
                "Sid": "kopsK8sS3NodeBucketSelectiveGet",
                "Effect": "Allow",
                "Action": [
                    "s3:Get*"
                ],
                "Resource": [
                    "arn:aws-cn:s3:::kops-k8s-v1-state-store/jike-a.k8s.local/addons/*",
                    "arn:aws-cn:s3:::kops-k8s-v1-state-store/jike-a.k8s.local/cluster.spec",
                    "arn:aws-cn:s3:::kops-k8s-v1-state-store/jike-a.k8s.local/config",
                    "arn:aws-cn:s3:::kops-k8s-v1-state-store/jike-a.k8s.local/instancegroup/*",
                    "arn:aws-cn:s3:::kops-k8s-v1-state-store/jike-a.k8s.local/pki/issued/*",
                    "arn:aws-cn:s3:::kops-k8s-v1-state-store/jike-a.k8s.local/pki/private/kube-proxy/*",
                    "arn:aws-cn:s3:::kops-k8s-v1-state-store/jike-a.k8s.local/pki/private/kubelet/*",
                    "arn:aws-cn:s3:::kops-k8s-v1-state-store/jike-a.k8s.local/pki/ssh/*",
                    "arn:aws-cn:s3:::kops-k8s-v1-state-store/jike-a.k8s.local/secrets/dockerconfig"
                ]
            },
            {
                "Sid": "kopsK8sS3NodeBucketGetKuberouter",
                "Effect": "Allow",
                "Action": [
                    "s3:Get*"
                ],
                "Resource": "arn:aws-cn:s3:::kops-k8s-v1-state-store/jike-a.k8s.local/pki/private/kube-router/*"
            },
            {
                "Sid": "kopsK8sECR",
                "Effect": "Allow",
                "Action": [
                    "ecr:GetAuthorizationToken",
                    "ecr:BatchCheckLayerAvailability",
                    "ecr:GetDownloadUrlForLayer",
                    "ecr:GetRepositoryPolicy",
                    "ecr:DescribeRepositories",
                    "ecr:ListImages",
                    "ecr:BatchGetImage"
                ],
                "Resource": [
                    "*"
                ]
            }
        ]
    }
}

可以看到这里面有一些 Statement,每个对应了一组 Permission。

接下来给 Node 增加一个 Permission Statement:

kops edit cluster

然后 .spec 中增加:

spec:
  additionalPolicies:
    node: |
      [
        {
          "Action": ["ec2:*"],
          "Effect": "Allow",
          "Resource": "*"
        }
      ]

然后运行

kops update cluster --yes 

再次运行

aws iam list-role-policies --role-name nodes.${NAME}

结果变了:

{
    "PolicyNames": [
        "additional.nodes.${NAME}",
        "nodes.${NAME}"
    ]
}

其中 additional.nodes.${NAME} 这个 policy inline 地包含了我们刚刚增加的 permission,验证一下:

aws iam get-role-policy --role-name nodes.${NAME} --policy-name additional.nodes.${NAME}

结果为

{
    "RoleName": "nodes.${NAME}",
    "PolicyName": "additional.nodes.${NAME}",
    "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "",
                "Effect": "Allow",
                "Action": [
                    "ec2:*"
                ],
                "Resource": "*"
            }
        ]
    }
}

4. 坑

kube-dns 的 scale 问题

运行一段时间后发现有些网络问题,pod 之间互相访问不到,最后发现可能是 DNS 解析失败了,究其原因是 kube-dns pod 数量不够。
直接改 scale 之后发现会被 cluster-proportional-autoscaler 改回去,这是因为这个 autoscaler 是根据集群规模去修改 kube-dns 的规模的,主要参考 node 数量和 cpu 核心数量,所以想增加 kube-dns,就要修改 autoscaler 的配置

HPA 不工作

有这个 issue,需要部署 metrics-server
如果不想部署,那可以在 ClusterSpec 里定义 KubeControllerManagerConfig.horizontalPodAutoscalerUseRestClients


5. 常见问题

LoadBalancer Service 怎么使用

本来以为可以直接把 pod 路由到 ELB 上,试了一下发现还是创建了 NodePort,然后让 ELB 监听 NodePort。这样做有方便的地方,比如不需要自己去管理 ELB 到 Auto Scaling Group 的关系,也可以自动配上 ssl 证书。

但是,有几点需要注意

  • HTTPS 的问题

一个 Service,如果配置了两个端口,并且配置了 SSL 证书,那么默认情况下这两个端口都会变成 HTTPS 的。如果想要只有指定端口使用 HTTPS,其余端口走 HTTP,需要设置 service.beta.kubernetes.io/aws-load-balancer-ssl-ports

  • 如何使用 Application Load Balancer(ALB)

不能支持,目前只能建立 Classic 的 ELB,所以也有后面的 WebSocket 问题

  • 如何支持 WebSocket?

不方便支持,因为目前 kops 只能建出 ELB 而建不出 ALB,除非打开 Proxy Protocol,但是这样配置起来很麻烦。支持 WebSocket 最好的办法是使用 ALB,方法:

  1. 建一个 NodePort 类型的 Service 而不是 LoadBalancer 类型的 Service
  2. 把适当的 instance-group 对应的 auto-scaling-group 指到一个 Target Group 上
  3. 把一个 Application LB 指到这个 Target Group 上
  • 手动配置被重置的问题

一个 Service 生成的 ELB,如果去 AWS 控制台上加了一些自定义的配置,例如增加端口,那么在 master 被重启或者 rollingUpdate 时,ELB 上的配置会被重置为 Service 自动生成的配置。所以不要去手动修改 ELB 的配置,否则后期会难以升级集群。

  • 一个 TargetPort 必须对应一个 NodePort

有时候希望一个 TargetPort 映射到一个 NodePort,一个 NodePort 映射到 ELB 的两个端口,这件事做不到。

update 和 rolling-update 操作

rolling-update 是 kops 最棒的特性之一,可以在尽量不伤害集群的情况下对集群进行滚动升级,但是 kops 的新手经常不知道什么时候应该运行 rolling-update 操作。
总的来说,如果变更在 node 重启前无法生效,就必定需要 rolling-update。
所幸,不管是 update 还是 rolling-update,都需要加上 --yes 才会实装,所以如果不确定,可以先跑一次 kops rolling-update cluster --name $NAME 看看结果。

以下是一些提示:

  • 有些操作可能出乎意料地需要 rolling update,例如给 IG 里的 node 加 label
  • rolling update 的默认操作间隔很长,可以通过参数调整:--master-interval 和 --node-interval
  • rolling update 如果中断,不用担心,可以 resume,直到集群达到预定的目标
  • 如果要强制进行 rolling update,可以加上 --force

为什么 docker storage-driver 使用 overlay 而不是 overlay2

AMI 里的 docker 版本是 17.03.2,为什么使用的 storage-driver 是 overlay 而不是 overlay2:stretch 才推荐 overlay2

为什么默认 CIDR 是 100.64.0.0/10 ?

K8S 内的网络(包括 Pod 和 Service)使用的 CIDR 默认是 100.64.0.0/10,这是故意的,当然在建立集群的时候也可以改。

如何启用 bastion?

使用 Bastion 主机作为跳板机,可以把集群中的节点与公网隔离开。

可以在建集群的时候加上 --bastion 选项,但是 kops 会要求 master 和 node 的 topology 都是 private 的。如果已经新建了 public 的 master 或 node,那么可以在集群建立以后再加入 bastion:

kops create instancegroup bastions --role Bastion --subnet ${UTILITY-SUBNET}

UTILITY-SUBNET 前文中提到过。

不考虑 IPVS 吗?

K8S 1.9 中 IPVS 模式已经进入 beta。实际上我们尝试过 kube-router,但是网络测试的结果非常不理想。尽管快的时候很快,但是稳定性非常差,看起来还不能用于生产环境。除此以外还有很多隐藏的坑,不在此详述了。


作者:若瑜(知乎 && 即刻)

参考:

Kubernetes 官方文档

Kops 官方文档