本文档初版写于 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,方法:
- 建一个 NodePort 类型的 Service 而不是 LoadBalancer 类型的 Service
- 把适当的 instance-group 对应的 auto-scaling-group 指到一个 Target Group 上
- 把一个 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,但是网络测试的结果非常不理想。尽管快的时候很快,但是稳定性非常差,看起来还不能用于生产环境。除此以外还有很多隐藏的坑,不在此详述了。
作者:若瑜(知乎 && 即刻)
参考: