代码优先 Kubernetes(2):部署不安全的 K8s 集群

上一个部分中,我部署完了一个安全的 etcd 集群。在这个部分,我将部署一个可用的,但是并不安全的 K8s 集群。

准备节点

除了需要继续用到第一部分使用的管理节点和 3 个 master 节点外,本文的操作将用到 2 个新的节点,分别为 node-1node-2。这里也同样假设这两个节点是 CentOS 1810 最小化安装的节点。如果应用于其他 Linux 发行版,可能需要调整部分命令。

和之前已用到的节点一样,不需要这两个节点的 root 账号,但是需要用到 sudo。

更新 hostname 和增加 hosts 解析

首先为 Ansible 的 hosts 文件加入两个新节点的信息(注意替换其中的 IP 信息):

sudo bash -c 'cat >> /etc/ansible/hosts' << EOF
[nodes]
node-1 ansible_host=10.0.2.24
node-2 ansible_host=10.0.2.25
EOF

然后在 hosts 文件中加入两个新节点的信息(注意替换其中的 IP 信息):

sudo bash -c 'cat >> /etc/hosts' << EOF
10.0.2.24 node-1
10.0.2.25 node-2
EOF

最后,为所有节点更新 hostname 和 hosts:

cat > p2-pb1.yml << EOF
- hosts: all
  become: true
  tasks:
    - hostname:
        name: "{{ inventory_hostname }}"
    - lineinfile:
        regexp: ".*{{ item }}$"
        line: "{{ hostvars[item].ansible_host }} {{ item }}"
        state: present
        dest: /etc/hosts
      with_items: "{{ ansible_play_hosts_all }}"
EOF
ansible-playbook -k -K p2-pb1.yml

部署控制平面

控制平面由 3 个 master 节点组成。

安装 Kubernetes master 组件

直接通过 yum 安装 Kubernetes master 组件:

cat > p2-pb2.yml << EOF
- hosts: masters
  become: true
  tasks:
    - yum:
        name: kubernetes-master
        state: latest
EOF
ansible-playbook -k -K p2-pb2.yml

配置 kube-apiserver

Kubernetes 的组件中,只有 kube-apiserver 需要访问 etcd。而因为我的 etcd 配置了 SSL 双向认证,因此需要为 3 个 master 节点生成对应的 etcd 客户端证书:

for i in {1..3}
do
  openssl genrsa -out etcd-client-master-$i.key 2048
  openssl req -new -key etcd-client-master-$i.key -out etcd-client-master-$i.csr
  openssl x509 -req -in etcd-client-master-$i.csr -CA etcd-client-ca.crt -CAkey etcd-client-ca.key -CAcreateserial -out etcd-client-master-$i.crt
done

把证书拷贝到对应的节点,修改 kube-apiserver 配置文件,然后启动 kube-apiserver:

cat > p2-pb3.yml << EOF
- hosts: masters
  become: true
  tasks:
    - lineinfile:
        regexp: .*KUBE_API_ADDRESS=.*
        line: KUBE_API_ADDRESS="--insecure-bind-address=0.0.0.0"
        state: present
        dest: /etc/kubernetes/apiserver
    - set_fact:
        etcd_members: "{{ (etcd_members | default([])) + ['https://'+item+':2379'] }}"
      with_items: "{{ ansible_play_hosts_all }}"
    - lineinfile:
        regexp: .*KUBE_ETCD_SERVERS=.*
        line: KUBE_ETCD_SERVERS="--etcd-servers={{ etcd_members | join(',') }}"
        state: present
        dest: /etc/kubernetes/apiserver
    - copy:
        src: etcd-server-ca.crt
        dest: /etc/kubernetes/etcd-server-ca.crt
    - copy:
        src: etcd-client-{{ inventory_hostname }}.crt
        dest: /etc/kubernetes/etcd-client.crt
    - copy:
        src: etcd-client-{{ inventory_hostname }}.key
        dest: /etc/kubernetes/etcd-client.key
    - lineinfile:
        regexp: .*KUBE_API_ARGS=.*
        line: KUBE_API_ARGS="--advertise-address={{ hostvars[inventory_hostname].ansible_host }} --etcd-cafile /etc/kubernetes/etcd-server-ca.crt --etcd-certfile /etc/kubernetes/etcd-client.crt --etcd-keyfile /etc/kubernetes/etcd-client.key"
        state: present
        dest: /etc/kubernetes/apiserver
    - systemd:
        name: kube-apiserver.service
        state: started
        enabled: yes
EOF
ansible-playbook -k -K p2-pb3.yml

这里我修改配置的原则是只配置必须的项。如果后面遇到有些因配置项导致的问题,我再针对着修改。

为了让客户端可以访问到 API,需配置 firewalld 打开对应的端口:

cat > p2-pb4.yml << EOF
- hosts: masters
  become: true
  tasks:
    - firewalld:
        port: 6443/tcp
        state: enabled
        permanent: yes
        immediate: yes
    - firewalld:
        port: 8080/tcp
        state: enabled
        permanent: yes
        immediate: yes
EOF
ansible-playbook -k -K p2-pb4.yml

验证下 K8s 集群状态:

sudo yum install kubernetes-client -y
kubectl -s master-1:8080 get cs

不出意外的话,应该有 5 个条目,分别是 scheduler、controller-manager 和 3 个 etcd 节点,但是全都是 Unhealthy 的状态。前两者比较好理解,因为我还没有启动 scheduler 和 controller-manager,那 3 个 etcd 节点的不健康状态说来有点复杂,后面解释。

配置 kube-controller-manager

kube-controller-manager 目前只需要配置下 apiserver 的信息就行了:

cat > p2-pb5.yml << EOF
- hosts: masters
  become: true
  tasks:
    - lineinfile:
        regexp: .*KUBE_MASTER=.*
        line: KUBE_MASTER="--master=http://master-1:8080"
        state: present
        dest: /etc/kubernetes/config
    - systemd:
        name: kube-controller-manager.service
        state: started
        enabled: yes
EOF
ansible-playbook -k -K p2-pb5.yml

这里有一点需要提一下。我在上面配置启动的 apiserver 有 3 个,但这里只用了 master-1 上的 apiserver。这其实并不满足高可用的要求:如果 master-1 挂了,那么这个 controller 也就联系不上 apiserver,哪怕实际上还有两个 apiserver 在运行。一个更好的做法是设置一个高可用的负载均衡器,它能将通往 apiserver 的请求均衡的路由到健康的 apiserver 上。不过目前我不打算这么折腾,因此就先容忍下这个问题。

再次看下集群状态,不出意外的话,controller-manager 应转为健康状态了:

kubectl -s master-1:8080 get cs

配置 kube-scheduler

和 kube-controller-manager 一样,kube-scheduler 也只需要配置下 apiserver 信息就行了。更妙的是,上面配置 kube-controller-manager 用到的 /etc/kubernetes/config 配置是和 kube-scheduler 共享的,所以其实已经不用改什么了,直接启动 kube-scheduler 即可:

cat > p2-pb6.yml << EOF
- hosts: masters
  become: true
  tasks:
    - systemd:
        name: kube-scheduler.service
        state: started
        enabled: yes
EOF
ansible-playbook -k -K p2-pb6.yml

再次看下集群状态,不出意外的话,这次 scheduler 应转为健康状态了:

kubectl -s master-1:8080 get cs

解决 etcd 健康状态异常的问题

刚才在刚拉起 apiserver 的时候,通过查看集群状态可以发现 3 个 etcd 节点处于不健康的状态,并且错误信息和 TLS 的证书相关。经过一番搜索后,发现是这个版本(1.5.2)的 K8s 的一个 bug。我尝试用最新版(1.13)的 K8s 试了一下,发现最新版中是没有这个问题的,进一步确认了这的确是一个 K8s 的 bug。

不过既然已经用了 1.5.2 的版本,我还是打算用这个版本继续试一下。既然这个版本的 etcd 客户端证书支持有问题,那么就先把 etcd 的客户端证书验证给关了:

cat > p2-pb7.yml << EOF
- hosts: masters
  become: true
  serial: 1
  tasks:
    - lineinfile:
        regexp: .*ETCD_CLIENT_CERT_AUTH=.*
        line: ETCD_CLIENT_CERT_AUTH="false"
        state: present
        dest: /etc/etcd/etcd.conf
    - systemd:
        name: etcd.service
        state: restarted
EOF
ansible-playbook -k -K p2-pb7.yml

检查下是否全部变成了健康的状态:

kubectl -s master-1:8080 get cs

部署 2 个 K8s node

目前,控制平面已经起来,但是还没有真正干活的节点。K8s 把真正干活的节点叫做,呃,“节点”。

安装 Kubernetes node 组件

直接通过 yum 安装:

cat > p2-pb8.yml << EOF
- hosts: nodes
  become: true
  tasks:
    - yum:
        name: kubernetes-node
        state: latest
EOF
ansible-playbook -k -K p2-pb8.yml

配置 kubelet 服务

修改 kubelet 配置,然后启动:

cat > p2-pb9.yml << EOF
- hosts: nodes
  become: true
  tasks:
    - lineinfile:
        regexp: .*KUBELET_ADDRESS=.*
        line: KUBELET_ADDRESS="--address=0.0.0.0"
        state: present
        dest: /etc/kubernetes/kubelet
    - lineinfile:
        regexp: .*KUBELET_HOSTNAME=.*
        line: KUBELET_HOSTNAME="--hostname-override={{ inventory_hostname }}"
        state: present
        dest: /etc/kubernetes/kubelet
    - lineinfile:
        regexp: .*KUBELET_API_SERVER=.*
        line: KUBELET_API_SERVER="--api-servers=http://master-1:8080"
        state: present
        dest: /etc/kubernetes/kubelet
    - systemd:
        name: kubelet.service
        state: started
        enabled: yes
EOF
ansible-playbook -k -K p2-pb9.yml

确认下 2 个 node 都加入了集群:

kubectl -s master-1:8080 get nodes

配置 kube-proxy 服务

修改 kube-proxy 配置,然后启动:

cat > p2-pb10.yml << EOF
- hosts: nodes
  become: true
  tasks:
    - lineinfile:
        regexp: .*KUBE_MASTER=.*
        line: KUBE_MASTER="--master=http://master-1:8080"
        state: present
        dest: /etc/kubernetes/config
    - systemd:
        name: kube-proxy.service
        state: started
        enabled: yes
EOF
ansible-playbook -k -K p2-pb10.yml

尝试部署第一个应用

如果不出意外的话,到这里就应该能部署应用了:

kubectl -s master-1:8080 run busybox --image=busybox --command -- sleep 3600

不过部署并不会成功。通过查看 deployment 或者 pod 信息可以发现,K8s 并没有为这个应用创建对应的 pod。这里我通过 kubectl -s master-1:8080 get events 发现,是权限控制导致的。搜了一下,定位是 kube-apiserver 的 --admission-control 选项导致的。因为暂时我的目标是一个不安全但是可以工作的 K8s 集群,因此果断把这个选项置空,跳过鉴权。

cat > p2-pb11.yml << EOF
- hosts: masters
  become: true
  tasks:
    - replace:
        regexp: (^.*KUBE_ADMISSION_CONTROL=.*)
        replace: '# \1'
        path: /etc/kubernetes/apiserver
    - systemd:
        name: kube-apiserver.service
        state: restarted
EOF
ansible-playbook -k -K p2-pb11.yml

但是,我的 pod 还是没有起来。再次通过 kubectl -s master-1:8080 get events 发现,是 node 访问 Docker registry 的时候遇到了 CA 证书问题。可以通过创建一个空白的 CA 来解决这个问题:

cat > p2-pb12.yml << EOF
- hosts: nodes
  become: true
  tasks:
    - file:
        path: /etc/rhsm/ca/redhat-uep.pem
        state: touch
    - systemd:
        name: kubelet.service
        state: restarted
EOF
ansible-playbook -k -K p2-pb12.yml

再看下 pod 是否正常创建并运行了。这里可能稍微需要等待一会,因为需要先下载几个 Docker 镜像。不出意外的话,1-2 分钟后就能看到 1 个正常运行的 pod 了:

kubectl -s master-1:8080 get pods

尝试访问下 pod:

POD_NAME=$(kubectl -s master-1:8080 get pods -l run=busybox -o jsonpath="{.items[0].metadata.name}")
kubectl -s master-1:8080 exec -ti $POD_NAME -- echo 'Hello world!'

访问失败了。错误信息提示无法连上 node 的 10250 端口。看来又是 firewalld 阻止了对这个端口的访问。强忍着粗暴干掉 firewalld 的冲动,我再一次礼貌的让 firewalld 打开我要的端口:

cat > p2-pb13.yml << EOF
- hosts: nodes
  become: true
  tasks:
    - firewalld:
        port: 10250/tcp
        state: enabled
        permanent: yes
        immediate: yes
EOF
ansible-playbook -k -K p2-pb13.yml

再尝试下 kubectl -s master-1:8080 exec -ti $POD_NAME -- echo 'Hello world!' 命令,应该能输出令人熟悉的 ”Hello world!“。

接下来尝试改下副本数:

kubectl -s master-1:8080 scale deployment/busybox --replicas=3

看起来我的 K8s 集群可以工作了,或者至少目前看来是这样的。

总结

基于上一部分部署完成的 etcd 集群,我部署完了一个可用的 K8s 集群,它包含 3 个 master 组成的控制平面,以及 2 个干活的 node。不过,从安全性的角度看,这个 K8s 集群离用于生产环境还相差很远。

此外,我也并没有尝试运行更复杂的、需要涉及网络的应用。而且我相当确信在我让那些应用跑起来之前,还需要对当前的 K8s 集群做很多新的工作。不过,不管怎么样,就现在来说,可以来杯🍺奖励一下了。