Kubernetes 集群安全配置案例

Kubernetes 系统提供了三种认证方式:CA 认证、Token 认证 和 Base 认证。安全功能是一把双刃剑,它保护系统不被攻击,但是也带来额外的性能损耗。集群内的各组件访问 API Server 时,由于它们与 API Server 同时处于同一局域网内,所以建议用非安全的方式访问 API Server 效率更高。

接下来对集群的双向认证配置和简单认证配置过程进行详细说明。

双向认证配置

双向认证方式是最为严格和安全的集群安全配置方式,主要配置流程如下:

  1. 生成根证书、API Server 服务端证书、服务端私钥、各个组件所用的客户端证书和客户端私钥。
  2. 修改 Kubernetes 各个服务进程的启动参数,启用双向认证模式。

详细的配置操作流程如下:

生成根证书

用 openssl 工具生成 CA 证书,请注意将其中 subject 等参数改为用户所需的数据,CN 的值通常是域名、主机名或 IP 地址。

1
2
3
$ cd /var/run/kubernetes
$ openssl genrsa -out dd_ca.key 2048
$ openssl req -x509 -new -nodes -key dd_ca.key -subj "/CN=YOUDOMAIN.COM" -days 5000 -out dd_ca.crt

生成 API Server 服务端证书和私钥

1
2
3
4
$ openssl genrsa -out dd_server.key 2048
$ HN=`hostname`
$ openssl req -new -key dd_server.key -subj "/CN=$HN" -out dd_server.csr
$ openssl x509 -req -in dd_server.csr -CA dd_ca.crt -CAkey dd_ca.key -CAcreateserial-out dd_server.crt -days 5000

生成 Controller Manager 与 Scheduler 进程共用的证书和私钥

1
2
3
$ openssl genrsa -out dd_cs_client.key 2048
$ openssl req -new -key dd_cs_client.key -subj "/CN=$HN" -out dd_cs_client.csr
$ openssl x509 -req -in dd_cs_client.csr -CA dd_ca.crt -CAkey dd_ca.key -CAcreateserial -out dd_cs_client.crt -days 5000

生成 Kubelet 所用的客户端证书和私钥

注意,这里假设 Kubelet 所在机器的 IP 地址为 192.168.1.129。

1
2
3
$ openssl genrsa -out dd_kubelet_client.key 2048
$ openssl req -new -key dd_kubelet_client.key -subj "/CN=192.168.1.129" -out dd_kubelet_client.csr
$ openssl x509 -req -in dd_kubelet_client.csr -CA dd_ca.crt -CAkey dd_ca.key -CAcreateserial -out dd_kubelet_client.crt -days 5000

修改 API Server 的启动参数

增加 CA 根证书、Server 自身证书等参数并设置安全端口为 443.

修改/etc/kubernetes/apiserver 配置文件的 KUBE_API_ARGS 参数:

1
KUBE_API_ARGS="--log-dir=/var/log/kubernetes --secure-port=443 --client_ca_file=/var/run/kubernetes/dd_ca.crt --tls-private-key-file=/var/run/kubernetes/dd_server.key --tls-cert-file=/var/run/kubernetes/dd_server.crt"

重启 kube-apiserver 服务:

1
# systemctl restart kube-apiserver

验证 API Server 的 HTTPS 服务。

1
$ curl https://kubernetes-master:443/api/v1/nodes --cert /var/run/kubernetes/dd_cs_client.crt --key /var/run/kubernetes/dd_cs_client.key --cacert /var/run/kubernetes/dd_ca.crt

修改 Controller Manager 的启动参数

修改/etc/kubernetes/controller-manager 配置文件

1
KUBE_CONTROLLER_MANAGER_ARGS="--log-dir=/var/log/kubernetes --service_account_private_key_file=/var/run/kubernetes/server.key --root-ca-file=/var/run/kubernetes/ca.crt --master=https://kubernetes-master:443 --kubeconfig=/etc/kubernetes/cmkubeconfig"

创建/etc/kubernetes/cmkubeconfig 文件,配置证书等相关参数,具体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Config
users
- name: controllermanager
user:
client-certificate: /var/run/kubernetes/dd_cs_client.crt
client-key: /var/run/kubernetes/dd_cs_client.key
clusters:
- name: local
cluster:
certificate-authority: /var/run/kubernetes/dd_ca.crt
contexts:
- context:
cluster: local
user: controllermanager
name: my-context
current-context: my-context

重启 kube-controller-manager 服务:

1
# systemctl restart kube-controller-manager

配置各个节点上的 Kubelet 进程

复制 Kubelet 的证书、私钥 与 CA 根证书到所有 Node 上。

1
2
$ scp /var/run/kubernetes/dd_kubelet* root@kubernetes-minion1:/home
$ scp /var/run/kubernetes/dd_ca.* root@kubernetes-minion:/home

在每个 Node 上创建/var/lib/kubelet/kubeconfig 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Config
users:
- name: kubelet
user:
client-certificats: /home/dd_kubelet_client.crt
client-key: /home/dd_kubelet_client.key
clusters:
- name: local
cluster:
certificate-authority: /home/dd_ca.crt
contexts:
- context:
cluster: local
user: kubelet
name: my-context
current-context: my-context

修改 Kubelet 的启动参数,以修改/etc/kubernetes/kubelet 配置文件为例:

1
2
KUBELET_API_SERVER="--api_servers=https://kubernetes-master:443"
KUBELET_ARGS="--pod_infro_container_image=192.168.1.128:1180/google_containers/pause:latest --cluster_dns=10.2.0.100 --cluster_domain=cluster.local --kubeconfig=/var/lib/kubelet/kubeconfig"

重启 kubelet 服务:

1
# systemctl restart kubelet

配置 kube-proxy

首先,创建/var/lib/kubeproxy/proxykubeconfig 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Config
users:
- name: kubeproxy
user:
client-certificate: /home/dd_kubelet_client.crt
client-key: /home/dd_kubelet_client.key
clusters:
- name: local
cluster:
certificate-authority: /home/dd_ca.crt
contexts:
- context:
cluster: local
user: kubeproxy
name: my-context
current-context: my-context

然后,修改 kube-proxy 的启动参数,引用上述文件并指明 API Server 在安全模式下的访问地址,以修改配置文件/etc/kubenetes/proxy 为例:

1
KUBE_PROXY_ARGS="--kubeconfig=/var/lib/kubeproxy/proxykubeconfig --master=https://kubenetes-master:443"

重启 kube-proxy 服务:

1
# systemctl restart kube-proxy

至此,一个双向认证的 Kubernetes 集群环境就搭建完成了。

简单认证配置

除了双向认证方式,Kubernets 也提供了基于 Token 和 HTTP Base 的简单认证方式。通信方式仍然采用 HTTPS,但不使用数字证书。

采用基于 Token 和 HTTP Base 的简单认证方式时,API Server 对外暴露 HTTPS 端口,客户端提供 Token 或用户名、密码来完成认证过程。这里需要说明的一点是 Kubelet 比较特殊,它同时支持双向认证与简单认证两种模式,其他组件智能配置为双向认证或非安全模式。

API Server 基于 Token 认证的配置过程如下

建立包括用户名、密码和 UID 的文件 token_auth_file:

1
2
3
4
$ cat /root/token_auth_file
dingmingk,dingmingk,1
admin,admin,2
system,system,3

修改 API Server 的配置,采用上述文件进行安全认证

1
2
$ vi /etc/kubernetes/apiserver
KUBE_API_ARGS="--secure-port=443 --token_auth_file=/root/token_auth_file"

重启 API Server 服务

1
# systemctl restart kube-apiserver

用 curl 验证连接 API Server

1
2
3
4
5
6
7
8
$ curl https://kubenetes-master:443/version --header "Authorization: Bearer dingmingk" -k
{
"major": "1",
"minor": "0",
"gitVersion": "v1.0.0",
"gitCommit": "xxxHASHCODE",
"gitTreeState": "clean"
}

API Server 基于 HTTP Base 认证的配置过程如下

创建包括用户名、密码和 UID 的文件 basic_auth_file:

1
2
3
4
$ cat /root/basic_auth_file
dingmingk,dingmingk,1
admin,admin,2
system,system,3

修改 API Server 的配置,采用上述文件进行安全认证

1
2
$ vi /etc/kubernetes/apiserver
KUBE_API_ARGS="--secure-port=443 --basic_auth_file=/root/basic_auth_file"

重启 API Server 服务

1
# systemctl restart kube-apiserver

用 curl 验证连接 API Server

1
2
3
4
5
6
7
8
$ curl https://kubernetes-master:443/version --basic -u dingmingk:dingmingk -k
{
"major": "1",
"minor": "0",
"gitVersion": "v1.0.0",
"gitCommit": "xxxHASHCODE",
"gitTreeState": "clean"
}

使用 Kubelet 时则需要指定用户名和密码来访问 API Server

1
$ kubectl get nodes --server="https://kubernetes-master:443" --api-version="v1" --username="dingmingk" --password="dingmingk" --insecure-skip-tls-verify=true

Kubernetes 集群性能监控

在 Kubernetes 系统中,使用 cAdvisor 对 Node 所在主机资源和在该 Node 上运行的容器进行监控和性能数据采样。由于 cAdvisor 集成在 Kubelet 中,即运行在每个 Node 上,所以一个 cAdvisor 仅能对一台 Node 进行监控。在大规模容器集群中,我们需要对所有 Node 和全部容器进行性能监控,Kubernetes 使用一套工具来实现集群性能数据的采集、存储和展示:Heapster、InfluxDB 和 Grafana。

Heapster:是对集群中各 Node、Pod 的资源使用数据进行采集的系统,通过访问每个 Node 上 Kubelet 的 API,再通过 Kubelet 调用 cAdvisor 的 API 来采集该节点上所有容器的性能数据。之后 Heapster 进行数据聚合,并将结果保存到后端存储系统中。Heapster 支持多种后端存储系统,包括 memory、InfluxDB、BigQuery、谷歌云平台提供的 Google Cloud Monitoring 和 Google Cloud Logging等。

InfluxDB:是分布式时序数据库(每条记录都带有时间戳属性),主要用于实时数据采集、事件跟踪记录、存储时间图标、原始数据等。InfluxDB 提供 REST API 用于数据的存储和查询。

Grafana:通过 Dashboard 将 InfluxDB 中的时序数据展现成图标或曲线等形式,便于运维人员查看集群的运行状态。

配置 Kubernetes 集群的 ServiceAccount 和 Secret

Heapster 当前版本需要使用 HTTPS 的安全方式与 Kubernetes Master 进行连接,所以需要先进行 ServiceAccount 和 Secret 的创建。如果不使用 Secret,则 Heapster 启动时将会报错,然后 Heapster 容器会被 ReplicationController 反复销毁、创建,无法正常工作。

关于 ServiceAccount 和 Secret 的原理详见http://blog.dingmingk.com/blog/kube_security.html

在进行一下操作时,我们假设在 Kubernetes 集群中没有创建过 Secret(如果之前创建过,则可以先删除 etcd 中与 Secret 相关的键值)。

首先,使用 OpenSSL 工具在 Master 服务器上创建一些证书和私钥相关的文件:

1
2
3
4
5
# openssl genrsa -out ca.key 2048
# openssl req -x509 -new -nodes -key ca.key -subj "/CN=yourcompany.com" -days 5000 -out ca.crt
# openssl genrsa -out server.key 2048
# openssl req -new -key server.key -subj "/CN=kubernetes-master" -out server.csr
# openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 5000

注意,在生成 server.csr 时 -subj 参数中 /CN 指定的名字需为 Master 的主机名。另外,在生成 ca.crt 时 -subj 参数中 /CN 的名字最好与主机名不同,设为相同可能导致对普通 Master 的 HTTPS 访问认证失败。

执行完成后会生成 6 个文件:ca.crt、ca.key、ca.srl、server.crt、server.csr、server.key。将这些文件复制到 /var/run/kubernetes/ 目录中,然后设置kube-apiserver 的启动参数:

1
2
3
--client_ca_file=/var/run/kubernetes/ca.crt
--tls-private-key-file=/var/run/kubernetes/server.key
--tls-cert-file=/var/run/kubernetes/server.crt

之后重启 kube-apiserver 服务。

接下来,给 kube-controller-manager 服务添加以下启动参数:

1
2
--service_account_private_key_file=/var/run/kubernetes/server.key
--root-ca-file=/var/run/kubernetes/ca.crt

然后重启 kube-controller-manager 服务。

在 kube-apiserver 服务成功启动后,系统会自动为每个命名空间创建一个 ServiceAccount 和一个 Secret(包含一个 ca.crt 和一个 token):

1
2
3
$ kubectl get serviceaccounts --all-namespaces
$ kubectl get secrets --all-namespaces
$ kubectl describe secret xxx

之后 ReplicationController 在创建 Pod 时,会生成类型为 Secret 的 Volume 存储卷,并将该 Volume 挂载到 Pod 内的如下目录中:/var/run/secrets/kubernetes.io/serviceaccount。然后,容器内的应用程序就可以使用该 Secret 与 Master 建立 HTTPS 连接了。Pod 的 Volumes 设置和挂载操作由 ReplicationController 和 Kubelet 自动完成,可以通过查看 Pod 的详细信息了解到。

1
$ kubectl get pods kube-dns-v8-xxxxx --namespace=kube-system -o yaml

进入容器,查看/var/run/secrets/kubernetes.io/serviceaccount 目录,可以看到两个文件 ca.crt 和 token,这两个文件就是与 Master 通信时所需的证书和秘钥信息。

部署 Heapster、InfluxDB、Grafana

在 ServiceAccount 和 Secrets 创建完成后,我们就可以创建 Heapster、InfluxDB 和 Grafana 等 ReplicationController 和 Service 了。

heapsster-service.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
labels:
kubernetes.io/cluster-service: "true"
kubernetes.io/name: Heapster
name: heapster
namespace: kube-system
spec:
ports:
- port: 80
targetPort: 8082
selector:
k8s-app: heapster

InfluxDB-service.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: Service
metadata:
labels: null
name: monitoring-InfluxDB
namespace: kube-system
spec:
type: NodePort
ports:
- name: http
port: 8083
targetPort: 8083
nodePort: 30083
- name: api
port: 8086
targetPort: 8086
nodePort: 30086
selector:
name: influxGrafana

Grafana-service.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Service
metadata:
labels:
kubernetes.io/name: monitoring-Grafana
kubernetes.io/cluster-service: "true"
name: monitoring-Grafana
namespace: kube-system
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: 30080
selector:
name: influxGrafana

heapster-controller.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: v1
kind: ReplicationController
metadata:
labels:
k8s-app: heapster
name: heapster
version: v6
name: heapster
namespace: kube-system
spec:
replicas: 1
selector:
name: heapster
k8s-app: heapster
version: v6
template:
metadata:
labels:
k8s-app: heapster
version: v6
spec:
containers:
- name: heapster
image: gcr.io/google_containers/heapster:v0.17.0
command:
- /heapster
- --source=kubernetes:http://192.168.1.128:8080?inClusterConfig=false&kubeletHttps=true&useServiceAccount=true&auth=
- --sink=InfluxDB:http://monitoring-InfluxDB:8086

Heapster 需要设置的参数如下:

  • –source:为配置监控来源。在本例中使用 kubernetes:表示从 Kubernetes Master 获取各 Node 的信息。在 URL 后面的参数部分,修改 kubeletHttps、inClusterConfig、useServiceAccount 的值,并设置 auth 的值为空。URL中可配置的参数如下:
    • IP 地址和端口号:为 Kubernetes Master 的地址。
    • kubeletPort:默认为 10255(Kubelet 服务的只读端口号)。
    • kubeletHttps:是否通过 HTTPS 方式连接 Kubelet,默认为 false。
    • apiVersion: API 版本号,默认为 Kubernetes 系统的版本号,当前为 v1.
    • inClusterConfig:是否使用 Heapster 命名空间中的 ServiceAccount,默认为 true。
    • insecure:是否信任 Kubernetes 证书,默认为 false。
    • auth:客户端认证授权文件,当 ServiceAccount 不可用时对其进行设置。
    • useServiceAccount:是否使用 ServiceAccount,默认为 false。
  • –sink:为配置后端的存储系统,在本例中使用 InfluxDB 系统。

InfluxDB-Grafana-controller.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apiVersion: v1
kind: ReplicationController
metadata:
labels:
name: influxGrafana
name: infludb-Grafana
namespace: kube-system
spec:
replicas: 1
selector:
name: influxGrafana
template:
metadata:
labels:
name: influxGrafana
spec:
containers:
- name: InfluxDB
image: gcr.io/google_containers/heapster_InfluxDB:v0.3
ports:
- containerPort: 8083
hostPort: 8083
- containerPort: 8086
hostPort: 8086
- name: Grafana
image: gcr.io/google_containers/heapster_Grafana:v0.7
ports:
- containerPort: 8080
hostPort: 8080
env:
- name: INFLUXDB_HOST
value: monitoring-InfluxDB

最后,创建所有 Service 和 RC:

1
2
3
4
5
$ kubectl create -f heapster-service.yaml
$ kubectl create -f InfluxDB-server.yaml
$ kubectl create -f Grafana-service.yaml
$ kubectl create -f InfluxDB-Grafana-controller.yaml
$ kubectl create -f heapster-controller.yaml

通过 kubectl get pods –namespace=kube-system 确认各 Pod 都成功启动了。

Kubernetes 安全机制

Kubernetes 通过一系列机制来实现集群的安全控制,其中包括 API Server 的认证授权、准入控制机制及保护敏感信息的 Secret 机制等。集群的安全性必须考虑如下几个目标:

  1. 保证容器与其所在的宿主机的隔离;
  2. 限制容器给基础设施及其他容器带来消极影响的能力;
  3. 最小权限原则————合理限制所有组件的权限,确保组件只执行它被授权的行为,通过限制单个组件的能力来限制它所能到达的权限范围;
  4. 明确组件间边界的划分;
  5. 划分普通用户和管理员的角色;
  6. 在必要的时候允许将管理员权限赋给普通用户;
  7. 允许拥有“Secret”数据(Keys、Certs、Passwords)的应用在集群中运行。

下面分别从 Authentication、Authorization、Admission Control、Secret 和 Service Account 六个方面来说明集群的安全机制。

Authentication 认证

Kubernetes 对 API 调用使用 CA(Client Authentication)、Token 和 HTTP Base 方式实现用户认证。

使用 CA 认证的应用需包含一个 CA 认证机构给服务器端下发的根证书、服务端证书和私钥文件。因此 API Server 的三个参数“–client-ca-file”“–tls-cert-file”和“–tls-private-key-file”分别指向根证书文件、服务端证书文件和私钥文件。API Server 客户端应用的三个启动参数(例如 Kubectl 的三个参数 “certificate-authority”“client-certificate”和“client-key”),或客户端应用的 kubeconfig 配置文件中的配置项“certificate-authority”“client-certificate”和“client-key”分别指向根证书文件、客户端证书文件和私钥文件。

Kubernetes 的 CA 认证方式通过添加 API Server 的启动参数“–client-ca-file=SOMEFILE”实现,其中“SOMEFILE”为认证授权文件,该文件包含一个或多个证书颁发机构(CA Certificates Authorities)。

Token 认证方式通过添加 API Server 的启动参数“–token_auth_file=SOMEFILE”实现,其中“SOMEFILE”指的是 Token 文件。

1
2
3
4
5
6
7
8
9
例如 Token 文件内容为:
lkjqweroiuuou,Thomas,8x7d1kklzseertyywx
用 CURL 去访问该 API Server:
curl $APISERVER/api --header "Authorization: Bearer lkjqweroiuuou" --insecure
{
"version" : [
"v1"
]
}

基本认证方式是通过添加 API Server 的启动参数“–basic_auth_file=SOMEFILE”实现的,其中“SOMEFILE”指的是用于存储用户和密码信息的基本认证文件。

当使用基本认证方式从 HTTP 客户端访问 API Server 时,HTTP 请求头中的 Authorization 域必须包含“Basic BASE64ENCODEDUSER:PASSWORD”的值。

1
2
3
4
5
6
7
tmp=`base64 "dingmingk:passwd"`
curl $APISERVER/api --header "Authorization: Basic $tmp" --insecure
{
"version":[
"v1"
]
}

Authorization 授权

在 Kubernetes 中,授权(Authorization)是认证(Authenticaiton)后的一个独立步骤,作用于 API Server 主要端口的所有 HTTP 访问。授权流程不作用于只读端口,在计划中只读端口在不久之后将被删除。授权流程通过访问策略比较请求上下文的属性(例如用户名、资源和 Namespace)。在通过 API 访问资源之前,必须通过访问策略进行校验。访问策略通过 API Server 的启动参数 –authorization_mode 配置,该参数包含如下三个值:

  • –authorization_mode=AlwaysDeny
  • –authorization_mode=AlwaysAllow
  • –authorization_mode=ABAC

其中,“AlwaysDeny”表示拒绝所有的请求,该配置一般用于测试;“AlwaysAllow”表示接受所有请求,如果集群不需要授权流程,则可以采用该策略;“ABAC”表示使用用户配置的授权策略去管理访问 API Server 的请求,ABAC(Attribute-Based Access Control)为基于属性的访问控制。

在 Kubernetes 中,一个 HTTP 请求包含如下 4 个能被授权进程识别的属性:

  • 用户名(代表一个已经被认证的用户的字符型用户名);
  • 是否是只读请求(REST 的 GET 操作是只读的);
  • 被访问的是哪一类资源,例如访问 Pod 资源/api/v1/namespaces/defaults/pods;
  • 被访问对象所属的 Namespace,如果这被访问的资源不支持 Namespace,则是空字符串。

如果选用 ABAC 模式,那么需要通过设置 API Server 的“–authorization_policy_file=SOME_FILENAME”参数来指定授权策略文件,其中“SOME_FILENAME”为授权策略文件。授权策略文件的每一行都是一个 JSON 对象,该 JSON 对象是一个 Map,这个 Map 内不包含 List 和 Map。每行都是一个“策略对象”。策略对象包含下面 4 个属性:

  • user(用户名),为字符串类型,该字符串类型的用户名来源于 Token 文件或基本认证文件中的用户名字段的值;
  • readonly(只读标识),为布尔类型,当它的值为 true 时,表明该策略允许 GET 请求通过;
  • resource(资源),为字符串类型,来自于 URL 的资源,例如“Pods”;
  • namespace(命名空间),为字符串类型,表明该策略允许访问某个 Namespace 的资源。

没被设置的属性,将被等同于根据值的类型设置成零值(例如为字符串类型属性设置一个空字符串;为布尔值属性设置 false;为数值类型属性设置 0)。

授权策略文件中的策略对象的一个未设置属性,表示匹配 HTTP 请求中该属性的任何值。对请求的 4 个属性值和授权策略文件中的所有策略对象逐个匹配,如果至少有一个策略对象被匹配上,则该请求将被授权通过。

例如:

1
2
3
4
允许用户 alice 做任何事情:{"user":"alice"}。
用户 Kubelet 指定读取资源 Pods:{"user":"kubelet","resource":"pods","readonly":true}。
用户 Kubelet 能读和写资源 events:{"user":"kubelet","resource":"events"}
用户 bob 只能读取 Namespace "myNamespace" 中的资源 Pods:{"user":"bob","resource":"pods","readonly":true,"ns":"myNamespace"}

Admission Control 准入控制

Admission Control 是用于拦截所有经过认证和鉴权后的访问 API Server 请求的可插入代码(或插件)。这些可插入代码运行于 API Server 进程中,在被调用前必须被编译成二进制文件。在请求被 API Server 接收前,每个 Admission Control 插件按配置顺序执行。如果其中的任意一个插件拒绝该请求,就意味着这个请求被 API Server 拒绝,同时 API Server 反馈一个错误信息给请求发起方。

在某些情况下,Admission Control 插件会使用系统配置的默认值取改变进入集群对象的内容。此外,Admission Control 插件可能会改变请求处理所使用的配额,比如增加请求处理的资源配额。

通过配置 API Server 的启动参数“admission_control”,在该参数中加入需要的 Admission Control 插件列表,各插件的名称之间用逗号隔开。例如:

1
--admission_control=NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota

Admission Control 的插件列表如下表所示:

名称 说明
AlwaysAdmit 允许所有请求通过
AlwaysDeny 拒绝所有请求,一般用于测试
DenyExecOnPrivileged 拦截所有带有 SecurityContext 属性的 Pod 的请求,拒绝在一个特权容器中执行命令
ServiceAccount 配合 Service Account Controller 使用,为设定了 Service Account 的 Pod 自动管理 Secret,使得 Pod 能够使用相应的 Secret 下载 Image 和访问 API Server
SecurityContextDeny 不允许带有 SecurityContext 属性的 Pod 存在,SecurityContext 属性用于创建特权容器
ResourceQuota 在 Namespace 中做资源配额限制
LimitRanger 限制 Namespace 中的 Pod 和 Container 的 CPU 和 内存配额
NamespaceExists 读区请求中的 Namespace 属性,如果该 Namespace 不存在,则拒绝该请求
NamespaceAutoProvision(deprecated) 读取请求中的 Namespace 属性,如果该 Namespace 不存在,则尝试创建该 Namespace
NamespaceLifecycle 该插件限制访问处于中止状态的 Namespace,禁止在该 Namespace 中创建新的内容。当 NamespaceLifecycle 和 NamespaceExists 能够合并成一个插件后,NamespaceAutoProvision 就会变成 deprecated

Secret 私密凭据

Secret 的主要作用是保管私密数据,比如密码、OAuth Tokens、SSH Keys 等信息。将这些私密信息放在 Secret 对象中比直接放在 Pod 或 Docker Image 中更安全,也更便于使用。

Kubernetes 在 Pod 创建时,如果该 Pod 指定了 Service Account,那么为该 Pod 自动添加包含凭证信息的 Secrets,用于访问 API Server 和下载 Image。该功能可以通过 Admission Control 添加或失效,然而如果需要以安全的方式去访问 API Server,则建议开启该功能。

下面的例子用于创建一个 Secret:

1
2
3
4
5
6
7
8
9
10
11
$ kubectl namespace myspace
$ cat <<EOF > secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
data:
password: dmFsdWUtMgOK
username: dmFsdWUtMQOK
$ kubectl create -f secrets.yaml

在上面的例子中,data域的各子域的值必须为 base64 编码值,其中 password 域和 username 域 base64 编码前的值分别为“value-1” 和“value-2”。

一旦 Secret 被创建,则可以通过下面的三种方式使用它:

  1. 在创建 Pod 时,通过为 Pod 指定 Service Account 来自动使用该 Secret;
  2. 通过挂载该 Secret 到 Pod 来使用它;
  3. 在创建 Pod 时,指定 Pod 的 spc.ImagePullSecrets 来引用它。

将一个 Secret 通过挂载的方式添加到 Pod 的 Volume 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Pod
metadata:
name: myPod
namespace: myns
spec:
containers:
name: mycontainer
image: redis
volumeMounts:
- name: foo
mountPath: /etc/foo
readOnly: true
volumes:
- name: foo
secret:
secretNmae: mysecret

手动使用 imagePullSecret:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ docker login localhost:1180
用 base64 编码 dockercfg 的内容
$ cat ~/.dockercfg | base64
将上一步命令的输出结果作为 Secret 的“data.dockercfg” 域的内容,由此来创建一个 Secret
$ cat > image-pull-secret.yaml <<EOF
apiVersion: v1
kind: Secret
metadata:
name: myregistrykey
data:
.dockercfg: xxxxxxxxxxxxxxxxxxxxx
type: kubernetes.io/dockercfg
EOF
$ kubectl create -f image-pull-secret.yaml
在创建 Pod 时,引用该 Secret
$ cat > pods.yaml << EOF
apiVersion: v1
kind: Pod
metadata:
name: mypod2
spec:
containers:
- name: foo
image: janedoe/awesomeapp:v1
imagePullSecrets:
- name: myregistrykey
EOF
$ kubectl create -f pods.yaml

Pod 创建时会验证所挂载的 Secret 是否真的指向一个 Secret 对象,因此 Secret 必须在任何引用它的 Pod 之前被创建。Secret 对象属于 Namespace,它们只能被同一个 NameSpace 中的 Pod 所引用。

Secret 包含三种类型:Opaque、ServiceAccount 和 Dockercfg。前面举例介绍了如何创建 Opaque 和 Dockercfg 类型的 Secret。下面的例子为创建一个 Service Account Secret:

1
2
3
4
5
6
7
apiVersion: v1
kind: Secret
metadata:
name: mysecret
annotations:
kubernetes.io/service-account.name: myserviceaccount
type: kubernetes.io/service-account-token

Service Account

Service Account 是多个 Secret 的集合。它包含两类 Secret:一类为普通 Secret,用于访问 API Server,也被称为 Service Account Secret;另一类为 imagePullSecret,用于下载容器镜像。如果镜像库运行在 Insecure 模式下,则该 Service Account 可以不包含 imagePullSecret。在下面的例子中创建了一个名为 build-robot 的 Service Account,并查询该 Service Account 的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ cat > serviceaccount.yaml << EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: myserviceaccount
secrets:
- name: mysecret
kind: Secret
apiVersion: v1
- name: mysecret1
kind: Secret
apiVersion: v1
imagePullSecrets:
- name: mysecret2
EOF
$ kubectl create -f serviceaccount.yaml
$ kubectl get serviceaccounts build-robot -o yaml

在 Pod 的创建过程中指定“spec.serviceAccountName”的值为相应的 Service Account 的名称:

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: mycontainer
image: nginx
serviceAccountName: myserviceaccount

Kubernetes DNS 服务配置案例

在 Kubernetes 系统中,Pod 在访问其他 Pod 的 Service 时,可以通过两种服务发现方式完成,即环境变量和 DNS 方式。但是使用环境变量是有限制条件的,即 Service 必须在 Pod 之前被创建出来,然后系统才能在新建的 Pod 中自动设置与 Service 相关的环境变量。DNS 则没有这个限制,其通过全局的 DNS 服务器来完成服务的注册与发现。

Kubernetes 提供的 DNS 由以下三个组件组成:

  1. etcd: DNS 存储
  2. kube2sky: 将 Kubernetes Master 中的 Service(服务)注册到 etcd。
  3. skyDNS: 提供 DNS 域名解析服务。

这三个组件以 Pod 的方式启动和运行,所以在一个 Kubernetes 集群中,它们都可能被调度到任意一个 Node 节点上去。为了能够使它们之间网络互通,需要将各 Pod 之间的网络打通。

skydns 配置文件

首先创建 DNS 服务的 ReplicationController 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
apiVersion: v1
kind: ReplicationController
metadata:
name: kube-dns-v8
namespace: kube-system
labels:
k8s-app: kube-dns
version: v8
kubernetes.io/cluster-service: " true "
spec:
replicas: 1
selector:
k8s-app: kube-dns
version: v8
template:
metadata:
labels:
k8s-app: kube-dns
version: v8
kubernetes.io/cluster-service: " true "
spec:
containers:
- name: etcd
image: gcr.io/google_containers/etcd:2.0.9
resources:
limits:
cpu: 100m
memory: 50Mi
command:
- /usr/local/bin/etcd
- -data-dir
- /var/etcd/data
- -listen-client-urls
- http://127.0.0.1:2379,http://127.0.0.1:4001
- -advertise-client-urls
- http://127.0.0.1:2379,http://127.0.0.1:4001
- -initial-cluster-token
- skydns-etcd
volumeMounts:
- name: etcd-storage
mountPath: /var/etcd/data
- name: kube2sky
image: gcr.io/google_containers/kube2sky:1.11
resources:
limits:
cpu: 100m
memory: 50Mi
args:
# command = " /kube2sky "
- --kube_master_url=http://192.168.1.128:8080
- -domain=cluster.local
- name: skydns
image: gcr.io/google_containers/skydns:2015-03-11-001
resources:
limits:
cpu: 100m
memory: 50Mi
args:
# command = " /skydns "
- -machines=http://localhost:4001
- -addr=0.0.0.0:53
- -domain=cluster.local
ports:
- containerPort: 53
name: dns
protocol: UDP
- containerPort: 53
name: dns-tcp
protocol: TCP
volumes:
- name: etcd-storage
emptyDir: {}
dnsPolicy: Default

需要修改的几个配置参数如下:

  • kube2sky 容器需要访问 Kubernetes Master,需要配置 Master 所在物理主机的 IP 地址和端口号,本例中设置参数 –kube_master_url 的值为 http://192.168.1.128:8080。
  • kube2sky 容器和 skydns 容器的启动参数 -domain,设置 Kubernetes 集群中 Service 所属的域名,本例中为 cluster.local。启动后,kube2sky 会监听 Kubernetes,当有新的 Service 创建时,就会生成相应的记录并保存到 etcd 中。kube2sky 为每个 Service 生成两条记录:
    • ..;
    • ..svc.
  • skydns 的启动参数 -addr=0.0.0.0:53 表示使用本机 TCP 和 UDP 的 53 端口提供服务。

创建 DNS 服务的 Service 配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: Service
metadata:
name: kube-dns
namespace: kube-system
labels:
k8s-app: kube-system
labels:
k8s-app: kube-dns
kubernetes.io/cluster-services: " true "
kubernetes.io/name: " KubeDNS "
spec:
selector:
k8s-app: kube-dns
clusterIP: 20.1.0.100
ports:
- name: dns
port: 53
protocol: UDP
- name: dns-tcp
port: 53
protocol: TCP

注意,skydns 服务使用的 clusterIP 需要我们指定一个固定的 IP 地址,每个 Node 的 Kubernetes 进程都将使用这个 IP 地址,不能通过 Kubernetes 自动分配。

另外,这个 IP 地址需要在 kube-apiserver 启动参数 –service-cluster-ip-range 指定的 IP 地址范围内。

修改每个 Node 上的 Kubelet 启动参数

修改每台 Node 上 Kubelet 的启动参数:

  • –cluster_dns=20.1.0.100,为 DNS 服务的 ClusterIP 地址;
  • –cluster_domain=cluster.local,为 DNS 服务中设置的域名。

然后重启 Kubelet 服务。

创建 skydns Pod 和服务

通过 kubectl create 完成 RC 和 Service 的创建:

1
2
# kubectl create -f skydns-rc.yaml
# kubectl create -f skydns-svc.yaml

创建完成后,可以查看系统创建的 RC、Pod、Service 是否创建成功:

1
# kubectl get rc|pods|services --namespace=kube-system

DNS 服务的工作原理

让我们看看 DNS 服务背后的工作原理。

kube2sky 容器应用通过调用 Kubernetes Master 的 API 获得集群中所有 Service 的信息,并持续监控新 Service 的生成,然后写入 etcd 中。

1
2
查看 etcd 中存储的 Service 信息
# kubectl exec kube-dns-v8-xxxxx -c etcd --namespace=kube-system etcdctl ls /skydns/local/cluster

根据 Kubelet 启动参数的设置(–cluster_dns),Kubelet 会在每个新创建的 Pod 中设置 DNS 域名解析配置文件 /etc/resolv.conf 文件,在其中增加了一条 nameserver 配置和一条 search 配置。

最后,应用程序就能够像访问网站域名一样,仅仅通过服务的名字就能访问到服务了。

通过 DNS 设置,对于其他 Service 的查询将可以不再依赖系统为每个 Pod 创建的环境变量,而是直接使用 Service 的名字就能对其进行访问,使得应用程序中的代码更加简洁。

Kubernetes 网络配置方案

为了实现各 Node 上 Pod 之间的互通,需要一些方案来打通网络,这是 Kubernetes 能够正常工作的前提。本篇将对常用的直接路由、Flannel 和 Open vSwitch 三种配置进行详细说明。

直接路由

通过在每个 Node 上添加到其他 Node 上 docker0 的静态路由规则,就可以将不同物理服务器上 Docker Daemon 创建的 docker0 网桥互联互通。

例如 Pod1 所在 docker0 网桥的 IP 子网是 10.1.10.0,Node 地址为 192.168.1.128;而 Pod2 所在 docker0 网桥的 IP 子网是 10.1.20.0,Node 地址为 192.168.1.129.

在 Node1 上增加一条到 Node2 上 docker0 的静态路由规则:

1
route add -net 10.1.20.0 netmask 255.255.255.0 gw 192.168.1.129

同样,在 Node2 上增加一条到 Node1 上 docker0 的静态路由规则:

1
route add -net 10.1.10.0 netmask 255.255.255.0 gw 192.168.1.128

我们的集群中机器数量通常很多。假设有 100 台服务器,那么就需要在每台服务器上手工添加到另外 99 台服务器 docker0 的路由规则。为了减少手工操作,可以使用 Quagga 软件来实现路由规则的动态添加。

除了在每台服务器上安装 Quagga 软件并启动,还可以使用互联网上的一个 Quagga 容器来运行,在这里使用 index.alauda.cn/georce/router 镜像启动 Quagga。在每台 Node 上下载该 Docker 镜像:

1
$ docker pull index.alauda.cn/georce/router

在运行 Quagga 路由器之前,需要确保每个 Node 上 docker0 网桥的子网地址不能重叠,也不能与物理机所在的网络重叠,这需要网络管理员仔细规划。

下面以 3 个 Node 为例,使用 ifconfig 命令修改 docker0 网桥的地址和子网(假设 Node 所在的物理网络不是 10.1.X.X地址段):

1
2
3
Node 1: # ifconfig docker0 10.1.10.1/24
Node 2: # ifconfig docker0 10.1.20.1/24
Node 3: # ifconfig docker0 10.1.30.1/24

然后在每个 Node 上启动 Quagga 容器。需要说明的是,Quagga 需要以 –privileged 特权模式运行,并且指定 –net=host,表示直接使用物理机的网络:

1
$ docker run -itd --name=router --privileged --net=host index.alauda.cn/georce/router

启动成功后,Quagga 会相互学习来完成到其他机器的 docker0 路由的添加。

使用 flannel 叠加网络

falnnel 采用叠加网络(Overlay Network)模型来完成网络的打通。

安装 etcd

由于 flannel 使用 etcd 作为数据库,所以需要预先安装好 etcd。

安装 flannel

需要在每台 Node 上都安装 flannel。flannel 软件的下载地址为 (https://github.com/coreos/flannel/releases)。将下载的压缩包解压,把二进制文件 flanneld 和 mk-docker-opts.sh 复制到 /usr/bin(或其他 PATH 环境变量中的目录),即可完成对 flannel 的安装。

配置 flannel

此处以使用 systemd 系统为例对 flanneld 服务进行配置。编辑服务配置文件 /etc/systemd/system/flanneld.service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Unit]
Description=Flanneld overlay address etcd agent
After=network.target
Before=docker.service
[Service]
Type=notify
EnvironmentFile=/etc/sysconfig/flanneld
EnvironmentFile=-/etc/sysconfig/docker-network
ExecStart=/usr/bin/flanneld -etcd-endpoints=${FLANNEL_ETCD} $FLANNEL_OPTIONS
[Install]
RequiredBy=docker.service
WantedBy=multi-user.target

编辑配置文件/etc/sysconfig/flannel,设置 etcd 的 URL 地址:

1
2
3
4
5
6
7
8
9
10
# Flanneld configuration options
# etcd url location. Point this to the server where etcd runs
FLANNEL_ETCD="http://192.168.1.128:2379"
# etcd config key. THis is the configuration key that flannel queries
# For address range assignment
FLANNEL_ETCD_KEY="/coreos.com/network"
# Any additional options that you want to pass
#FLANNEL_OPTIONS=" "

在启动 flannel 之前,需要在 etcd 中添加一条网络配置记录,这个配置将用于 flannel 分配给每个 Docker 的虚拟 IP 地址段:

1
# etcdctl set /coreos.com/network/config '{ "Network" : " 10.1.0.0/16 " }'

由于 flannel 将覆盖 docker0 网桥,所以如果 Docker 服务网已启动,则停止 Docker 服务。

启动 flanneld 服务

1
# systemctl restart flanneld

在每个 Node 节点执行以下命令来完成对 docker0 网桥的配置:

1
2
3
# mk-docker-opts.sh -i
# source /run/flannel/subnet.env
# ifconfig docker0 ${FLANNEL_SUBNET}

重新启动 Docker 服务

使用 Open vSwitch

以两个 Node 为例,首先确保节点 192.168.18.128 的 Docker0 采用 172.17.43.0/24 网段,而 192.168.18.131 的 Docker0 采用 172.17.42.0/24 网段,对应参数为 DockerDaemon 进程里的 bip 参数。

安装 ovs

1
# yum install openvswitch-x.x.x-x.x86_64.rpm

禁止 SELINUX 功能,配置后重启机器

1
2
# vi /etc/selinux/config
SELINUX=disabled

查看 Open vSwitch 的服务状态,应该启动两个进程:ovsdb-server 与 ovs-vswitchd。

创建网桥和 GRE 隧道

接下来需要在每个 Node 上建立 ovs 的网桥 br0,然后在网桥上创建一个 GRE 隧道连接对端网桥,最后把 ovs 的网桥 br0 作为一个端口连接到 docker0 这个 Linux 网桥上。这样以来,两个节点上的 docker0 网段就能互通了。

创建网桥

1
# ovs-vsctl add-br br0

创建 GRE 隧道连接对端,remote_ip 为对端 eth0 的网卡地址

1
# ovs-vsctl add-port br0 grel -- set interface grel type=gre option:remote_ip=192.168.18.128

添加 br0 到本地 docker0,使得容器流量通过 OVS 流经 tunnel

1
# brctl addif docker0 br0

启动 br0 与 docker0 网桥

1
2
# ip link set dev br0 up
# ip link set dev docker0 up

添加路由规则。由于 192.168.18.128 与 192.168.18.131 的 docker0 网段分别为 172.17.43.0/24 与 172.17.42.0/24,这两个网段的路由都需要经过本机的 docker0 网桥路由,其中一个 24 网段是通过 OVS 的 GRE 隧道到达对端的,因此需要在每个 Node 上添加通过 docker0 网桥转发的 172.17.0.0/16 段的路由规则:

1
# ip route add 172.17.0.0/16 dev docker0

清空 Docker 自带的 iptables 规则及 Linux 的规则 ,后者存在拒绝 icmp 报文通过防火墙的规则:

1
# iptables -t nat -F; iptables -F

在 192.168.18.131 上完成上述步骤后,在 192.168.18.128 节点执行同样的操作。注意,GRE 隧道里的 IP 地址药改为对端节点(192.168.18.131)的 IP 地址。

Node 上容器之间的互通测试

1
# tshark -i br0 -R ip proto great

Kubernetes 资源配额管理

资源的配额管理功能主要用于对集群中可用的资源进行分配和限制。为了开启配额管理,需要设置 kube-apiserver 的 –admission_control 参数,使之家在这两个准入控制器。

1
kube-apiserver ... --admission_control=LimitRanger,ResourceQuota...

指定容器配额

对指定容器实施配额管理非常简单,只要在 Pod 或 ReplicationController 的定义文件中设定 resources 属性即可为某个容器指定配额。目前容器支持 CPU 和 Memory 两类资源的配额限制。

在下面这个 RC 定义文件中增加了 redis-master 的资源配额声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: v1
kind: ReplicationController
metadata:
name: redis-master
labels:
name: redis-master
spec:
replicas: 1
selector:
name: redis-master
template:
metadata:
labels:
name: redis-master
spec:
containers:
- name: master
image: kubenetes/redis-master
ports:
- containerPort: 6379
resources:
limits:
cpu: 0.5
memory: 128Mi

以上配置表示,系统将对名为 master 的容器限制 CPU 为 0.5(也可以写为500m),可用内存限制为 128MiB字节。

1 KB(kilobyte) = 1000 bytes = 8000 bits

1 KiB(kibibyte) = 2^10 bytes = 1024 bytes = 8192 bits

全局默认配额

除了可以直接在容器(或 RC)的定义文件中给指定的容器增加资源配额参数,我们还可以通过创建 LimitRange 对象来定义一个全局默认配额模版。这个默认配额模版会加载到集群中的每个 Pod 及容器上,这样就不用为每个 Pod 和容器重复设置了。

我们定义一个名为 limit-range-1 的 LimitRange:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
kind: LimitRange
metadata:
name: limit-range-1
spec:
limits:
- type: " Pod "
max:
cpu: " 2 "
memory: 1Gi
min:
cpu: 250m
memory: 32Mi
- type: " Container "
max:
cpu: " 2 "
memory: 1Gi
min:
cpu: 250m
memory: 32Mi
default:
cpu: 250m
memory: 64Mi

上述设置表明:

  • 任意 Pod 内所有容器的 CPU 使用限制在 0.25~2;
  • 任意 Pod 内所有容器的内存使用限制在 32Mi~1Gi;
  • 任意容器的 CPU 使用限制在 0.25~2,默认值为 0.25;
  • 任意容器的内存使用限制在 32Mi~1Gi,默认值为 64Mi。

多租户配额管理

多租户在 Kubernetes 中以 Namespace 来体现,这里的多租户可以是多个租户、多个业务系统或者相互隔离的多种作业环境。一个集群中的资源总是有限的,当这个集群被多个租户的应用同时使用时,为了更好地使用这种有限的共有资源,我们需要将资源配额的管理单元提升到租户级别,只需要在不同租户对应的 Namespace 上加载对应的 ResourceQuota 配置即可达到目的。

假设我们集群拥有的总资源为:CPU 共有 128core;内存总量为 1024GiB;有两个租户,分别是开发组和测试组,开发组的资源配额为 32core CPU 及 256GiB 内存,测试组的资源配额为 96core CPU 及 768GiB 内存。

首先,创建开发组对应的命名空间:

1
2
3
4
apiVersion: v1
kind: Namespace
metadata:
name: development

接着,创建用于限定开发组的 ResourceQuota 对象,注意 metadata.namespace 属性被设定为开发组的命名空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: ResourceQuota
metadata:
name: quota-development
namespace: development
spec:
hard:
cpu: " 32 "
memory: 256Gi
persistentvolumeclaims: " 10 "
pods: " 100 "
replicationcontrollers: " 50 "
resourcequotas: " 1 "
secrets: " 20 "
services: " 50 "

测试组相应的 namespace 与 ResourceQuota 同上,略。

Kubernetes 常用运维技巧

这篇文章对日常工作中常用的 Kubernetes 系统运维操作和技巧进行总结和详细说明。(不定期更新!)

Node 的隔离和恢复

在硬件升级、维护等情况下,我们需要将某些 Node 进行隔离,脱离 Kubernetes 集群的调度范围。 Kubernetes 提供了一种机制,既可以将 Node 纳入调度范围,也可以将 Node 脱离调度范围。

创建配置文件 unschedule_node.yaml,在 spec 部分指定 unschedulable 为 true:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Node
metadata:
name: kubernetes-minion1
labels:
kubernetes.io/hostname: kubernetes-minion1
spec:
unschedulable: true

然后,通过kubectl replace命令完成对 Node 状态的修改:

1
$ kubectl replace -f unschedule_node.yaml

查看 Node 状态,可以观察到在 Node 的状态中增加了一项 SchedulingDisabled。

对于后续创建的 Pod,系统将不会再向该 Node 进行调度。

另一种方法时不使用配置文件,直接使用kubectl patch命令完成:

1
$ kubectl patch node kubernetes-minion1 -p '{"spec": {"unschedulable": true}}'

需要注意的是,将某个 Node 脱离调度范围时,在其上运行的 Pod 并不会自动停止,需要手动停止在该 Node 上运行的 Pod。。

同样,如果需要将某个 Node 重新纳入集群调度范围,则将 unschedulable 设置为 false,再次执行kubectl replacekubectl patch命令就能恢复系统对该 Node 的调度。

Node 的扩容

在实际生产系统中会经常遇到服务器容量不足的情况,这时就需要购买新的服务器,然后将应用系统进行水平扩展来完成对系统的扩容。

在 Kubernetes 集群中,对于一个新 Node 的加入是非常简单的。可以在 Node 节点上安装 Docker、Kubelet 和 kube-proxy服务,然后将 Kubelet 和 kube-proxy 的启动参数中的 Master URL 指定为当前 Kubernets 集群 Master 的地址,最后启动这些服务。基于 Kubelet 的自动注册机制,新的 Node 将会自动加入现有的 Kubetnetes 集群中。

Kubenetes Master 在接受了新 Node 的注册后,会自动将其纳入当前集群的调度范围内,在之后创建容器时,就可以向新的 Node 进行调度了。

Pod 动态扩容和缩放

在实际生产系统中,我们经常会遇到某个服务需要扩容的场景,也可能会遇到由于资源紧张或者工作负责降低需要减少服务实例数的场景。此时我们可以利用命令kubectl scale rc来完成这些任务。比如通过执行以下命令将 redis-slave RC 控制的 Pod 副本数量更新为 3:

1
$ kubectl scale rc redis-slave --replicas=3

将 –replicas 设置为比当前 Pod 副本数量更小的数字,系统将会“杀掉”一些运行中的 Pod,即可实现应用集群缩容。

更新资源对象的 Label

Label(标签)作为用户可灵活定义的对象属性,在已创建的对象上,仍然可以随时通过kubectl label命令对其进行增加、修改、删除等操作。

例如,我们要给已创建的 Pod “redis-master-bobr0”添加一个标签 role=backend:

1
$ kubectl label pod redis-master-bobr0 role=backend

删除一个 Label,只需在命令后最后指定 Label 的 key 名并与一个减号相连即可:

1
$ kubectl label pod redis-master-bobr0 role-

修改一个 Label 的值,需要加上 –overwrite 参数:

1
$ kubectl lable pod redis-master-bobr0 role=master --overwrite

将 Pod 调度到指定的 Node

Kubernetes 的 Scheduler 服务(kube-scheduler 进程)负责实现 Pod 的调度,整个调度过程通过一系列复杂的算法最终为每个 Pod 计算出一个最佳的目标节点,这一过程是自动完成的,我们无法知道 Pod 最终会被调度到哪个节点上。有时我们可能需要将 Pod 调度到一个指定的 Node 上,此时,我们可以通过 Node 的标签(Label)和 Pod 的 nodeSelector 属性相匹配,来达到上述目的。

首先,我们可以通过kubectl label命令给目标 Node 打上一个特定的标签,下面是此命令的完整用法:

1
$ kubectl label nodes <node-name> <label-key>=<label-value>

这里,我们为 kubenetes-minion1 节点打上一个 zone=north 的标签,表明它是“北方”的一个节点:

1
$ kubectl label nodes kubernetes-minion1 zone=north

上述命令行操作也可以通过修改资源定义文件的方式,并执行kubectl replace -f xxx.yaml命令来完成。

然后,在 Pod 的配置文件中加入 nodeSelector 定义,以 redis-master-controller.yaml 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: ReplicationController
metadata:
name: redis-master
labels:
name: redis-master
spec:
replicas: 1
selector:
name: redis-master
template:
metadata:
labels:
name: redis-master
spec:
containers:
- name: master
image: kubeguide/redis-master
ports:
- containerPort: 6379
nodeSelector:
zone: north

运行kubectl create -f命令创建 Pod,scheduler 就会将该 Pod 调度到拥有 zone=north 标签的 Node 上去。

使用kubectl get pods -o wide命令可以验证 Pod 所在的 Node。

如果我们给多个 Node 都定义了相同的标签,则 scheduler 将会根据调度算法从这组 Node 中挑选一个可用的 Node 进行 Pod 调度。

这种基于 Node 标签的调度方式灵活性很高,比如我们可以把一组 Node 分别贴上“开发环境”“测试环境”“正式环境”这三组标签中的一种,此时一个 Kubernetes 集群就承载了 3 个环境,这将大大提高开发效率。

需要注意的是,如果我们指定了 Pod 的 nodeSelector 条件,且集群中不存在包含相应标签的 Node 时,即使还有其他可供调度的 Node,这个 Pod 也最终会调度失败。

应用的滚动升级

当集群中的某个服务需要升级时,我们需要停止目前与该服务相关的所有 Pod,然后重新拉取镜像并启动。如果集群规模比较大,则这个工作就变成了一个挑战,而且先全部停止然后逐步升级的方式会导致较长时间的服务不可用。Kubernetes 提供了 rolling-update(滚动升级)功能来解决上述问题。

滚动升级通过执行kubectl rolling-update命令一键完成,该命令创建了一个新的 RC,然后自动控制旧的 RC 中的 Pod 副本数量逐渐减少到 0,同时新的 RC 中的 Pod 副本数量从 0 逐步增加到目标值,最终实现了 Pod 的升级。需要注意的是,系统要求新的 RC 需要与旧的 RC 在相同的命名空间(Namespace)内,即不能把别人的资产偷偷转移到自家名下。

以 redis-master 为例,假设当前运行的 redis-master Pod 是 1.0 版本,则现在需要升级到 2.0 版本。

创建 redis-master-controller-v2.yaml 的配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: ReplicationController
metadata:
name: redis-master-v2
version: v2
spec:
replicas: 1
selector:
name: redis-master
version: v2
template:
metadata:
labels:
name: redis-master
version: v2
spec:
containers:
- name: master
image: kubenetes/redis-master:2.0
ports:
- containerPort: 6379

在配置文件中有几处需要注意:

  1. RC 的名字(name)不能与旧的 RC 的名字相同;
  2. 在 selector 中应至少有一个 Label 与旧的 RC 的 Label 不同,以标识其为新的 RC。本例中新增了一个名为 version 的 Label,以与旧的 RC 进行区分。

运行kubectl rolling-update命令完成 Pod 的滚动升级:

1
$ kubectl rolling-update redis-master -f redis-master-controller-v2.yaml

等所有新的 Pod 启动完成后,旧的 Pod 也被全部销毁,这样就完成了容器集群的更新。

另一种方法是不使用配置文件,直接用kubectl rolling-update命令,加上--image参数指定新版镜像名称来完成 Pod 的滚动升级:

1
$ kubectl rolling-update redis-master --image=redis-master:2.0

如果在更新过程中发现配置有误,则用户可以中断更新操作,并通过执行kubectl rolling-update --rollback完成 Pod 版本的回滚:

1
$ kubectl rolling-update redis-master --image=redis-master:2.0 --rollback

Kubernetes 关键对象定义详解

Pod定义文件详解

Pod 的定义模版(yaml 格式)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apiVersion: v1
kind: Pod
metadata:
name: string
namespace: string
labels:
- name: string
annotations:
- name: string
spec:
containers:
- name: string
image: string
imagePullPolicy: [Always | Never | IfNotPresent]
command: [string]
workingDir: string
volumeMounts:
- name: string
mountPath: string
readOnly: boolean
ports:
- name: string
containerPort: int
hostPost: int
protocol: string

对各属性的详细说明如下:

属性名称 取值类型 是否必选 取值说明
version String Required v1
kind String Required Pod
metadata Object Required 元数据
metadata.name String Required Pod 名称,需符合 RFC 1035 规范
metadata.namespace String Required 命名空间,在不指定系统时将使用名为 “defautl” 的命名空间
metadata.labels[] List 自定义标签属性列表
metadata.annotation[] List 自定义注解属性列表
spec Object Required 详细描述
spec.containers[] List Required Pod 中运行的容器的列表
spec.containers[].name String Required 容器名称,需符合 RFC 1035 规范
spec.containers[].image String Required 容器的镜像名,在 Node 上如果不存在该镜像,则 Kubelet 会先下载
spec.containers[].imagePullPolicy String 获取镜像的策略,可选值包括:Always、Never、IfNotPresent,默认值为 Always。Always:表示每次都下载镜像。IfNotPresent:表示如果本地有该镜像,就是用本地的镜像。Never:表示仅使用本地镜像。
spec.containers[].command[] List 容器的启动命令列表,如果不指定,则使用镜像打包时使用的 CMD 命令。
spec.containers[].workingDir String 容器的工作目录
spec.containers[].volumeMounts[] List 可供容器使用的共享存储卷列表
spec.containers[].volumeMounts[].name String 引用 Pod 定义的共享存储卷的名称,需使用 volumes[] 部分定义的共享存储卷名称
spec.containers[].volumeMounts[].mountPath String 存储卷在容器内 Mount 的绝对路径,应少于 512 个字符
spec.containers[].volumeMounts[].readOnly boolean 是否为只读模式,默认为读写模式
spec.containers[].ports[] List 容器需要暴露的端口号列表
spec.containers[].ports[].name String 端口名称
spec.containers[].ports[].containerPort Int 容器需要监听的端口号
spec.containers[].ports[].hostPort Int 容器所在主机需要监听的端口号,默认与 containerPort 相同
spec.containers[].ports[].protocol String 端口协议,支持 TCP 和 UDP,默认为 TCP
spec.containers[].env[] List 容器运行前需要设置的环境变量列表
spec.containers[].env[].name String 环境变量名称
spec.containers[].env[].value String 环境变量的值
spec.containers[].resources Object 资源限制条件
spec.containers[].resources.limits Object 资源限制条件
spec.containers[].resources.limits.cpu String CPU 限制条件,将用于 docker run –cpu-shares 参数
spec.containers[].resources.limits.memory String 内存限制条件,将用于 docker run –memory 参数
spec.volumes[] List 在该 Pod 上定义的共享存储卷列表
spec.volumes[].name String 共享存储卷名称,需唯一,符合 RFC 1035 规范。容器定义部分 containers[].volumeMounts[].name 将引用该共享存储卷的名称
spec.volumes[].emptyDir Object 默认的存储卷类型,表示与 Pod 同生命周期的一个临时目录,其值为一个空对象:emptyDir:{}。该类型与 hostPath 类型互斥,应只定义一种
spec.volumes[].hostPath Object 使用 Pod 所在主机的目录,通过 volumes[].hostPath.path 指定。该类型与 emptyDir 类型互斥,应只定义一种
spec.volumes[].hostPath.path String Pod 所在主机的目录,将被用于容器中 mount 的目录。
spec.dnsPolicy String Required DNS 策略,可选值包括:Default、ClusterFirst
spec.restartPolicy Object 该 Pod 内容器的重启策略,可选值为 Always、OnFailure、Never,默认值为 Always。Always:容器一旦终止运行,无论容器是如何终止的,Kubelet 都将重启它。OnFailure:只有容器以非零退出码终止时,Kubelet 才会重启该容器。如果容器正常结束(退出码为0),则 Kubelet 将不会重启它。Never:容器终止后,Kubelet 将退出码报告给 Master,不再重启它
spec.nodeSelector Object 指定需要调度到的 Node 的 Label,以 key=value 格式指定
spec.imagePullSecrets Object Pull 镜像时使用的 secret 名称,以 name=secretkey 格式定义

RC 定义文件详解

RC(ReplicationController)定义文件模版(yaml格式)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: ReplicationController
metadata:
name: string
namespace: string
labels:
- name: string
annotations:
- name: string
spec:
replicas: number
selector: []
template: object

对各属性的说明如下:

属性名称 取值类型 是否必选 取值说明
version String Required v1
kind String Required ReplicationController
metadata object Required 元数据
metadata.name String Required ReplicationController 名称,需符合 RFC 1035 规范
metadata.namespace String Required 命名空间,不指定系统时将使用名为 “default” 的命名空间
metadata.labels[] List 自定义标签属性列表
metadata.annotaion[] List 自定义注解属性列表
spec Object Required 详细描述
spec.replicas number Required Pod 副本数量,设置为 0 表示不创建 Pod
spec.selector[] Listt Required Label Selector 配置,将选择具有指定 Label 标签的 Pod 作为管理范围
spec.template Object Required 容器的定义,与 Pod 的 spec 内容相同

Service定义文件详解

Service 的定义文件模版(yaml格式)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: v1
kind: Service
metadata:
name: string
namespace: string
labels:
- name: string
annotations:
- name: string
spec:
selector: []
type: string
clusterIP: string
sessionAffinity: string
ports:
- name: string
port: int
targetPort: int
protocol: string
status:
loadBalancer:
ingress:
ip: string
hostname: string

对各属性的说明如下:

属性名称 取值类型 是否必选 取值说明
version String Required v1
kind String Required Service
metadata Object Required 元数据
metadata.name String Required Service 名称,需符合 RFC 1035 规范
metadata.namespace String Required 命名空间,不指定系统时将使用名为“default”的命名空间
metadata.labels[] List 自定义标签属性列表
metadata.annotation[] List 自定义注解属性列表
spec Object Required 详细描述
spec.selector[] List Required Label Selector 配置,将选择具有指定 Label 标签的 Pod 作为管理范围
spec.type String Required Service 的类型,指定 Service 的访问方式,默认为 ClusterIP。ClusterIP:虚拟的服务器 IP 地址,该地址用于 Kubernetes 集群内部的 Pod 访问,在 Node 上 kube-proxy 通过设置的 iptables 规则进行转发。NodePort:使用宿主机的端口,使能够访问各 Node 的外部客户端通过 Node 的 IP 地址和端口号就能访问服务。LoadBalancer:使用外接负载均衡器完成到服务的负载分发啊 ,需要在 spec.status.loadBalancer 字段指定外部负载均衡器的 IP 地址,并同时定义 nodePort 和 clusterIP
spec.type.clusterIP String 虚拟服务 IP 地址,当 type=ClusterIP 时,如果不指定,则系统将自动分配;当 type=LoadBalancer 时,则需要指定
spec.sessionAffinity String 是否支持 Session,可选值为 ClientIP,默认为空。ClientIP:表示将同一个客户端(根据客户端的IP地址决定)的访问请求都转发到同一个后端 Pod
spec.ports[] List Service 需要暴露的端口号列表
spec.ports[].name String 端口名称
spec.ports[].port Int 服务监听的端口号
spec.ports[].targetPort Int 需要转发到后段 Pod 的端口号
spec.ports[].protocol String 端口协议,支持 TCP 和 UDP,默认为 TCP
status Object 当 spec.type=LoadBalancer 时,设置外部负载均衡器的地址
status.loadBalancer Object 外部负载均衡器
status.loadBalancer.ingress Object 外部负载均衡器
status.loadBalancer.ingress.ip String 外部负载均衡器的 IP 地址
status.loadBalancer.ingress.hostname String 外部负载均衡器的主机名

Fleet 跨节点服务调度

在 Systemd 的许多工具中,例如 systemctl 命令,都有一个 “–host”参数,它提供了一种可以跨主机操作另一个节点上服务的方法。然而,这种方式仍然具有比较大的局限性。

  • 节点间必须相互添加 SSH Key 信任,因为 Systemd 的远程操作是通过 SSH 连接进行的。
  • 操作时每次都必须指明目的节点的用户名和主机地址(IP 地址)。
  • 只能够远程控制已有的服务,但不能远程创建服务。
  • 仅限简单的服务操作,对于同时牵连多个节点的情况无能为力。例如,将服务迁移到另一个节点上运行。

为了更方便地在集群中部署和管理服务,CoreOS 基于 Systemd 的接口设计了服务调度工具 FLeet,它继承并扩展了 Unit 文件格式,使之更加适用于集群环境的服务配置。

Fleet的基本操作

Fleet 服务实际上是由运行在每一个节点上的后台服务进程 fleetd 组成的。此外,Fleet 还提供了一个用于交互控制的工具:fleetctl。

  • fleetctl list-machines 查看整个集群的基本信息
  • fleetctl list-units 查看整个集群的所有服务
  • fleetctl ssh ID 跳转到指定节点(需要添加公钥)
  • fleetctl ssh ID CMD 跨节点执行命令

通过 Unit 文件运行跨节点调度的服务

Fleet 的 Unit 文件

在服务管理方面,Fleet 使用和 Systemd 相似的 Unit 文件进行配置。不同的地方在于,Fleet 额外支持一个 X-Fleet 配置段,用于指定服务可以在哪些节点上运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Unit]
Description=Hello World
After=docker.service
Requires=docker.service
[Service]
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker kill busybox1
ExecStartPre=-/usr/bin/docker rm busybox1
ExecStartPre=/usr/bin/docker pull busybox
ExecStart=/usr/bin/docker run --name busybox1 busybox /bin/sh -c "while true; do echo Hello World; sleep 1; done"
ExecStop=/usr/bin/docker kill busybox1
[X-Fleet]
X-Conflicts=hello*.service

最后的 X-Fleet 配置段的 X-Conflicts 属性指定了这个 Hello 服务不能运行在“任意已经分配了任何名字以 hello 开头的服务”的节点上。

在集群上运行服务

由于 Fleet 需要在集群层面上对服务进行管理,因此它的服务管理流程与 Systemd 略有不同。最明显的区别是,Fleet 没有指定 Unit 文件必须放置在哪些目录下 ,而是直接通过参数的方式告诉 fleetctl 命令。

$ fleetctl start ${HOME}/hello.service

Fleet 的 X-Fleet 段

  • MachineID: 直接了当地告诉 Fleet 这个服务只能运行在特定节点上。注意,这里的值必须是完整的节点 ID,这个 ID 可以通过 “fleetctl list-machines -l”命令获得。
  • MachineOf: 值是另一个“.service”文件,表示当前服务必须与指定的这个服务运行在同一个节点上。
  • MachineMetadata: 值是一个节点的 Metadata 内容,例如“region=us-east-1”,这些 Metadata 是在启动节点时通过 Cloudinit 写进去的。这个参数可以使用多次,或者通过空格将多个值同时传进去。
  • Conflicts: 值是一个 .service 文件名或用于匹配文件名的通配字符串,Conflicts 参数也可以使用多次,并且其值可以使用通配符,例如 apache* 表示所有以“apache”开头的服务。
  • Global: 如果值为 true,则这个服务会被部署到集群中符合 MachineMetadata 限定条件的每一个节点上。注意,当 Global 值为 true 时,除了 MachineMetadata 以外的所有其它约束条件都会被忽略。

模版参数

Fleet 的 Unit 模版文件与 Systemd 的模版文件基本一致,同样在文件名的末尾有一个特征式的@符号。

虽然 Fleet 没有特定的 Unit 文件存放目录,但是只要在通过“fleetctl start”或“fleetctl submit”命令指定 Unit 文件路径时加上后缀参数,Fleet 同样会自动匹配去掉后缀参数的模版文件。例如 “fleetctl submit ${HOME}/apache@8080.service”,就会匹配到 ${HOME} 目录下面的 apache@.service 模版文件。

几乎所有的 Unit 模版文件都会使用到“%i”参数,因为它代表的是运行具体服务时,写在@符号后面部分的内容,这个值对于区分各个服务实例具有十分重要的意义。

集群中的服务生命周期

  • fleetctl submit ${PWD}/xxx.service 提交服务,将指定的 Unit 文件添加到 Fleet 的记录缓存中。
  • fleetctl cat xxx.service 打印已经缓存了的 Unit 文件内容
  • fleetctl load xxx.service 就绪
  • fleetctl start xxx.service 启动服务,并设置自动启动
  • fleetctl stop xxx.service 停止服务
  • fleetctl status xxx.service 查看服务状态
  • fleetctl journal xxx.service 查看服务日志
  • fleetctl journal –lines 20 xx.service 最新20行日志
  • fleetctl journal –follow xxx.service 跟随日志输出

服务热迁移

除了自动选择部署的节点外,Fleet的另一个重要功能便是在节点出现故障时,自动将故障节点上运行的服务迁移到另一个健康节点上。

RabbitMQ 集群

集群部署

我们准备三台服务器配置集群,其中两台RabbitMQ为内存(RAM)节点,单台为磁盘节点。

集群节点配置 DNS 记录或 hosts。

TODO

启动集群节点

1
2
3
rabbit1$ rabbitmq-server -detached
rabbit2$ rabbitmq-server -detached
rabbit3$ rabbitmq-server -detached

创建集群

1
2
3
rabbit2$ rabbitmqctl stop_app
rabbit2$ rabbitmqctl join_cluster --ram rabbit@rabbit1
rabbit2$ rabbitmqctl start_app

rabbit3 同上,也可以加入rabbit2

分离集群

1
2
3
4
rabbit3$ rabbitmqctl stop_app
rabbit3$ rabbitmqctl reset
rabbit1$ rabbitmqctl forget_cluster_node rabbit@rabbit3

集群备注

rabbitmqctl cluster_status 查看集群状态

rabbitmqctl stoprabbitmqctl stop_app 的区别是后者只停止应用,前者还会停止节点。

rabbitmqctl change_cluster_node_type ram|disc 可以更改节点类型(需要先停止应用)。

高可用

TODO