0%

Kubernetes中的InitContainer资源是一个有趣且非常有用的资源。在许多情况下,我们会看到它曾用于在Pod部署时,创建容器之前在卷中预填充数据,因此在业务容器启动时,卷数据已被初始化。

就我而言,我有一个带有单个静态页面的简单Web前端,它使用标准的nginx基础镜像:

1
2
3
4
FROM nginx

COPY index.html /usr/share/nginx/html/index.html
COPY smartos.ipxe /usr/share/nginx/html/smartos.ipxe

该镜像的构建和下载速度非常快,这非常棒,但是部分原因是它是无状态的。例如,smartos.ipxe文件中需要一些数据,这些数据在启动应用程序时需要可用,否则这些引用将无法按预期工作(抽象为404 HTTP响应):

1
2
3
4
5
6
#!ipxe
dhcp
set base-url http://sdc-ipxe.east.gourmet.yoga
kernel ${base-url}/smartos/smartos/platform/i86pc/kernel/amd64/unix -B smartos=true,console=ttyb,ttyb-mode="115200,8,n,1,-"
module ${base-url}/smartos/smartos/platform/i86pc/amd64/boot_archive type=rootfs name=ramdisk
boot

但是,这些文件不是应用程序的一部分,因为它们经常更新。因此,每次推出新版本时,我们都希望该卷中包含最新版本,并且由于我们不需要维护镜像中的这些文件,否则在我们的Registry中存储起来会非常大且昂贵,我们可以在Pod中的容器上挂载一个Volume来提供它们。

因此,基本上,我们需要一种方法来预填充要装入到/usr/share/nginx/html/smartos的卷。

使用InitContainer资源,我们可以指定要运行的命令,并且像Pod中的任何其他容器一样,我们可以分配要挂载的卷,因此让我们从这样的Kubernetes清单开始:

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: sdc-ipxe-deployment
labels:
app: sdc-ipxe
spec:
replicas: 2
selector:
matchLabels:
app: sdc-ipxe
template:
metadata:
labels:
app: sdc-ipxe
spec:
initContainers:
- name: config-data
image: ubuntu:xenial
command: ["/bin/sh","-c"]
args: ["apt update; apt install -y wget tar; wget https://us-east.manta.joyent.com/Joyent_Dev/public/SmartOS/platform-latest.tgz; tar xvf platform-latest.tgz -C /data; mkdir /data/smartos; mv /data/platform* /data/smartos/platform"]
volumeMounts:
- mountPath: /data
name: sdc-data
volumes:
- name: sdc-data
hostPath:
path: /mnt/kube-data/sdc-ipxe/
type: DirectoryOrCreate

因此,在这一点上,我们正在准备卷sdc数据,将其挂载到initContainer的/data目录上并运行:

1
apt update; apt install -y wget tar; wget https://us-east.manta.joyent.com/Joyent_Dev/public/SmartOS/platform-latest.tgz; tar xvf platform-latest.tgz -C /data; mkdir /data/smartos; mv /data/platform* /data/smartos/platform

上述命令下载数据并将其提取到卷中。现在,我们向yaml中添加一个container,然后再次附加该Volume,将可以使用预填充的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: apps/v1
kind: Deployment
metadata:
name: sdc-ipxe-deployment
labels:
app: sdc-ipxe
...
containers:
- name: sdc-ipxe
image: coolregistryusa.bix/jmarhee/sdc-ipxe:latest
imagePullPolicy: Always
ports:
- containerPort: 80
volumeMounts:
- mountPath: /usr/share/nginx/html/smartos
name: sdc-data
...

在业务容器中配置相同名称的卷,则业务容器就可以通过/usr/share/nginx/html/smartos目录获取sdc数据。

如果我们的应用程序依赖于具有可变需求的配置,则这种模式是有用的。可能是我们需要获得令牌,或者地址是动态的,并且需要通过磁盘上的文件而不是环境(比如负载平衡器,Web服务器或具有配置文件的数据库客户端,不容易通过它处理)传递文件(因为它们更改的频率不同)(Secret或ConfigMap),这种方法提供了一个易于编程的界面,用于预先填充或完成传递给容器的数据的模板化。

由于Docker在Kubernetes v1.20中已弃用,最近几天在Twitter上发生了很多讨论。

问题背景

Kubernetes v1.20的废弃说明:

如果想了解更多,强烈建议查看此Twitter

考虑到最近部署了一个Raspberry Pi Kubernetes集群,因此想就地进行更新,以使用Containerd代替Docker作为容器运行时。

免责声明–不要在生产集群中这样做。对于这些集群,只需删除现有节点,然后滚动引入新节点。这个博客只是关于Raspberry Pi集群的一个有趣的话题,看看是否可以在无需重建节点的情况下就地完成更新。

因此,要做的第一件事是drain需要更新的节点(我的节点称为k8s-node-1)并且cordon它:

1
kubectl drain k8s-node-1 --ignore-daemonsets

然后ssh进入节点并停止kubelet:

1
systemctl stop kubelet

然后删除Docker:

1
apt-get remove docker.io

删除旧的依赖项:

1
apt-get autoremove

现在unmask现有的containerd服务(Docker使用containerd,这就是为什么它已经存在的原因):

1
systemctl unmask containerd

安装所需的依赖项:

1
apt-get install unzip make golang-go libseccomp2 libseccomp-dev btrfs-progs libbtrfs-dev

完成以上步骤后,现在我们按照官方说明开始安装containerd。

无论如何,以root身份进行所有操作,获取containerd的源代码:

1
go get -d github.com/containerd/containerd

获取protoc 并安装:

1
2
wget -c https://github.com/google/protobuf/releases/download/v3.11.4/protoc-3.11.4-linux-x86_64.zip
sudo unzip protoc-3.11.4-linux-x86_64.zip -d /usr/local

获取runc 的源代码:

1
go get -d github.com/opencontainers/runc

进入到下载的包目录(检查$ GOPATH变量),使用make进行构建和安装runc和containerd:

1
2
3
4
5
6
cd ~/go/src/github.com/opencontainers/runc
make
make install
cd ~/go/src/github.com/containerd/containerd
make
make install

现在,将containerd.service文件复制到systemd以创建containerd的服务、启动服务并查看启动状态:

1
2
3
4
5
6
cp containerd.service /etc/systemd/system/
chmod 644 /etc/systemd/system/containerd.service
systemctl daemon-reload
systemctl start containerd
systemctl enable containerd
systemctl status containerd

差不多完成了,现在我们需要更新kubelet,将默认使用的docker改为containerd(参考flag设置)。我们可以通过运行:

1
sed -i 's/3.2/3.2 --container-runtime=remote --container-runtime-endpoint=unix:\/\/\/run\/containerd\/containerd.sock/g' /var/lib/kubelet/kubeadm-flags.env

如果上面的命令不起作用,请直接修改kubeadm-flags.env文件。

重启kubelet并查看服务状态:

1
2
systemctl start kubelet
systemctl status kubelet

最后,uncordon节点,并查看节点信息中的Runtime已变为containerd:

1
2
kubectl uncordon k8s-node-1
kubectl get nodes -o wide

问题引出

Helm是将应用程序部署到Kubernetes的绝佳工具。我们可以打包所有deployment和service等yaml文件,并使用一个简单的命令将它们部署到集群中。

但是Helm的另一个非常酷的功能是能够轻松升级和回滚版本(在集群中运行的Helm Chart实例的术语)的功能。

现在,我们可以使用kubectl进行此操作。如果我们使用kubectl apply升级deployment资源,则可以使用kubectl rollout undo来回滚该升级。这很棒!这是Kubernetes的最佳功能之一。

升级deployment时,将为该deployment创建一个新的replicaset,该replicaset将在一组新的Pod中运行升级后的应用程序。

如果使用kubectl rollout undo进行回滚,会删除最新replicaset中的容器,并回滚到旧replicaset的容器。

但是这里有一个潜在的问题。如果删除旧的replicaset会怎样?如果发生这种情况,我们将无法回滚升级。好吧,我们无法使用kubectl rollout undo将其回滚,但是如果我们使用Helm,会发生什么?

让我们来看一个演示。

Helm环境准备

创建一个称为testchart的Chart:

1
helm create testchart	

删除模板目录中所有不必要的文件:

1
rm -rf ./testchart/templates/*

创建一个deployment yaml文件:

1
2
3
4
kubectl create deployment nginx \
--image=nginx:1.17 \
--dry-run=client \
--output=yaml > ./testchart/templates/deployment.yaml

这将创建以下yaml并将其另存为templates目录中的deployment.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: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: nginx
name: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: nginx
spec:
containers:
- image: nginx:1.17
name: nginx
resources: {}
status: {}

创建deployment:

1
kubectl create deployment nginx --image=nginx:1.17 

为service生成yaml:

1
2
3
4
5
kubectl expose deployment nginx \
--type=LoadBalancer \
--port=80 \
--dry-run=client \
--output=yaml > ./testchart/templates/service.yaml

这将为我们提供以下yaml并将其另存为模板目录中的service.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: nginx
name: nginx
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: nginx
type: LoadBalancer
status:
loadBalancer: {}

删除deployment,模板化values.yaml 和deployment.yaml文件:

1
2
3
4
kubectl delete deployment nginx
rm ./testchart/values.yaml
echo "containerImage: nginx:1.17" > ./testchart/values.yaml
sed -i 's/nginx:1.17/{{ .Values.containerImage }}/g' ./testchart/templates/deployment.yaml

最终,deployment.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: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: nginx
name: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: nginx
spec:
containers:
- image: {{ .Values.containerImage }}
name: nginx
resources: {}
status: {}

改造后的yaml中容器镜像不再是硬编码的。它将从values.yaml文件中获取nginx:1.17的值,或者我们可以使用set标志来覆盖它(我们将在一分钟内完成)。

Helm部署示例

首先,将Chart部署到Kubernetes集群中:

1
helm install testchart ./testchart

helm-install-1

该应用程序版本是Chart.yaml文件中设置的默认版本(尚未更新)

检查部署中运行的镜像版本:

1
kubectl get deployment -o jsonpath='{ .items[*].spec.template.spec.containers[*].image }{"\n"}'

get-container-image-1

查看到的容器镜像就是Chart中values.yaml文件中定义的镜像版本。

现在升级Release,将默认的容器镜像值替换为set标志指定的值:

1
helm upgrade testchart ./testchart --set containerImage=nginx:1.18

确认版本已升级(检查版本号):

1
helm list

helm-upgrade-1

另外,请确认Release历史:

1
helm history testchart

helm-history-updated

这样我们就可以看到该Release的初始部署,然后是升级。应用版本保持不变,因为我没有更改Chart.yaml文件中的值。但是,镜像版本已更改,我们可以通过以下方式看到:

1
kubectl get deployment -o jsonpath='{ .items[*].spec.template.spec.containers[*].image }{"\n"}'

get-container-image-2

因此,我们已经升级了在deployment中容器运行的镜像版本。

让我们看一下deployment的replicasets:

1
kubectl get replicasets

get-replicasets-1

因此,我们为Helm版本创建的deployment有两个replicasets。最初的一个运行nginx v1.17,最新的一个运行nginx v1.18。

如果我们想使用kubectl回退升级,则可以使用(不要运行此代码!):

1
kubectl rollout undo deployment nginx

kubectl-rollout-undo

这里将发生的是,删除最新replicasets下的Pod,并创建旧replicasets下的Pod,将nginx回滚到v1.17。

但是我们不会那样做,因为我们正在使用Helm。

问题复现

继续在当前环境中获取最旧的replicasets名称,并删除它::

1
2
REPLICA_SET=$(kubectl get replicasets -o jsonpath='{.items[0].metadata.name }' --sort-by=.metadata.creationTimestamp)
kubectl delete replicasets $REPLICA_SET

因此,我们现在只有一个replicasets:

1
kubectl get replicasets

get-replicasets-2

现在尝试使用kubectl rollout undo命令进行回滚:

1
kubectl rollout undo deployment nginx

kubectl-rollout-undo-2

失败的原因是我们删除了旧的replicasets,因此该deployment没有历史记录,可以通过以下方式查看:

1
kubectl rollout history deployment nginx

kubectl-rollout-history

使用Helm回滚

虽然旧的replicasets被删除了,但是Helm的实现机制决定了使用Helm部署的Release会保留历史:

1
helm history testchart

helm-history-2

所以,我们可以使用Helm回滚:

1
helm rollback testchart 1

helm-rollback

查看Release状态:

1
helm list

helm-list-rollback

查看Release历史:

1
helm history testchart

helm-rollback-history

查看replicasets:

1
kubectl get replicasets

get-replicasets-3

旧的replicasets又回来了!怎么样?

原理探究

让我们看一下集群中的secrets:

1
kubectl get secrets

kubectl-get-secrets

可以看出,这些secrets中会存储Helm发布所有历史记录!初始版本(v1),升级(v2)和回滚(v3)。

让我们仔细看看v1版本:

1
kubectl get secret sh.helm.release.v1.testchart.v1 -o json

kubectl-get-secrets-2

嗯,这个Release内容看起来很有趣。我们可以做的是对base64进行解码,然后通过http://www.txtwizard.net/compression进行解压缩,得到结果如下:

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
{
"name":"testchart",
"info":
{
"first_deployed":"2020-08-09T11:21:20.4665817+01:00",
"last_deployed":"2020-08-09T11:21:20.4665817+01:00",
"deleted":"",
"description":"Install complete",
"status":"superseded"},
"chart":{"metadata":
{
"name":"testchart",
"version":"0.1.0",
"description":"A Helm chart for Kubernetes",
"apiVersion":"v2",
"appVersion":"1.16.0",
"type":"application"},
"lock":null,
"templates":[
{
"name":
"templates/deployment.yaml",
"data":"YXBpVmVyc2lvbjogYXBwcy92MQpraW5kOiBEZXBsb3ltZW50Cm1ldGFkYXRhOgogIGNyZWF0aW9uVGltZXN0YW1wOiBudWxsCiAgbGFiZWxzOgogICAgYXBwOiBuZ2lueAogIG5hbWU6IG5naW54CnNwZWM6CiAgcmVwbGljYXM6IDEKICBzZWxlY3RvcjoKICAgIG1hdGNoTGFiZWxzOgogICAgICBhcHA6IG5naW54CiAgc3RyYXRlZ3k6IHt9CiAgdGVtcGxhdGU6CiAgICBtZXRhZGF0YToKICAgICAgY3JlYXRpb25UaW1lc3RhbXA6IG51bGwKICAgICAgbGFiZWxzOgogICAgICAgIGFwcDogbmdpbngKICAgIHNwZWM6CiAgICAgIGNvbnRhaW5lcnM6CiAgICAgIC0gaW1hZ2U6IHt7IC5WYWx1ZXMuY29udGFpbmVySW1hZ2UgfX0KICAgICAgICBuYW1lOiBuZ2lueAogICAgICAgIHJlc291cmNlczoge30Kc3RhdHVzOiB7fQo="},{"name":"templates/service.yaml","data":"YXBpVmVyc2lvbjogdjEKa2luZDogU2VydmljZQptZXRhZGF0YToKICBjcmVhdGlvblRpbWVzdGFtcDogbnVsbAogIGxhYmVsczoKICAgIGFwcDogbmdpbngKICBuYW1lOiBuZ2lueApzcGVjOgogIHBvcnRzOgogIC0gcG9ydDogODAKICAgIHByb3RvY29sOiBUQ1AKICAgIHRhcmdldFBvcnQ6IDgwCiAgc2VsZWN0b3I6CiAgICBhcHA6IG5naW54CiAgdHlwZTogTG9hZEJhbGFuY2VyCnN0YXR1czoKICBsb2FkQmFsYW5jZXI6IHt9Cg=="}],"values":{"containerImage":"nginx:1.17"},"schema":null,"files":[{"name":".helmignore","data":"IyBQYXR0ZXJucyB0byBpZ25vcmUgd2hlbiBidWlsZGluZyBwYWNrYWdlcy4KIyBUaGlzIHN1cHBvcnRzIHNoZWxsIGdsb2IgbWF0Y2hpbmcsIHJlbGF0aXZlIHBhdGggbWF0Y2hpbmcsIGFuZAojIG5lZ2F0aW9uIChwcmVmaXhlZCB3aXRoICEpLiBPbmx5IG9uZSBwYXR0ZXJuIHBlciBsaW5lLgouRFNfU3RvcmUKIyBDb21tb24gVkNTIGRpcnMKLmdpdC8KLmdpdGlnbm9yZQouYnpyLwouYnpyaWdub3JlCi5oZy8KLmhnaWdub3JlCi5zdm4vCiMgQ29tbW9uIGJhY2t1cCBmaWxlcwoqLnN3cAoqLmJhawoqLnRtcAoqLm9yaWcKKn4KIyBWYXJpb3VzIElERXMKLnByb2plY3QKLmlkZWEvCioudG1wcm9qCi52c2NvZGUvCg=="}]},
"manifest":"---\n#
Source: testchart/templates/service.yaml\n
apiVersion: v1\n
kind: Service\nmetadata:\n
creationTimestamp: null\n
labels:\n
app: nginx\n
name: nginx\n
spec:\n
ports:\n
- port: 80\n
protocol: TCP\n
targetPort: 80\n
selector:\n
app: nginx\n
type: LoadBalancer\n
status:\n loadBalancer: {}\n---\n#

Source: testchart/templates/deployment.yaml\n
apiVersion: apps/v1\n
kind: Deployment\n
metadata:\n
creationTimestamp: null\n
labels:\n
app: nginx\n
name: nginx\nspec:\n
replicas: 1\n
selector:\n
matchLabels:\n
app: nginx\n
strategy: {}\n
template:\n
metadata:\n
creationTimestamp: null\n
labels:\n
app: nginx\n
spec:\n
containers:\n
- image: nginx:1.17\n
name: nginx\n
resources: {}\n
status: {}\n",
"version":1,
"namespace":"default"
}

BOOM!看起来就像我们的deployment和service清单!我们可以看到最初的Helm版本中包含的所有信息(确认容器镜像为nginx:1.17)!

因此,通过将这些信息作为secrets存储在目标Kubernetes集群中,即使已删除了旧的replicasets,Helm也可以回滚升级!太酷了!

不过结果还不是很清晰,查看data字段……看起来像是加密信息。

让我们解密吧!这次在命令行上:

1
kubectl get secret sh.helm.release.v1.testchart.v1 -o jsonpath="{ .data.release }" | base64 -d | gunzip -c | jq '.chart.templates[].data' | tr -d '"' | base64 -d

decode-helm-secret

哈!这里有deployment和service的yaml文件!

通过使用Helm,即使已删除deployment的旧replicasets,我们也可以回滚,因为Helm将Release历史记录在secrets并存储在目标Kubernetes集群中。通过使用上面的代码,我们可以解密这些secrets并查看其中包含的信息。

如果我们在集群中安装了Helm Chart,可能会想知道Release的存储位置。

背景知识

让我们从一些背景开始。安装一个简单的Nginx Helm Chart:

1
$ helm install --name my-release stable/nginx-ingress

现在,要获取已安装Helm的详细信息,可以使用四个命令。

helm ls

1
2
3
$ helm ls 
NAME REVISION UPDATED STATUS
my-release 1 Wed Sep 12 07:41:48 2018 DEPLOYED

通常,我们要运行的第一个命令是helm ls。执行此操作是为了了解我们的集群中当前安装了哪些Helm Chart。无论它们是否失败,STATUS会展示出部署结果是成功还是失败。

helm get

一旦获得安装Chart的名称。下一步通常是尝试更详细地了解安装了什么。helm get命令可以为我们提供帮助。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ helm get my-release
REVISION: 1
RELEASED: Thu Mar 23 15:59:14 2017
CHART: nginx-1.0
USER-SUPPLIED VALUES:
foo: bar

COMPUTED VALUES:
foo: bar
image: nginx
imagePullPolicy: IfNotPresent
ingress:
# **....**

helm status

如果我们遇到任何问题,并且希望获得Chart开发人员写下的一些说明。helm status可以通过呈现NOTES.txt文件来帮助我们。

1
2
3
4
5
6
$ helm status my-release
The nginx-ingress controller has been installed.
Get the application URL by running these commands:
export NODE_IP=$(kubectl --namespace {{ .Release.Namespace }} get nodes -o jsonpath="{.items[0].status.addresses[1].address}")
echo "Visit http://10.10.10.10:80 to access your application via HTTP."
echo "Visit https://10.10.10.10:443 to access your application via HTTPS."

上面的Helm状态可以通过values.yaml或–set修改。这是从NOTES.txt呈现的帮助者文本。

helm history

最后,我们还可以获得Chart部署的修订历史记录。当运行helm upgrade命令时会更新版本。假设我们要使用override.yaml覆盖某些值。

1
2
3
4
5
$ helm upgrade --install my-release --values override.yaml --set foo=notbar nginx 
$ helm history my-release
REVISION UPDATED STATUS CHART DESCRIPTION
1 Thu Mar 23 15:57:40 2020 SUPERSEDED nginx-0.4.3 Install complete
2 Thu Mar 23 15:59:14 2020 DEPLOYED nginx-0.4.3 Upgrade complete

所有这些信息都存储在哪里?

  • Helm v2版本,默认位置在configmap中:

    1
    2
    3
    4
    $ kubectl get configmap -n kube-system -l "OWNER=TILLER"
    NAME DATA AGE
    my-release.v1 1 7m
    my-release.v2 1 6m
  • Helm v3版本,默认位置在secrets中。强烈建议这样做,因为这些数据包含许多有关我们部署的信息:

    1
    2
    3
    4
    5
    $ kubectl get secrets -n kube-system
    NAME DATA AGE
    my-release.v1 1 7m
    my-release.v2 1 6m
    default-token-43hfuds 1 1d

解析Configmap内容

步骤1. 获取Configmap数据:

1
$ kubectl get configmap -n kube-system my-release.v1 -o=jsonpath='{.data.release}' > release-encoded

步骤2. 确保编码后的Release包含如下字符串:

1
2
H4sIAAAAAAAC/+w6TY8cS.....
# you should see a long block of string like above

步骤3. 解析数据:

1
cat release-encoded | base64 -d | gzip -cd > release-decoded

步骤4. 查看数据:

1
2
3
4
cat release-decoded
# you should see a whole bunch of data for the chart similar to above when you did helm get.
# but also this data contains a lot more like. the actual template. Value rendered.. etc...
# try it :) i already gave you the commands 🤠⽕😁🏃🏼‍

将Chart存储在configmaps中的问题在于,一旦黑客进入我们的集群,它就会成为黑客的金钥匙。将其存储为secrets可以提供某种保护(假设我们对机密信息进行了加密)。☸️

解析Secrets内容

步骤1. 解析指定版本的Release所有内容(Template内容依然是编码格式):

1
kubectl get secret sh.helm.release.v1.my-release.v1 -o jsonpath="{ .data.release }" | base64 -d | gunzip -c | jq .

步骤2. 解析指定版本的Release中的Template内容:

1
kubectl get secret sh.helm.release.v1.my-release.v1 -o jsonpath="{ .data.release }" | base64 -d | gunzip -c | jq '.chart.templates[].data' | tr -d '"' | base64 -d

注:该方法同样适用于解析Configmap内容。

一些建议

还有一些保护tiller的方法,例如使用https连接。但是,按照设计,tiller仍然需要大量特权才能在我们的集群中运行。并且仍然违反最小特权原则我的建议是尽快移至helm3

Helm3完全删除了tiller,而是依靠本地计算机的身份验证在群集中工作。默认情况下,它还将Chart数据作为secrets存储在群集中。Helm2将在2020年12月停止提供安全修复程序

本文提供了在Kubernetes上部署安全,可伸缩和弹性服务的最佳实践。内容开源在github仓库。如果有缺少或不足之处,欢迎提issue。

Part1 应用开发

健康检测

  • 为容器配置Readiness探针
    • 如果未设置readiness探针,则kubelet会假定该应用程序已准备就绪,可以在容器启动后立即接收流量。
    • 如果容器需要2分钟才能启动,则这2分钟内对容器的所有请求将失败。
  • 发生致命错误时允许容器崩溃
    • 如果应用程序遇到不可恢复的错误,则应使其崩溃。

    • 此类不可恢复的错误的示例是:

      1. 未捕获的异常
      2. 代码中的错字(动态语言)
      3. 无法加载标头或依赖项
    • 上述错误不应发信号通知Liveness探针失败。相反,应该立即退出该进程,并让kubelet重新启动容器。

  • 配置被动的Liveness探针
    • Liveness探针旨在容器卡住时重新启动容器。
    • 考虑以下情形:如果应用程序正在处理无限循环,则无法退出。当该进程消耗100%的CPU时,将没有时间回复(其他)Readiness探针检查,并且最终将其从服务中删除。但是,该Pod仍被注册为当前Deployment的活动副本。如果没有Liveness探针,它将保持运行状态,但与服务分离。换句话说,该进程不仅不处理任何请求,而且也在消耗资源。
    • 请注意,不应该使用Liveness探针来处理应用程序中的致命错误,并要求Kubernetes重新启动应用程序。相反,应该让应用程序崩溃。仅在过程无响应的情况下,才应将“liveness”探针用作恢复机制。
  • 两个探针的值不同
    • 当“liveness”和“readiness”探针指向相同的端点时,探针的作用会合并在一起。当应用程序发出信号表明尚未准备就绪或尚待运行时,kubelet会将容器与服务分离并同时将其删除。这时可能会注意到连接断开,因为容器没有足够的时间耗尽当前连接或处理传入的连接。
    • 更多信息参考:handling-client-requests-properly-with-kubernetes/

请注意,readiness和liveness没有默认值。

应用独立

  • Readiness探针是独立的

    • Readiness不包括对服务的依赖性,例如:数据库、数据库的迁移、API、第三方服务(反例
  • 应用重试连接到依赖服务

    • 应用启动时,它不应该因为数据库等依赖项尚未就绪而崩溃。相反,应用程序应继续尝试重新连接数据库,直到成功为止。
    • Kubernetes希望可以以任何顺序启动应用程序。当确保应用程序可以重新连接到诸如数据库之类的依赖项时,便知道可以提供更强大,更灵活的服务。

友好关闭

  • 应用程序未通过SIGTERM关闭,但可以正常终止连接
    • 可能需要一些时间才能感知到诸如kube-proxy或Ingress控制器之类的组件endpoint更改。因此,尽管标记为已终止,流量仍可能流向Pod。
    • 应用程序应停止在所有剩余连接上接受新请求,并在耗尽传出队列后将其关闭。
    • 如果想回顾endpoint在群集中的传播方式,请参考:handling-client-requests-properly-with-kubernetes/
  • 应用程序仍在宽限期内处理传入的请求
    • 可能要考虑使用容器生命周期事件(例如preStop处理程序)来自定义Pod删除之前发生的情况。
  • Dockerfile中的CMD将SIGTERM转发到进程
  • 关闭所有空闲的keep-alive套接字
    • 如果应用程序调用未关闭TCP连接(例如使用TCP保持活动状态或连接池),它将连接到一个Pod,而不使用该服务中的其他Pod。
    • 不应该突然终止长期存在的连接。相反,应该在关闭应用程序之前终止它们。
    • 更多信息参考:gracefully-shutting-down-a-nodejs-http-server

失败容忍

  • 为Deployment部署运行多个副本
    • 切勿单独运行一个Pod类型的资源,而是考虑将Pod作为Deployment,DaemonSet,ReplicaSet或StatefulSet的一部分进行部署。
    • 示例参考:Node-Management-In-GKE
  • 避免将Pod放置在单个节点中
    • 即使运行Pod的多个副本,也无法保证丢失节点不会影响服务。
    • 应该将反关联性规则应用于部署,以便Pod分布在群集的所有节点中。
    • 更多信息参考:inter-pod-affinity-and-anti-affinit
  • 设定Pod中断预算
    • drain节点后,该节点上的所有Pod都将被删除并重新安排。
    • 为了保护Deployment免受可能同时摧毁多个Pod的意外事件的影响,可以定义Pod中断预算。
    • 更多信息参考:pod-disruptions

资源使用

  • 为所有容器设置内存限制和请求
    • 资源限制用于限制容器可以使用多少CPU和内存,并使用containerSpec的resources属性设置。
    • 调度程序将这些用作度量标准之一,以确定哪个节点最适合当前Pod。
    • 根据调度程序,没有内存限制的容器的内存利用率为零。
    • 如果可调度在任何节点上的Pod数量不受限制,则会导致资源超负荷使用并可能导致节点(和kubelet)崩溃。
    • 如果容器进程超出内存限制,则该进程将终止。由于CPU是可压缩的资源,因此如果容器超出限制,则将限制该过程。即使它可以使用当时可用的某些CPU。
    • 更多信息参考:understanding-resource-limits-in-kubernetes-memoryunderstanding-resource-limits-in-kubernetes-cpu
  • 将CPU请求设置为1个CPU或以下
    • 除非有计算密集型作业,否则建议将请求设置为1个CPU或更低.
    • 更多信息参考:YouTube视频
  • 禁用CPU限制—除非有很好的用例
    • CPU 资源以 CPU 单位度量。
    • cpu:1表示每秒1个CPU单位。如果有1个线程,则每秒消耗的CPU时间不能超过1秒。如果有2个线程,则可以在0.5秒内消耗1个CPU单位。8个线程可以在0.125秒内消耗1个CPU单位。此后,请求将受到限制。
    • 如果不确定最佳应用设置,最好不要设置CPU限制。
    • 更多信息参考:understanding-resource-limits-in-kubernetes-cpu
  • 命名空间具有LimitRange
    • 如果我们认为可能忘记设置内存和CPU限制,则应考虑使用LimitRange对象为当前名称空间中部署的容器定义标准大小。
    • 设置方法参考:limit-range
  • 为Pod设置适当的服务质量(QoS)
    • 当节点进入过量使用状态(即使用过多资源)时,Kubernetes会尝试驱逐该节点中的某些Pod。
    • Kubernetes根据定义明确的逻辑对Pod进行排名和逐出。
    • 设置方法参考:quality-service-pod

请注意,如果不确定如何配置正确的CPU或内存限制,则可以使用Kubernetes中的Vertical Pod Autoscaler。自动缩放器会分析应用并给出建议的值。

标签资源

  • 定义技术标签
  • 定义业务标签
  • 定义安全标签

日志配置

  • 将应用程序日志记录到stdout和stderr
    • 有两种日志记录策略:被动和主动。使用被动日志记录的应用程序不了解日志记录基础结构,而是将消息记录到标准输出中。
    • 在主动日志记录中,该应用程序与中间聚合器建立网络连接,将数据发送到第三方日志记录服务,或直接写入数据库或索引。主动日志记录被视为反模式,应避免使用它。
    • 最佳实践参考:logs
  • 避免使用sidecar记录日志(如果可以的话)
    • 如果希望将日志转换应用于具有非标准日志事件模型的应用程序,则可能需要使用sidecar容器。
    • 使用Sidecar容器,可以在将日志条目运送到其他地方之前对其进行规范化。例如,先将Apache日志转换为Logstash JSON格式,然后再将其发送到日志基础结构。但是,如果可以控制应用程序,则可以从一开始就输出正确的格式。这样可以节省为集群中的每个Pod运行额外的容器的时间。

Pod扩缩容

  • 容器在其本地文件系统中不存储任何状态
    • 容器可以访问本地文件系统,用户可能会想使用它来持久化数据。
    • 但是,将持久性数据存储在容器的本地文件系统中会阻止Pod进行水平缩放(即通过添加或删除Pod的副本)。
    • 这是因为,通过使用本地文件系统,每个容器都维护自己的“状态”,这意味着Pod副本的状态可能会随时间而变化。从用户的角度来看,这会导致行为不一致(例如,当请求命中一个Pod时,一条特定的用户信息可用,但当请求命中另一个Pod时,则不可用)。
    • 相反,任何持久性信息都应保存在Pod外部的集中位置。例如,在集群中的PersistentVolume中,或者在集群外部的某些存储服务中甚至更好。
  • 对具有可变使用模式的应用程序使用HPA
    • HPA是内置的Kubernetes功能,可监视应用程序并根据当前使用情况自动添加或删除Pod副本。
    • 配置HPA可使应用在任何流量情况下(包括意外的高峰)保持可用并响应。
    • 配置HPA时必须创建一个HorizontalPodAutoscaler资源,该资源定义要监视的应用程序的度量。
    • HPA可以监视内置资源指标(Pod的CPU和内存使用情况)或自定义指标。对于自定义指标,还负责收集和公开这些指标,例如,可以使用Prometheus和Prometheus Adapter进行此操作。
  • Vertical Pod Autoscaler仍处于Beta版,请勿使用
    • 类似于HPA,还有VPA。
    • VPA可以自动调整Pod的资源请求和限制,以便当Pod需要更多资源时可以获取它们(增加/减少单个Pod的资源称为垂直缩放,与水平缩放相对)。
    • 这对于缩放无法水平缩放的应用程序很有用。
    • 但是,HPA当前处于beta版本,它具有一些已知的局限性(例如,通过更改其资源要求来扩展Pod,要求终止Pod并重新启动它)。
    • 考虑到这些限制以及Kubernetes上大多数应用程序都可以水平扩展的事实,建议不要在生产环境中使用VPA(至少要等到稳定的版本才能使用)。
  • 如果工作负载差异很大,请使用群集自动伸缩放器
    • 群集自动缩放器是“自动缩放器”的另一种类型(HAP和VPA除外)。
    • 群集自动缩放器可以通过添加或删除工作节点来自动缩放群集的大小。
    • 当由于现有工作节点上的资源不足而无法调度Pod时,会进行放大操作。在这种情况下,Cluster Autoscaler将创建一个新的工作节点,以便可以调度Pod。同样,当现有工作节点的利用率较低时,群集自动伸缩程序可以通过从一个工作节点中逐出所有工作负载并将其删除来进行缩减。
    • 对于高度可变的工作负载,例如当Pods的数量可能在短时间内成倍增长然后返回到先前的值时,使用Cluster Autoscaler是有意义的。在这种情况下,群集自动伸缩器可以满足需求高峰,而不会通过过度配置工作节点来浪费资源。
    • 但是,如果工作负载变化不大,则可能不值得设置Cluster Autoscaler,因为它可能永远不会触发。如果工作负载缓慢且单调地增长,则足以监视现有工作节点的利用率并在达到临界值时手动添加其他工作节点。

配置原则

  • 外部化所有配置
    • 配置应在应用程序代码之外进行维护。
    • 这有几个好处。首先,更改配置不需要重新编译应用程序。其次,可以在应用程序运行时更新配置。第三,相同的代码可以在不同的环境中使用。
    • 在Kubernetes中,可以将配置保存在ConfigMaps中,然后可以在将卷作为环境变量传入时将其安装到容器中。
    • 在ConfigMap中仅保存非敏感配置。对于敏感信息(例如凭据),请使用Secret资源。
  • 将Secrets作为卷而不是环境变量安装
    • Secret资源的内容应作为卷装入容器中,而不应作为环境变量传递。
    • 这是为了防止秘密值出现在用于启动容器的命令中,该命令可能由不应该访问秘密值的人员看到。

Part2 集群管理

命名空间限制

  • 命名空间具有LimitRange
    • 没有限制的容器可能导致与其他容器的资源争用以及计算资源的消耗。
    • Kubernetes具有两个限制资源利用的功能:ResourceQuota和LimitRange。
    • 使用LimitRange对象,可以定义资源请求的默认值以及名称空间内单个容器的限制。
    • 在该命名空间内创建的,未明确指定请求和限制值的任何容器都将分配为默认值。
    • 更多信息参考:resource-quotas
  • 命名空间具有ResourceQuotas
    • 使用ResourceQuotas,可以限制命名空间内所有容器的总资源消耗。
    • 定义命名空间的资源配额会限制属于该名称空间的所有容器可以消耗的CPU,内存或存储资源的总量。
    • 还可以为其他Kubernetes对象设置配额,例如当前名称空间中的Pod数量。
    • 如果存在他人使用群集并创建20000 ConfigMap,则可以使用LimitRange来防止这种情况。

Pod安全策略

  • 启用Pod安全策略

    • 例如,可以使用Kubernetes Pod安全策略来限制:
      1. 访问主机进程或网络名称空间;
      2. 运行特权容器容器;
      3. 运行的用户;
      4. 访问主机文件系统;
      5. Linux功能,Seccomp或SELinux配置文件
    • 选择正确的策略取决于集群的性质。
    • 更多信息参考:kubernetes-pod-security-policy
  • 禁用特权容器

    • 在Pod中,容器可以以“特权”模式运行,并且对主机系统上的资源的访问几乎不受限制。
    • 尽管在某些特定的用例中,必须具有这种级别的访问权限,但总的来说,让容器执行此操作存在安全风险。
    • 特权Pod的有效使用案例包括在节点上使用硬件,例如GPU。
    • 更多信息参考:security-context
  • 在容器中使用只读文件系统

    • 在容器中运行只读文件系统会强制容器不可变。
    • 这不仅减轻了一些旧的(且有风险的)做法(例如热修补),而且还帮助防止了恶意进程在容器内存储或操作数据的风险。
    • 使用只读文件系统运行容器听起来可能很简单,但是可能会带来一些复杂性。
    • 如果需要写日志或将文件存储在临时文件夹中怎么办?
    • 更多信息参考:running-docker-containers-securely-in-production
  • 防止容器以root身份运行

    • 在容器中运行的进程与主机上的任何其他进程没有什么不同,只不过它有一小部分元数据声明它在容器中。
    • 因此,容器中的根与主机上的根(uid 0)相同。
    • 如果用户设法脱离了以root用户身份在容器中运行的应用程序,则他们可能能够使用同一root用户获得对主机的访问权限。
    • 配置容器以使用非特权用户是防止特权升级攻击的最佳方法。
    • 更多信息参考:processes-in-containers-should-not-run-as-root
  • 限制capabilities

    • Linux capabilities使进程能够执行许多特权操作,其中只有root用户默认可以执行。

    • 例如,CAP_CHOWN允许进程“对文件UID和GID进行任意更改”。

    • 即使进程不是以root身份运行,进程也有可能通过提升特权来使用那些类似root的功能。

    • 换句话说,如果不想受到损害,则应仅启用所需的功能。

    • 但是应该启用什么功能?为什么?以下两篇文章探讨了有关Linux内核功能的理论和最佳实践:

      Linux Capabilities: Why They Exist and How They Work

      Linux Capabilities In Practice

  • 防止特权升级

    • 应该在关闭特权升级的情况下运行容器,以防止使用setuid或setgid二进制文件提升特权。

网络策略

  • 启用网络策略
    • Kubernetes网络策略指定Pod组的访问权限,就像云中的安全组用于控制对VM实例的访问一样。
    • 换句话说,它在Kubernetes集群上运行的Pod之间创建了防火墙。
    • 更多信息参考:Securing Kubernetes Cluster Networking
  • 每个命名空间中都有一个保守的NetworkPolicy

RBAC策略

  • 禁用默认服务帐户的自动挂载RBAC策略
  • 设置为所需的最少特权
    • 寻找有关如何设置RBAC规则的好的建议是一项挑战。
    • Kubernetes RBAC的3种现实方法中,可以找到三种实用场景和有关如何入门的实用建议。
  • RBAC策略是精细的,不能共享
    • Zalando有一个简洁的策略来定义角色和ServiceAccounts。
    • 首先,他们描述他们的要求:
      1. 用户应该能够部署,但不应允许他们查看如“secret”这类资源
      2. 管理员应拥有对所有资源的完全访问权限
      3. 默认情况下,应用程序不应获得对Kubernetes API的写访问权限
      4. 对于某些用途,可以有Kubernetes API写权限。
    • 四个要求转化为五个单独的角色:
      1. ReadOnly
      2. PowerUser
      3. Operator
      4. Controller
      5. Admin
    • 更多信息参考:access-control-roles-and-service-accounts

自定义策略

  • 只允许从已知registry部署容器
    • 可能要考虑的最常见的自定义策略之一是限制可以在群集中部署的镜像。
    • 参考文档说明了如何使用开放策略代理来限制未批准的镜像。
  • 强制Ingress主机名唯一
    • 用户创建Ingress清单时,可以使用其中的任何主机名。
    • 但是,可能希望阻止用户多次使用相同的主机名并互相覆盖。
    • Open Policy Agent的官方文档包含有关如何在validation Webhook中检查Ingress资源的教程。
  • 仅在Ingress主机名中使用批准的域名
    • 用户创建Ingress清单时,可以使用其中的任何主机名。
    • 但是,可能希望阻止用户使用无效的主机名。
    • Open Policy Agent的官方文档包含有关如何在validation Webhook中检查Ingress资源的教程。

Part3 集群配置

该部分还在进行中。如果对这部分内容有意见,欢迎提issue。

集群要求

  • 集群通过CIS基准测试

    • 互联网安全中心提供了一些准则和基准测试,以确保代码安全的最佳做法
    • 他们还维护了Kubernetes的基准,可以从官方网站上下载该基准。
    • 虽然可以阅读冗长的指南并手动检查集群是否符合要求,但更简单的方法是下载并执行kube-bench
    • kube-bench是一个工具,用于自动执行CIS Kubernetes基准测试并报告集群中的错误配置。

    请注意,无法使用kube-bench检查托管集群(例如GKE,EKS和AKS)的主节点。主节点由云提供商控制。

  • 禁用云提供商的元数据API

    • 云平台(AWS,Azure,GCE等)通常将本地元数据服务公开给实例。
    • 默认情况下,实例上运行的Pod可以访问这些API,并且可以包含该节点的云凭据或诸如kubelet凭据之类的置备数据。
    • 这些凭据可用于在群集内升级或升级到同一帐户下的其他云服务。
  • 限制对Alpha或Beta功能的访问

    • Alpha和Beta Kubernetes功能正在积极开发中,可能会存在限制或错误,从而导致安全漏洞。
    • 始终评估Alpha或Beta功能可能提供的价值,以防对安全状况造成潜在风险。
    • 如有疑问,请禁用不使用的功能。

身份认证

  • 使用OpenID(OIDC)令牌作为用户身份验证策略

    • Kubernetes支持各种身份验证方法,包括OpenID Connect(OIDC)。
    • OpenID Connect允许单点登录(SSO)(例如Google身份)连接到Kubernetes集群和其他开发工具。
    • 无需单独记住或管理凭据。
    • 可能有多个群集连接到同一OpenID提供程序。
    • 更多信息参考:kubernetes-single-sign-one-less-identity
  • ServiceAccount令牌仅适用于应用程序和控制器

    • ServiceAccount不应用于尝试与Kubernetes群集进行交互的最终用户,但对于在Kubernetes上运行的应用程序和工作负载,它们是首选的身份验证策略。

日志设置

  • 有一个日志保留和归档策略
    • 应该保留30-45天的历史日志。
  • 从节点,控制平面,审计中收集日志
    • 从哪些地方收集日志:
      1. 节点 (kubelet, container runtime)
      2. 控制平面 (API server, scheduler, controller manager)
      3. Kubernetes审计 (all requests to the API server)
    • 应该收集什么:
      1. 应用名称。从元数据标签中检索。
      2. 应用程序实例。从元数据标签中检索。
      3. 应用程序版本。从元数据标签中检索。
      4. 集群ID。从Kubernetes集群检索。
      5. 容器名称。从Kubernetes API检索。
      6. 运行此容器的群集节点。从Kubernetes集群检索。
      7. 运行容器的Pod名称。从Kubernetes集群检索。
      8. 命名空间。从Kubernetes集群检索。
  • 在每个节点上最好有一个守护程序来收集日志,而不是sidecar
    • 应用程序日志应输出到标准输出,而不是文件。
    • 每个节点上的守护程序可以从容器运行时收集日志(如果记录到文件,则可能需要每个pod的sidecar容器)。
  • 提供日志聚合工具
    • 使用日志聚合工具,例如EFK技术栈(Elasticsearch,Fluentd,Kibana),DataDog,Sumo Logic,Sysdig,GCP Stackdriver,Azure Monitor,AWS CloudWatch。

创建Tag保留规则

一个repository可以快速积累大量镜像tag,在给定时间之后或一旦它们被后续镜像构建取代后,可能不需要许多镜像tag。这些多余的tag显然会消耗大量的存储容量。作为Harbor系统管理员,可以定义规则来管理给定repository中要保留多少个tag,或将某些tag保留多长时间。

Tag保留规则如何工作

在repositories上而不是projects上定义tag保留规则(repository属于project内的概念)。在定义保留规则时,这可以提供更大的粒度。顾名思义,当我们为repositories定义保留规则时,也即在定义要保留的tag。我们没有定义规则来显式删除tag,而是当设置规则时,repositories中任何未标识为可保留的标记都将被丢弃。

tag保留规则具有3个按顺序应用的过滤器,如下表所述。

Order Filter Description
First 一个或多个repository 标识要在其上应用规则的一个或多个repository。可以标识具有特定名称或名称片段的repository,或者不具有该名称或名称片段的repository。允许使用通配符(例如* repo,repo *和**)。首先使用repository filter以标记要对其应用保留规则的repository。根据标签标准,将识别出的repository指定用于进一步匹配。在此阶段,对没有指定的repository不采取任何措施。
Second 保留数量 通过指定最大数量的标签或指定最大保留标签的时间来设置要保留的标签。
Third 要保留的标签 标识要应用规则的一个或多个标签。可以标识具有特定名称或名称片段的标签,或者不具有该名称或名称片段的标签。允许使用通配符(例如* tag,tag *和**)。

有关如何应用**通配符的信息,请参见https://github.com/bmatcuk/doublestar#patterns。

Example 1

  • 在一个project中有5个repositories,repositories A到E。
    • repository A具有100个镜像tag,所有这些镜像tag均已在上周有pull操作。
    • repository B到E每个都有6个镜像,上个月都没有pull操作。
  • 将repositories过滤器设置为**,这意味着包括了project中的所有repositories。
  • 设置保留策略,以在每个repositories中保留最近提取的10个映像。
  • 将标签过滤器设置为**,这意味着包括repositories中的所有标签。

在此示例中,规则在repository A中保留了10个最近有pull操作的镜像,并且在repository B至E中的每一个中都保留了所有6个镜像。因此,project中总共保留了34个镜像tag。换句话说,该规则不会将repository A到E中的所有镜像都视为单个池,然后从中选择10个最新镜像。因此,即使repository A中的第11至第100个标签比repository B至E中的任何标签相比都有pull操作,也将保留repository B至E中的所有标签,因为每个repository中的标签少于10个。

Example 2

本示例使用与示例1相同的project和repositories,但设置了保留策略以保留每个repository中最近7天内有pull操作的镜像。

在这种情况下,保留了repository A中的所有镜像,因为它们是最近7天内有pull操作的。repository B到E中的任何镜像都不会保留,因为它们在上周都没有发生pull操作。在此示例中,保留了100个镜像,而示例1中则保留了34个镜像。

Tag保留规则和原生Docker Tag删除问题

警告:由于本机Docker tag删除行为,当前的保留策略实施存在问题。如果有多个tag引用相同的SHA摘要,并且如果这些tag的子集被配置的保留策略标记为要删除,则所有其余tag也将被删除。这违反了保留策略,因此在这种情况下,所有tag都将保留。在以后的更新版本中将解决此问题,以便tag保留策略可以删除tag而不删除摘要和其他共享tag。

例如,我们有以下tag,这些tag根据其推送时间列出,并且它们都引用相同的SHA摘要:

  • harbor-1.8, pushed 8/14/2019 12:00am
  • harbor-release, pushed 8/14/2019 03:00am
  • harbor-nightly, pushed 8/14/2019 06:00am
  • harbor-latest, pushed 8/14/2019 09:00am

如果配置了保留策略,以保留与Harbor- *匹配的两个最新标记,以便删除Harbor-rc和Harbor-latest。但是,由于所有tag都引用相同的SHA摘要,因此此策略还将删除标签Harbor-1.8和Harbor-release,因此将保留所有标签。(时间和删除的tag有点对不上)

在一个Repository上合并规则

每个project最多可以定义15条规则。我们可以将多个规则应用于一个repository或一组repositories。在将多个规则应用于repository 时,它们将使用OR逻辑而不是AND逻辑来应用。这样,就不会在给定的repository 上优先应用规则。规则在后台同时运行,每个规则的结果集在运行结束时合并。

Example 3

本示例使用与示例1和2相同的project和repositories,但是设置了两个规则:

  • 规则1:保留每个repository中最近7天内pull操作的所有镜像。
  • 规则2:每个repository中最多保留10个镜像。

对于repository A,规则1保留所有镜像,因为它们都是在上周发生pull操作的。规则2保留最近发生pull操作的10个镜像。因此,由于这两个规则是通过OR关系应用的,所有100个镜像都保留在repository A中。

对于repositories B-E,规则1将保留0个镜像,因为上周未发生pull操作。规则2将保留所有6个镜像,因为6 <10。因此,由于这两个规则以OR关系应用,对于repositories B-E,每个repository将保留所有6个图像。

在此示例中,所有镜像均被保留。

Example 4

本示例使用与先前示例不同的repository。

  • 包含12个tag的repository:

    Production Release Candidate Release
    2.1-your_repo-prod 2.1-your_repo-rc 2.1-your_repo-release
    2.2-your_repo-prod 2.2-your_repo-rc 2.2-your_repo-release
    3.1-your_repo-prod 3.1-your_repo-rc 3.1-your_repo-release
    4.4-your_repo-prod 4.4-your_repo-rc 4.4-your_repo-release
  • 在此repository上定义了3个tag保留规则:

    • 保留以2开头的10个最近发生pull操作的镜像tag。

    • 保留以-prod结尾的10个最近发生pull操作的镜像tag。

    • 保留所有不包含2.1-your_repo-prod的tag。

在此示例中,规则将应用于以下7个tag(与预期不符,官方收到反馈后已修改):

  • 2.1-your_repo-rc
  • 2.1-your_repo-release
  • 2.2-your_repo-prod
  • 2.2-your_repo-rc
  • 2.2-your_repo-release
  • 3.1-your_repo-prod
  • 4.4-your_repo-prod

Tag保留规则如何与项目配额搭配使用

Harbor系统管理员可以设置一个project可以包含的tag数量及其可以使用的存储量的最大值。有关project配额的信息,请参阅配置project配额

如果在project上设置配额,则不能超过该配额。即使设置的保留规则超过配额,配额也将应用于project。换句话说,不能使用保留规则来绕过配额。

配置Tag保留规则实例

  1. 使用至少具有项目管理员特权的帐户登录到Harbor界面。

  2. 转到”project”,选择一个project,然后选择tag保留。

    tag-retention

  3. 单击”添加规则”以添加规则。

  4. 在“repositories”下拉菜单中,选择匹配或排除。

    tag-retention

  5. 在“repositories”文本框中,标识要在其上应用规则的repository。

    可以通过输入以下信息来定义要在哪些repositories上应用规则:

  • repository名称,例如my_repo_1。

  • 以逗号分隔的repository名称列表,例如my_repo_1,my_repo_2,your_repo_3。

  • 带通配符的部分repository名称,例如my _ _ 3或*repo*。

  • **将规则应用于project中的所有repositories。

    如果选择匹配,则规则将应用于标识的repository。如果选择排除,则该规则将应用于project中除已标识的repositories之外的所有repositories。

  1. 在“按镜像或天数计数”下拉菜单中,定义要保留的tag数量或保留的时间。

    tag-retention

Option Description
retain the most recently pushed # images 输入要保留的最大镜像个数,保留最近发生push操作的镜像。不考虑镜像的时间。
retain the most recently pulled # images 输入要保留的最大镜像个数,保留最近发生pull操作的镜像。不考虑镜像的时间。
retain the images pushed within the last # days 输入保留镜像的天数,仅保留在此期间发生push操作的镜像。不考虑镜像数量。
retain the images pulled within the last # days 输入保留镜像的天数,仅保留在此期间发生pull操作的镜像。不考虑镜像数量。
retain always 始终保留此规则标识的镜像。
  1. 在”tag”下拉菜单中,选择匹配或排除。

  2. 在“标签”文本框中,标识要在其上应用规则的tag。

    可以通过输入以下信息来定义要在其上应用规则的tag:

  • tag名称,例如my_tag_1。
  • tag名称的逗号分隔列表,例如my_tag_1,my_tag_2,your_tag_3。
  • 带通配符的部分tag名称,例如my _ _ 3或* tag *。
  • **将规则应用于project中的所有tag。
  1. 单击”添加”以保存规则。

  2. (可选)单击“添加规则”以添加更多规则,每个project最多15条。

  3. (可选)在“计划”下,单击“编辑”,然后选择运行规则的频率。

    tag-retention

    如果选择“自定义”,请输入cron job命令以配置规则。

    注意:如果定义多个规则,则计划将应用于所有规则。不能配置不同的规则在不同的时间运行。

  4. 单击“模拟运行”以测试定义的一个或多个规则。

  5. 单击“立即运行”以立即运行规则。

警告:运行规则后,将无法还原它。强烈建议先执行模拟运行,然后再运行规则。

要修改现有规则,请使用规则旁边的操作下拉菜单来禁用,编辑或删除该规则。

tag-retention

Big picture

了解Calico支持的各种网络选项,以便可以根据需要选择最佳选项。

Value

Calico灵活的模块化体系结构支持广泛的部署选项,因此可以根据自己的特定环境和需求选择最佳的网络方案。这包括在BGP和非BGP的情况下,以underlying或overlay模式与各种CNI和IPAM插件以及基础网络类型一起运行的能力。

Concepts

如果想完全了解可用的网络选项,我们建议确保自己熟悉并理解以下概念。如果希望跳过学习并直接获得选择和建议,则可以跳至“网络选项”。

Kubernetes网络基础知识

Kubernetes网络模型定义了一个“扁平”网络,其中:

  • 每个Pod都有自己的IP地址。
  • 无需NAT,任何节点上的Pod均可与所有其他节点上的所有Pod通信。

这将创建一个干净的,向后兼容的模型,从端口分配,命名,服务发现,负载平衡,应用程序配置和迁移的角度来看,可以将Pod像VM或物理主机一样对待。可以使用网络策略来定义网络分段,以将流量限制在这些基本网络功能内。

在此模型中,可以灵活地支持不同的网络方案和环境。确切地如何实现网络的详细信息取决于所使用的CNI,网络和云提供商插件的组合。

CNI插件

CNI(容器网络接口)是一个标准API,允许将不同的网络实现插入Kubernetes。每当创建或销毁Pod时,Kubernetes都会调用API。CNI插件有两种类型:

  • CNI网络插件:负责向Kubernetes Pod网络中添加Pod或从Kubernetes Pod网络中删除Pod。这包括创建/删除每个Pod的网络接口,以及将其连接/断开与其他网络实现的连接。
  • CNI IPAM插件:负责在Pod创建或删除时分配和释放Pod的IP地址。根据插件的不同,这可能包括为每个节点分配一个或多个IP地址(CIDR)范围,或从底层公共云网络获取IP地址以分配给Pod。

云提供商集成

Kubernetes云提供商集成是特定于云的控制器,可以配置基础云网络以帮助提供Kuberenetes网络。根据云提供商的不同,这可能包括自动将路由编程到基础云网络中,以便它本机知道如何路由Pod流量。

Kubenet

Kubenet是Kubernetes中内置的一个非常基本的网络插件。它没有实现跨节点通信或网络策略。它通常与云提供商集成一起使用,后者在云提供商网络中设置路由以在节点之间或在单节点环境中进行通信。Kubenet与Calico不兼容。

Overlay网络

overlay网络是位于另一个网络之上的网络。在Kubernetes的上下文中,overlay网络可用于处理基础网络顶部节点之间的Pod到Pod流量,这些节点不知道Pod IP地址或哪些Pod在哪些节点上运行。overlay网络通过将基础网络不知道如何处理(例如使用Pod IP地址)的网络数据包封装在基础网络确实知道如何处理的外部数据包(例如节点IP地址)中。用于封装的两种常见网络协议是VXLAN和IP-in-IP。

使用overlay网络的主要优点是它减少了对基础网络的依赖性。例如,可以在几乎任何基础网络之上运行VXLAN,而无需与基础网络集成或对其进行任何更改。

使用overlay网络的主要缺点是:

  • 对性能有轻微影响。封装数据包的过程占用少量CPU,并且数据包中用于编码封装(VXLAN或IP-in-IP标头)所需的额外字节减少了可以发送的内部数据包的最大大小,这意味着需要为相同数量的总数据发送更多数据包。
  • Pod IP地址无法在集群外部路由。

跨子网Overlay网络

除了标准的VXLAN或IP-in-IP overlay外,Calico还支持VXLAN和IP-in-IP的“cross-subnet”模式。在这种模式下,在每个子网中,基础网络充当L2网络。在单个子网内发送的数据包不进行封装,因此可以获得非overlay网络的性能。跨子网发送的数据包像普通的overlay网络一样被封装,从而减少了对基础网络的依赖(无需与基础网络集成或对其进行任何更改)。

就像使用标准overlay网络一样,基础网络也不知道Pod IP地址,并且Pod IP地址无法在集群外部路由。

Pod IP路由到集群外部的能力

不同的Kubernetes网络实现的一个重要区别特征是Pod IP地址是否可在整个较宽网络的集群外部路由。

不可路由

如果Pod IP地址无法在集群外部路由,则当Pod尝试建立与集群外部IP地址的网络连接时,Kubernetes将使用一种称为SNAT(源网络地址转换)的技术来更改源IP从Pod的IP地址到托管Pod的节点的IP地址。连接上的所有返回数据包都会自动映射回Pod IP地址。因此,Pod不知道发生了SNAT,连接的目的地将节点视为连接的源,而底层的更广泛的网络不会看到Pod IP地址。

对于相反方向的连接,其中集群外部的某些设备需要连接到Pod,这只能通过Kubernetes service或Kubernetes ingress来完成。集群之外的任何人都无法直接连接到Pod IP地址,因为更广泛的网络不知道如何将数据包路由到Pod IP地址。

可路由

如果Pod IP地址可以在集群外部路由,则Pod可以不使用SNAT即可连接到外部世界,并且集群外部可以直接连接到Pod,而无需通过Kubernetes service或Kubernetes ingress。

Pod IP可路由到集群外部的优点是:

  • 避免将SNAT用于出站连接对于与现有更广泛的安全要求进行集成可能至关重要。它还可以简化操作日志的调试和易懂性。
  • 如果有专门的工作负载,这意味着某些Pod需要直接访问而不需要通过Kubernetes service或Kubernetes ingress,那么可路由的Pod IP在操作上可能更简单。

Pod IP地址可路由到集群外的主要缺点是,Pod IP在整个网络中必须是唯一的。因此,例如,如果运行多个群集,则需要为每个群集中的Pod使用不同的IP地址范围(CIDR)。当大规模运行时,或者如果现有其他企业对IP地址空间有大量重要需求,这又可能导致IP地址范围耗尽的挑战。

决定可路由性的因素是什么?

如果集群使用overlay网络,则Pod IP通常无法路由到集群外。

如果集群不使用overlay网络,那么Pod IP是否路由到集群外取决于所用的CNI插件,云提供商集成或与物理网络(对于本地)BGP对等连接。

BGP

BGP(边界网关协议)是用于跨网络共享路由的基于标准的网络协议。它是互联网的基本组成部分之一,具有出色的扩展特性。

Calico内置了对BGP的支持。在本地部署中,这使Calico可以与物理网络(通常连接到Top或Rack路由器)建立对等关系以交换路由,从而形成一个none-overlay网络,其中Pod IP地址可以在更广泛的网络中路由,就像附加的任何其他工作负载一样到网络。

关于Calico网络

Calico网络灵活的模块化架构包括以下内容。

Calico CNI网络插件

Calico CNI网络插件使用一对虚拟以太网设备(一对)将Pod连接到主机网络名称空间的L3路由。这种L3架构避免了许多其他Kubernetes网络解决方案中附加的L2桥不必要的复杂性和性能开销。

Calico CNI IPAM插件

Calico CNI IPAM插件为一个或多个可配置IP地址范围之外的Pod分配IP地址,并根据需要为每个节点动态分配IP块。与许多其他CNI IPAM插件(包括在许多网络解决方案中使用的主机本地IPAM插件)相比,具有更有效的IP地址空间使用。

Overlay网络模式

Calico可以提供VXLAN或IP-in-IP网络,包括cross-subnet模式。

Non-overlay网络模式

Calico可以提供在任何基础L2网络之上运行的non-overlay网络,或者是具有适当的云提供商集成的公共云网络或支持BGP的L3网络(通常是具有标准Top-of-Rack路由器)。

网络策略

Calico的网络策略执行引擎实现了Kubernetes网络策略的全部功能,以及Calico Network Policy的扩展功能。这可以与Calico的内置网络模式或任何其他Calico兼容的网络插件和云提供商集成结合使用。

与Calico兼容的CNI插件和云提供商集成

除Calico CNI插件和内置网络模式外,Calico还与许多第三方CNI插件和云提供商集成兼容。

Amazon VPC CNI

Amazon VPC CNI插件从基础AWS VPC分配Pod IP,并使用AWS弹性网络接口提供VPC本机Pod网络(可在集群外部路由的Pod IP)。它是Amazon EKS中使用的默认网络,并与Calico一起用于网络策略实施。

Azure CNI

Azure CNI插件从基础Azure VNET分配Pod IP,将Azure虚拟网络配置为提供VNET本机Pod网络(可在群集外部路由的Pod IP)。它是Microsoft AKS中使用的默认网络,可与Calico一起执行网络策略。

Azure cloud provider

Azure云提供商集成可以用作Azure CNI插件的替代方法。它使用host-local IPAM插件分配Pod IP,并使用相应的路由对基础Azure VNET子网进行编程。Pod IP仅可在VNET子网内路由(这通常意味着它们无法路由到群集外部)。

Google cloud provider

Google云提供商集成使用host-local IPAM插件分配Pod IP,并对Google Cloud网络Alias IP范围进行编程,以在Google Cloud上提供VPC本机Pod网络(可在集群外部路由的Pod IP)。它是Google Kubernetes Engine(GKE)的默认设置,并带有Calico来执行网络策略。

Host local IPAM

host-local IPAM插件是常用的IP地址管理CNI插件,它为每个节点分配固定大小的IP地址范围(CIDR),然后从该范围内分配Pod IP地址。默认地址范围大小是256个IP地址(a/24),其中两个IP地址是为特殊目的保留的,未分配给Pod。host-local IPAM插件的简单性使其易于理解,但与Calico CNI IPAM插件相比,其IP地址空间使用效率较低。

Flannel

Flannel使用从host-local IPAM插件获得的静态CIDR路由pod间的通信。Flannel提供了许多网络后端,但主要与VXLAN后端一起使用。Calico CNI和Calico网络策略可以与flannel和host-local IPAM插件结合使用,以提供具有策略实施功能的VXLAN网络。这种组合有时称为“Canal”。

注意:Calico现在内置了对VXLAN的支持,为了简化起见,我们通常建议优先使用Calico + Flannel组合。

网络选择

本地

Calico本地部署最常见的网络设置是non-overlay模式,该模式使用BGP与物理网络(通常是机架路由器的顶部)对等,以使Pod IP可在集群外部路由。(当然,可以根据需要配置其余的本地部署网络,以限制群集外的Pod IP路由的范围。)此设置提供了丰富的Calico高级功能,包括公告Kubernetes serviceIP的能力(cluster IPs or external IPs),以及在Pod,名称空间或节点级别控制IP地址管理的能力,以支持与现有企业网络和安全要求集成的各种可能性。

Policy IPAM CNI Overlay Routing
Calico Calico Calico No BGP

如果不能将BGP对等连接到物理网络,并且群集在单个L2网络中,则还可以运行non-overlay模式,而Calico只能在群集中的节点之间对等BGP。即使这不是严格的overlay网络,也无法在集群外部路由Pod IP,因为基础网络没有Pod IP的路由。

Policy IPAM CNI Overlay Routing
Calico Calico Calico No BGP

或者,可以在VXLAN或IP-in-IP模式下运行Calico,并使用cross-subnet模式来优化每个L2子网内的性能。

推荐方案:

Policy IPAM CNI Overlay Routing
Calico Calico Calico VXLAN Calico

替代方案:

Policy IPAM CNI Overlay Routing
Calico Calico Calico IPIP BGP

AWS

如果希望在集群外部可路由Pod IP地址,则必须使用Amazon VPC CNI插件。这是EKS的默认网络模式,并使用Calico的网络策略。Pod IP地址是从基础VPC分配的,每个节点的Pod的最大数量取决于实例类型。

Policy IPAM CNI Overlay Routing
Calico AWS AWS No VPC/Native

如果希望避免依赖特定的云提供商,或者由于IP地址范围耗尽的挑战,或者如果Amazon VPC CNI插件每个节点支持的最大Pod数量不足以从基础VPC分配Pod IP,则存在问题。根据需求,我们建议使用Calico的overlay + cross-subnet模式。Pod IP将无法在集群外部路由,但是可以在不依赖基础云网络的情况下将集群扩展到Kubernetes的极限。

Policy IPAM CNI Overlay Routing
Calico Calico Calico VXLAN Calico

在这个简短的视频中,可以了解有关AWS上的Kubernetes Networking的更多信息,包括上述每个选项的工作原理。Everything you need to know about Kubernetes networking on AWS

Azure

如果希望在群集外部可以路由Pod IP地址,则必须使用Azure CNI插件。这由AKS和Calico进行网络策略支持。Pod IP地址是从基础VNET分配的。

Policy IPAM CNI Overlay Routing
Calico Azure Azure No VPC/Native

如果要使用AKS,但由于IP地址范围耗尽的问题而无法从基础VNET分配Pod IP,则可以将Calico与Azure云提供商集成一起使用。它使用host-local IPAM为每个节点分配/24地址段,并为这些/24地址段在群集的基础VNET子网中编程路由。在群集/VNET子网外部无法路由Pod IP,因此如果需要,可以在多个群集中使用相同的Pod IP地址范围(CIDR)。

注意:在某些AKS文档中将其称为kubenet + Calico,但实际上是带有Azure云提供程序的Calico CNI,并且不使用kubenet插件。

Policy IPAM CNI Overlay Routing
Calico Host Local Calico No VPC/Native

如果不使用AKS,而是希望避免依赖于特定的云提供商,或者由于IP地址范围耗尽的问题而无法从基础VNET分配Pod IP,那么我们建议使用Calico的overlay + cross-subnet模式。Pod IP将无法在集群外部路由,但是可以在不依赖基础云网络的情况下将集群扩展到Kubernetes的极限。

Policy IPAM CNI Overlay Routing
Calico Calico Calico VXLAN Calico

可以在此短视频中了解有关Azure上Kubernetes Networking的更多信息,包括上述每个选项的工作原理:Everything you need to know about Kubernetes networking on Azure

Google Cloud

如果想在集群外部路由Pod IP地址,则必须将Google云提供商集成与host-local IPAM CNI插件结合使用。GKE和Calico都为网络策略提供了支持。从基础VPC分配Pod IP地址,并自动将相应的Alias IP地址分配给节点。

Policy IPAM CNI Overlay Routing
Calico Host Local Calico No VPC/Native

如果希望避免依赖特定的云提供商,或者由于IP地址范围耗尽的挑战而无法从基础VPC分配Pod IP,那么我们建议使用Calico的overlay模式。由于Google云网络是纯L3网络,因此不支持cross-subnet模式。Pod IP将无法在集群外部路由,但是可以在不依赖基础云网络的情况下将集群扩展到Kubernetes的极限。

推荐方案:

Policy IPAM CNI Overlay Routing
Calico Calico Calico VXLAN Calico

替代方案:

Policy IPAM CNI Overlay Routing
Calico Calico Calico IPIP BGP

可以在此短片中了解有关Google云上的Kubernetes Networking的更多信息,包括上述每个选项的工作原理:Everything you need to know about Kubernetes networking on Google cloud

IBM Cloud

如果使用的是IBM Cloud,则建议使用IKS,该产品具有内置Calico的功能,可提供cross-subnet +IPIP模式的网络模式。除了为Pod提供网络策略外,IKS还使用Calico网络策略来保护群集中的主机节点。

Policy IPAM CNI Overlay Routing
Calico Calico Calico IPIP BGP

Anywhere

上面的环境列表显然并不详尽。理解本指南中的概念和解释有助于确定适合的环境。如果仍然不确定,则可以通过Calico用户的Slack或Discourse论坛寻求建议。记住,如果要使用,而不想担心各种选项,则可以在几乎任何环境中以VXLAN + overlay模式运行Calico。

Policy IPAM CNI Overlay Routing
Calico Calico Calico VXLAN Calico

简介

边界网关协议(BGP)是互联网涉及的核心技术之一,它使网络能够相互通信。了解如何使用此工具来定义网络拓扑,不仅可以让我们更好地了解互联网络,还可以将这种鲁棒性应用到自己的网络。

在本教程结束时,我们将熟悉BGP的核心概念,并能够把相关的术语传达给其他人。此外,我们还将学会使用BIRD的用户界面建立对等会话并开始发布路由。

本文使用docker容器实现这一目标。为了完成本教程,需要确保已安装dockerdocker-compose

准备

首先,需要克隆bird_examples_docker项目。

1
2
3
ncatelli@ofet> git clone https://github.com/ncatelli/bird_examples_docker.git
ncatelli@ofet> cd bird_examples_docker
ncatelli@ofet> docker-compose up -d

上述命令将创建三个容器(peer1peer2peer3),所有这些容器均已安装BIRD并已建立对等会话。如果还不知道这意味着什么,请不要担心,我们将在设置好BGP并准备就绪后对其进行介绍。

首先连接到peer1并检查所有设置是否正确。

1
2
3
4
5
6
7
8
9
ncatelli@ofet> docker-compose exec peer1 bash
root@peer1:/# birdc show protocols
BIRD 1.6.6 ready.
name proto table state since info
kernel1 Kernel master up 02:36:03
device1 Device master up 02:36:03
direct1 Direct master up 02:36:03
peer2 BGP master up 02:36:08 Established
peer3 BGP master up 02:36:07 Established

如果发现peer2peer3已经显示Established,则说明一切正常,我们已准备就绪。在开始使用之前,将简要介绍BGP的工作原理。

原理概述

边界网关协议(BGP)是一种外部网关协议,用于在自治系统之间交换路由信息。自治系统(AS)是路由前缀和策略的组织单位。这些AS由唯一的16位(后来扩展到32位)自治系统编号(ASN)标识。例如,Facebook的ASN为32934或通常显示为AS32934。BGP的强大之处在于其在成千上万个分散式AS之间传递路由协议和策略的能力。

互联网以及许多其他网络由相互之间进行通信的许多自治系统组成。对等会话促进了这种通信,该会话允许两个AS交换策略,路由和链接状态。所有这些信息都在两个BGP守护程序之间交换,这两个守护程序在TCP的179端口上进行侦听。

虽然BGP被认为是用于在Internet上的大型组织之间进行路由的外部网关协议,但它也可以在AS中使用,以使网络工程师能够控制其内部网络的拓扑。这是eBGP和iBGP术语的来源。iBGP将是本教程其余部分的重点。现在,我们将开始使用BIRD及其交互式命令行工具birdc尝试这些对等会话。

BIRD简介

BIRD是功能齐全的路由守护程序,它支持许多不同的路由协议,包括BGP。BIRD提供了一种简单的配置格式和命令行,用于与会话进行交互。BIRD还内置了对IPv4和IPv6的支持,以及与这两种协议一起使用的相应工具。

检查BGP会话

与我们验证是否正确配置了docker环境的方式类似,我们可以通过运行以下命令查看正在运行的会话:

1
2
3
4
5
6
7
8
root@peer1:/# birdc show protocols
BIRD 1.6.6 ready.
name proto table state since info
kernel1 Kernel master up 02:36:02
device1 Device master up 02:36:02
direct1 Direct master up 02:36:02
peer2 BGP master up 02:36:07 Established
peer3 BGP master up 02:36:06 Established

这给了我们很多信息。但是,让我们关注最后两个条目,peer2peer3。我们可以看到它们都是BGP协议,并且info字段显示已经Established。这些条目中的每一个都对应于peer1peer2peer3打开的BGP会话。为了演示这些值与正在运行的会话之间的关系,让我们在peer2上停止Bird服务。在新的终端窗口中,运行以下命令来停止peer2,模拟网络故障。

1
2
ncatelli@ofet> docker-compose stop peer2
Stopping bird_examples_peer2_1 ... done
1
2
3
4
5
6
7
8
root@peer1:/# birdc show protocols
BIRD 1.6.6 ready.
name proto table state since info
kernel1 Kernel master up 02:36:02
device1 Device master up 02:36:02
direct1 Direct master up 02:36:02
peer2 BGP master start 02:43:38 Connect Socket: Connection closed
peer3 BGP master up 02:36:06 Established

通过重新启动peer2,BIRD应该重新启动,并且随后应重新建立对等会话。

1
2
ncatelli@ofet> docker-compose start peer2
Starting peer2 ... done
1
2
3
4
5
6
7
8
root@peer1:/# birdc show protocols
BIRD 1.6.6 ready.
name proto table state since info
kernel1 Kernel master up 02:36:02
device1 Device master up 02:36:02
direct1 Direct master up 02:36:02
peer2 BGP master up 02:46:29 Established
peer3 BGP master up 02:36:06 Established

通过停止peer2上的bird守护程序,我们使端口179上的TCP连接在peer1peer2之间关闭。这样做会将我们的对等会话从Established更改为Connect。这两个状态对应于许多BGP状态中的两个,但是出于本教程的考虑,我们将仅关注Established,并将所有其他值视为未建立。对于那些更好奇的人,可以在Wikipedia上有关BGP的文章中找到有关会话状态的更多信息。

配置BGP会话

尽管现在知道了如何检查会话是否已经建立,但了解这些会话的配置也很重要。为此,我们需要深入研究bird配置文件。让我们看一下peer1上的/etc/bird下的配置文件。

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
root@peer1:~# cat /etc/bird/bird.conf
router id 10.0.0.10;

protocol kernel {
metric 0;
import none;
learn;
export all;
}

protocol device {
}

protocol direct {
}

protocol bgp peer2 {
local as 64512;
neighbor 10.0.0.11 as 64513;
import all;
export all;
}

protocol bgp peer3 {
local as 64512;
neighbor 10.0.100.11 as 64514;
import all;
export all;

我们可以看到建立这些初始会话所需的配置非常少。为了更深入地了解这项工作的真正作用,我们将专注于一个特定块:bgp peer2

1
2
3
4
5
6
protocol bgp peer2 {
local as 64512;
neighbor 10.0.0.11 as 64513;
import all;
export none;
}

在本教程的前面,我们讨论了eBGP和iBGP之间的区别以及大型AS如何使用唯一的ASN标识自己。但是,一小部分可用的ASN已保留给专用iBGP使用。这个范围是64512-65534。知道了这一点,我们可以看到我们已经将私有范围中的ASN分配给了peer2peer1被分配了ASN 64512。

查看下一条语句,我们可以看到具有IP和附加AS的邻居语句。该IP对应于我们尝试与之建立会话的主机或BGP术语中的邻居,而64513对应于我们分配给peer2主机的AS。可以通过查看peer2上的配置文件来确认这一点。

1
2
3
4
5
6
root@peer2:/# grep -A4 peer1 /etc/bird/bird.conf
protocol bgp peer1 {
local as 64513;
neighbor 10.0.0.10 as 64512;
export none;
}

协议BGP块中的这两个指令处理会话的初始建立。

虽然建立和维护会话对于BGP的运行至关重要,但仅建立会话无法路由任何流量。在下一节中,我们将探讨配置文件中的其他一些元素,以及如何使用它们来发现和宣布节点之间的路由。在继续进行此操作之前,先回顾一下我们当前的拓扑。

当前,我们的网络中有三个节点,peer1(AS64512),peer2(AS64513)和peer3(AS64514)。这些配置在同一广播域中,但是对等的结构类似于peer3 <-> peer1 <-> peer2。这种结构允许通过我们的路由服务器peer1peer2peer3进行路由通信。在继续进行本教程的下一步,即发布路由时,请牢记此拓扑。

IP地址 节点名 AS号
10.0.0.10 peer1 64512
10.0.0.11 peer2 64513
10.0.100.11 peer3 64514

BGP发布路由

内核协议

在开始发布Bird守护程序之间的路由之前,我们应该首先了解BIRD如何在Linux内核和BIRD守护程序之间传递路由。这就是我们前面看到的内核协议块起作用的地方。

1
2
3
4
5
6
protocol kernel {
metric 0;
learn;
import none;
export all;
}

kernel块中可以指定许多选项,并且可以在此处找到关于这些选项的更多信息,但是我们要执行的大部分操作由导入/导出定义。

1
import none;

告诉BIRD不要将路由从内核路由表中读取到BIRD中。我们将通过直接协议(即配置)获取路由。

1
export all;

告诉BIRD将其他公告了解的所有路由导出到内核的路由表中。这使我们可以实际利用此主机上的任何已获取的路由。

1
metric 0;

度量标准值由内核用来确定路由的优先级,并选择优先级最低的路由。在这种情况下,我们将其设置为0或未定义,以便我们首选本地路由。

1
learn;

最后,我们将设置学习指令,该指令将允许其他守护程序从内核路由表中学习路由。

发现直接路由

现在,我们已经配置了BIRD守护程序以将路由直接推送到内核路由表,我们将需要配置对等端以发现本地直接路由。由于我们会将这些路由直接添加到我们的环回接口,因此在选择的编辑器中,将直接协议配置为仅使用lo接口。

1
2
3
4
5
6
ncatelli@ofet> grep -A2 direct conf/peer2/etc/bird/bird.conf
protocol direct {
interface "lo";
}
ncatelli@ofet> docker-compose restart peer2
Restarting bird_examples_peer2_1 ... done

由于我们的网络上也有peer3,因此在此主机上进行相同操作,以防止宣布其他任何路由。

1
2
3
4
5
6
ncatelli@ofet> grep -A2 direct conf/peer3/etc/bird/bird.conf
protocol direct {
interface "lo";
}
ncatelli@ofet> docker-compose restart peer3
Restarting bird_examples_peer3_1 ... done

此时,除了默认的10.0.0.0子网(可以使用birdc进行验证)以外,我们将没有其他路由学习和发布。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@peer2:/# birdc show route all
BIRD 1.6.6 ready.
10.0.0.0/24 via 10.0.0.10 on eth0 [peer1 03:05:02] ! (100) [AS64512i]
Type: BGP unicast univ
BGP.origin: IGP
BGP.as_path: 64512
BGP.next_hop: 10.0.0.10
BGP.local_pref: 100
10.0.100.0/24 via 10.0.0.10 on eth0 [peer1 03:05:02] * (100) [AS64512i]
Type: BGP unicast univ
BGP.origin: IGP
BGP.as_path: 64512
BGP.next_hop: 10.0.0.10
BGP.local_pref: 100

过滤引用和导出

与内核模块类似,导出和导入可用于控制BGP对等方导入和导出的内容。首先,我们探讨过滤的概念以及如何将其用于控制将宣布或导出哪些路由。

BIRD中的过滤器基本上是在路由上执行的函数,返回接受或拒绝。这使我们能够应用一种简单的编程语言来为我们的路由策略添加逻辑。过滤器可以包含任何内容,从单个语句到非常复杂的逻辑。首先,让我们将none和all指令重新实现为过滤器,然后将它们添加到include指令上方的bird.conf文件中。

1
2
3
filter accept_all {
accept;
};
1
2
3
filter reject_all {
reject;
};

现在我们已经有了过滤器,让我们在我们的协议块之一的导入/导出指令中实现它们。在主机peer1上,让我们看一下peer2块。

1
2
3
4
5
6
protocol bgp peer2 {
local as 64512;
neighbor 10.0.0.11 as 64513;
import filter accept_all;
export filter accept_all;
}

从功能上讲,这与我们的原始配置相同,但是现在我们可以使用进一步的逻辑扩展这些设置。通过进一步研究过滤器脚本语言,可以了解这些过滤器的功能。为了扩展我们所学的知识,让我们在peer2上的bird.conf中创建一个过滤器,以控制要向peer1发布的路由。

1
2
3
4
5
6
filter export_subnets {
if net ~ [ 192.168.5.5/32 ] then {
accept;
}
reject;
}

最后,我们需要在peer2上更新peer1以使用此导出过滤器。

1
2
3
4
5
6
root@peer2:/# grep -A4 peer1 /etc/bird/bird.conf
protocol bgp peer1 {
local as 64513;
neighbor 10.0.0.10 as 64512;
export filter export_subnets;
}
1
2
3
ncatelli@ofet> docker-compose restart peer1 peer2
Restarting bird_examples_peer2_1 ... done
Restarting bird_examples_peer1_1 ... done

发布路由

现在,我们有了开始发布peer1peer2之间的路由所需的所有构造块。在此之前,让我们回顾一下我们所做的事情。首先,我们已将BIRD守护程序配置为使用我们的内核协议在其内部路由表和内核路由表之间进行通信。我们已将BIRD守护程序配置为从具有直接协议的环回接口中学习路由。我们还配置了peer1从其他对等节点导入路由并导出这些路由。最终,我们将peer2配置为仅使用export_subnets过滤器将192.168.5.5/32导出到peer1。但是,目前我们还没有从peer2通告到peer1的路由。

1
2
3
4
root@peer1:/# ip route  
default via 10.0.0.1 dev eth0
10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.10
10.0.100.0/24 dev eth1 proto kernel scope link src 10.0.100.10

此时,所有设置已经完成,因此可以从环回接口中学习路由。通过将IP添加到peer2的环回接口上,我们应该能够看到路由的发布。

1
root@peer2:/# ip a add 192.168.5.5/32 dev lo

现在,如果我们同时查看peer1上的birdc和内核路由表,我们应该可以看到peer1上新IP的路由。

1
2
3
4
5
6
root@peer1:~# ip route
default viia 10.0.2.2 dev eth0
10.0.0.0/24 dev eth1 proto kernel scope link src 10.0.0.10
10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15
10.0.100.0/24 dev eth2 proto kernel scope link src 10.0.100.10
192.168.5.5 via 10.0.0.11 dev eth1 proto bird
1
2
3
4
5
6
7
8
9
10
11
12
root@peer1:/# birdc show route all
BIRD 1.6.6 ready.
10.0.0.0/24 dev eth0 [direct1 03:10:33] * (240)
Type: device unicast univ
10.0.100.0/24 dev eth1 [direct1 03:10:33] * (240)
Type: device unicast univ
192.168.5.5/32 via 10.0.0.11 on eth0 [peer2 03:12:39] * (100) [AS64513i]
Type: BGP unicast univ
BGP.origin: IGP
BGP.as_path: 64513
BGP.next_hop: 10.0.0.11
BGP.local_pref: 100

ping将显示我们现在可以从peer1向该主机发送流量。

1
2
3
4
5
6
7
root@peer1:/# ping -c 1 192.168.5.5
PING 192.168.5.5 (192.168.5.5) 56(84) bytes of data.
64 bytes from 192.168.5.5: icmp_seq=1 ttl=64 time=0.135 ms

--- 192.168.5.5 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.135/0.135/0.135/0.000 ms

现在可以看到这些路由已通过peer1通告到peer3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@peer3:/# birdc show route all
BIRD 1.6.6 ready.
10.0.0.0/24 via 10.0.100.10 on eth0 [peer3 03:10:37] * (100) [AS64512i]
Type: BGP unicast univ
BGP.origin: IGP
BGP.as_path: 64512
BGP.next_hop: 10.0.100.10
BGP.local_pref: 100
10.0.100.0/24 via 10.0.100.10 on eth0 [peer3 03:10:37] ! (100) [AS64512i]
Type: BGP unicast univ
BGP.origin: IGP
BGP.as_path: 64512
BGP.next_hop: 10.0.100.10
BGP.local_pref: 100
192.168.5.5/32 via 10.0.100.10 on eth0 [peer3 03:12:38] * (100) [AS64513i]
Type: BGP unicast univ
BGP.origin: IGP
BGP.as_path: 64512 64513
BGP.next_hop: 10.0.100.10
BGP.local_pref: 100
1
2
3
4
5
6
7
root@peer3:/# ping -c 1 192.168.5.5
PING 192.168.5.5 (192.168.5.5) 56(84) bytes of data.
64 bytes from 192.168.5.5: icmp_seq=1 ttl=63 time=0.082 ms

--- 192.168.5.5 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.082/0.082/0.082/0.000 ms
1
2
3
4
root@peer3:/# traceroute 192.168.5.5
traceroute to 192.168.5.5 (192.168.5.5), 64 hops max
1 10.0.100.10 0.005ms 0.003ms 0.003ms
2 192.168.5.5 0.003ms 0.003ms 0.003ms

我们也可以通过查看AS PATH来了解情况。通过查看与birdc中的路由关联的AS PATH,可以看到从64513到64512的路由在到达peer3之前发布。

因为将peer1配置为将路由导出到peer3,并且由于peer3配置为从peer1导入路由,所以我们能够将此路由获取到peer3的BIRD路由表中。然后,由于我们已将内核协议配置为在BIRD中导出路由,因此这些路由会将其放入peer3的内核路由表中。

下一步

在这个简单的教程中,我们探讨了许多概念,但是,我们几乎没有涉及bird的概念。随时使用此示例,以发布和过滤路由的方式进一步探索。在以后的教程中,我们将更深入地探讨BGP的工作方式以及用于确定路由的过程,包括如何通信和本地优先级以及BGP守护程序如何使用它们来选择通往服务器的最佳路径。我们还将探讨什么是任播IP,以及如何使用BGP配置高可用性,以及如何使用过滤策略代替直接接口策略来控制向每个节点声明哪些前缀。BGP可以为我们提供对网络拓扑的大量控制,并且了解如何使用BGP可以更好地调整网络以适应自己的需求。

在构建裸机Kubernetes集群时,您可能会遇到一个常见问题,就像我在做的那样,除了使用NodePort之外,您真的不知道如何向Internet公开Kubernetes服务。如果使用的是NodePort服务类型,它将分配一个要打开的较大端口号,并且您必须允许防火墙规则连接到这些端口。这对您的基础架构不利,尤其是在将服务器暴露于外部Internet时。好吧,还有另一种简洁的方法可以将您的Kubernetes服务公开出去,并且使用服务的原始端口号。例如,您可以将Kubernetes群集中部署的MySQL服务通过3306而不是32767端口暴露给外界。答案是使用Kubernetes External IP服务类型

就个人而言,我发现在Kubernetes社区中并未广泛讨论此主题,这可能是因为许多人正在使用云提供商的负载均衡器或将Metal LB用于本地部署。

什么是External IP服务

Kubernetes官方文档中可以看到External IP的描述:

如果存在路由到一个或多个集群节点的外部IP,则Kubernetes Services可以在这些External IP上公开。在服务端口上使用外部IP(作为目标IP)进入群集的流量将被路由到服务端点之一。External IP不受Kubernetes的管理,由集群管理员负责。

对于大多数人来说,这种解释是可以理解的。这里最重要的是确保使用哪个IP来访问Kubernetes集群。使用External IP服务类型,我们可以将服务绑定到用于连接集群的IP。

如果您了解Kubernetes网络的工作方式,那将是很好的。如果您不熟悉它,请查看Mark Betz撰写的博客文章,以详细了解它们。这里最重要的是要知道Kubernetes网络与Overlay网络一起工作。这意味着一旦您到达群集中的任何节点(主节点或工作节点),您就可以虚拟访问群集中的所有节点。

下图就是他们的组网图:

kubernetes-external-ip

在上图中,节点1和节点2都有1个IP地址。节点1上的IP地址1.2.3.4绑定到实际Pod驻留在节点2中的httpd服务,而IP地址1.2.3.5绑定到实际Pod驻留在节点1中的nginx服务。底层的overlay网络使这成为可能。当我们curl 1.2.3.4时,应该看到来自httpd服务的响应,而curl 1.2.3.5时,则应该看到来自nginx服务的响应。

为什么不使用Ingress

即使Ingress也用于将服务公开给外部,但Ingress是为L7路由构建的。这意味着它构建为支持HTTP(端口80)和/或HTTPS(端口443)流量,而不支持其他端口。Ingress充当基于主机的路由,或类似于Web Server中的虚拟主机。一些能够为其他端口提供服务的ingress controllers,或者可能为L4路由提供了解决方法,但我从未真正尝试使用它们。

External IP的优缺点

使用External IP的优点是:

  • 您可以完全控制所使用的IP。您可以使用属于您的ASN的IP,而不要使用云提供商的ASN。

外部IP的缺点是:

  • 我们现在将要进行的简单设置并不是高可用的。这意味着,如果节点异常,则该服务将不再可用,您将需要手动修复该问题。
  • 管理IP需要做一些手工工作。IP不是为您动态配置的,因此需要人工干预。

如何使用External IP服务

同样,我们将使用与我们的群集设置相同的组网图,不同的是IP地址和主机名不同。这不是一个好例子,但是当我们验证设置时,很容易区分是哪个。在实际示例中,您可能希望在一个外部IP上公开MySQL DB,在另一个外部IP上公开Kafka群集。

kubernetes-external-ip-demo

我已为本教程配置了2个VM。k3s-external-ip-master将是我们的Kubernetes master节点,其IP为1.2.4.120。k3s-external-ip-worker将是Kubernetes worker节点,其IP为1.2.4.114。

步骤1:部署Kubernetes集群

让我们在主节点上安装k3s,然后让另一个节点加入集群。

1
2
$ k3sup install --ip <master node ip> --user <username>
$ k3sup join --server-ip <master node ip> --ip <worker node ip> --user <username>

您现在应该会看到类似的内容:

1
2
3
4
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k3s-external-ip-master Ready master 7m24s v1.16.3-k3s.2
k3s-external-ip-worker Ready <none> 2m21s v1.16.3-k3s.2

步骤2:创建Kubernetes Deployment资源

我们将创建nginx和httpd资源。

1
2
$ kubectl create deployment nginx --image=nginx
$ kubectl create deployment httpd --image=httpd

你现在应该看到这个:

1
2
3
4
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-86c57db685-fzxn5 1/1 Running 0 22s
httpd-7bddd4bd85-zk8ks 1/1 Running 0 16s

步骤3:将Deployment公开为External IP类型

让我们创建Nginx服务的yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ cat << EOF > nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
externalIPs:
- 1.2.4.114
EOF

创建httpd服务的yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ cat << EOF > httpd-service.yaml
apiVersion: v1
kind: Service
metadata:
name: httpd-service
spec:
selector:
app: httpd
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
externalIPs:
- 1.2.4.120
EOF

使用kubectl命令创建2个服务的yaml:

1
2
$ kubectl create -f nginx-service.yaml
$ kubectl create -f httpd-service.yaml

现在您的Kubernetes服务应该如下所示:

1
2
3
4
5
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 18m
httpd-service ClusterIP 10.43.240.149 1.2.4.120 80/TCP 32s
nginx-service ClusterIP 10.43.13.149 1.2.4.114 80/TCP 26s

您可能会在此处看到服务类型为ClusterIP。我不确定为什么它不显示“外部IP”。

k8s官网有说明,External IP与type无关

步骤4:瞧!

让我们curl httpd服务,您应该看到Apache默认页面。

1
2
3
4
5
6
7
8
9
10
$ curl -i 1.2.4.120
HTTP/1.1 200 OK
Date: Fri, 20 Dec 2019 03:36:23 GMT
Server: Apache/2.4.41 (Unix) <------
Last-Modified: Mon, 11 Jun 2007 18:53:14 GMT
ETag: "2d-432a5e4a73a80"
Accept-Ranges: bytes
Content-Length: 45
Content-Type: text/html
<html><body><h1>It works!</h1></body></html>

接下来,curl nginx服务,您应该看到nginx默认页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ curl -i 1.2.4.114
HTTP/1.1 200 OK
Server: nginx/1.17.6 <------
Date: Fri, 20 Dec 2019 03:36:01 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 19 Nov 2019 12:50:08 GMT
Connection: keep-alive
ETag: "5dd3e500-264"
Accept-Ranges: bytes
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
....

下一步是什么

浮动IP

如今,大多数云提供商都提供浮动IP服务。浮动IP允许您拥有1个IP,并将该IP动态分配给所需的任何IP。在这种情况下,可以将IP分配给Kubernetes集群中的任何工作节点。

在DigitalOcean(我相信其他提供商也允许这样做)中,您可以使用API调用将IP重新分配给其他VM。这意味着您可以在其他VM发生故障时迅速将其主动重新分配给其他VM,或者可以定期轮换IP。

kubernetes-floating-ip

从图中可以看到,我们有1个浮动IP 1.2.3.6,该IP首先分配给节点1,当节点1不可用时将切换到节点2。IP 1.2.3.6适用于我们的MySQL服务,并将放入我们的应用程序配置中。

我尚未尝试此设置,因此无法确认它是否有效。我将在以后的博客文章中更新结果。

任播IP

anycast

您可以将Anycast IP用作外部IP,以便它们具有高可用性。对于不熟悉Anycast IP的用户,这意味着1个IP可能会路由到2个或更多服务器。你可以在这里阅读更多。就个人而言,我不确定如何设置此设置。但是,我认为这在技术上是可行的。我认为这是运行外部IP服务的最佳方法。

结论

您可以通过很多选项为裸机Kubernetes集群获取IP。例如,您可以为此使用Inlets和MetalLB。此设置可能不是您的组织需要的最合适的设置。但是,很高兴知道如何使用此方法。

免责声明:我仅将其用于实验和测试,而本文不适用于生产。如果您打算在生产中使用它,请咨询您的解决方案架构师或CTO。

访问Kubernetes集群中托管的服务的两种最常见方法是通过IngressLoad Balancer。对于公有云用户来说,这些是访问服务的简单有效方法。云托管的控制器可以完成分配公共IP,设置负载平衡和管理SSL的繁重工作。

在本地运行私有托管的Kubernetes集群的运营商将很快意识到,将服务公开提供比在公有云上更为复杂。服务将在公共互联网上还是仅对本地用户公开?应该如何设置Ingress,负载平衡,IP分配和SSL管理?在维护安全性的同时将服务公开发布的最有效方法是什么?

Calico项目通过其服务IP发布功能为某些问题提供了答案,该功能与现有的机架(ToR)基础架构集成在一起,可以提供Kubernetes服务IP或Kubernetes外部服务IP的路由。在以下情况下,服务IP发布是一个很好的解决方案:

  • 您的ToR解决方案能够运行边界网关协议(BGP)
  • 您希望将服务从群集共享到网络基础结构的其余部分
  • 您想利用网络负载均衡

为了启用服务IP发布功能,Calico需要与BGP路由器建立对等关系,该路由器在Calico的内部路由器之外,但在网络本地。BGP是网络中使用的最基本的路由协议之一。在较高级别,BGP通过在信任对等方之间共享路由来工作。与ToR对等时,Calico共享Kubernetes服务的路由,这使它们可用于整个网络。

为了帮助讨论此功能,假定我们安装以下方式配置了网络和群集:

  • 托管Kubernetes节点的服务器机架通过顶部机架式路由器连接到物理网络
  • Calico作为CNI和网络策略插件运行
  • Calico和Rack顶部路由器配置为使用BGP对等

在此设置中,我们具有以下网络配置:

  • 机架顶部路由器的IP地址是192.168.1.1,服务器的IP地址是从192.168.1.0/24分配的
  • Kubernetes Pod网络配置了CIDR 10.48.0.0/16
  • Kubernetes服务集群IP范围配置为10.49.0.0/16
  • 外部服务IP范围配置为192.168.3.0/24

您的ToR BGP路由器的确切配置超出了本文的范围,并且会因您使用的供应商或软件包而异。如果您没有可用的服务器机架,但仍然想尝试该功能,那么在单独的服务器上运行的Bird Internet Routing Daemon(BIRD)是尝试进行操作的不错选择。

第一步是启用ToR和Calico网络之间的对等连接。其工作方式将根据您的ToR实现而有所不同,但是要牢记一些关键事项:

  • 必须将ToR路由器配置为与在每个节点上运行的Calico对等
  • ToR需要接受来自外部网络,外部服务网络和Pod服务网络的路由和流量
  • 如果可以选择,ToR应该启用正常重启,以防止网络服务中断

将ToR配置为接受路由后,下一步是在Calico端启用对等。首先通过以下清单告诉Calico有关外部BGP路由器的信息:

1
2
3
4
5
6
7
8
9
calicoctl apply -f - << EOF
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
name: bgppeer-global-64512
spec:
peerIP: 192.168.1.1
asNumber: 64512
EOF

启用对等功能后,Calico可以使Pod成为网络中的头等公民,而无需覆盖网络,并使他们可以直接在群集外部访问。

尽管启用了对等连接,但是Calico仍需要进一步配置以公开Kubernetes服务IP范围。这可以通过创建新的Calico BGPConfiguration资源来完成:

1
2
3
4
5
6
7
8
9
calicoctl create -f - <<EOF
apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
name: default
spec:
serviceClusterIPs:
- cidr: 10.49.0.0/16
EOF

启用服务群集IP范围的发布后,ToR上的路由表将如下所示:

1
2
3
4
5
6
7
$ ip r
...
10.49.0.0/16
nexthop via 192.168.1.10 dev eth2 weight 1
nexthop via 192.168.1.11 dev eth2 weight 1
nexthop via 192.168.1.12 dev eth2 weight 1
...

注意到10.49.0.0/16网络的路由如何在节点之间实现ECMP负载平衡。您公开的所有服务将由ToR在所有节点上进行负载平衡。为了说明这一点,我们可以创建一个基本的Nginx服务。

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
control:~$ kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
EOF
control:~$ kubectl expose nginx
control:~$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.49.0.1 <none> 443/TCP 19m
nginx ClusterIP 10.49.62.131 <none> 80/TCP 4m43s

现在,您可以从外部网络通过其内部群集IP地址访问Kubernetes托管服务。

1
2
3
4
5
6
external:~$ curl 10.49.62.131
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

这里有两个阶段的负载平衡。首先,ToR通过将其路由到节点来对与群集IP的连接进行负载平衡。然后,使用NAT(网络地址转换)在该节点上运行的kube-proxy负载均衡到特定的Pod,以将目标IP从群集IP更改为后备Pod之一的IP地址。后备Pod可能在本地节点上,也可能在其他节点之一上,从而导致了另一个网络跃点。

如果我们想避免额外的潜在网络跳数,可以通过将服务上的外部流量策略设置为本地来实现。

1
2
control:~$ kubectl patch service nginx \
-p '{"spec":{"type": "NodePort", "externalTrafficPolicy":"Local"}}'

现在,在ToR上,您将看到路由表的新增内容,包括Nginx服务到正在运行Nginx的特定节点的ECMP负载平衡:

1
2
3
4
5
6
7
8
9
10
$ ip r
...
10.49.0.0/16
nexthop via 192.168.2.10 dev eth2 weight 1
nexthop via 192.168.2.11 dev eth2 weight 1
nexthop via 192.168.2.12 dev eth2 weight 1
...
10.49.62.131
nexthop via 192.168.2.11 dev eth2 weight 1
nexthop via 192.168.2.12 dev eth2 weight 1

这是公开Kubernetes网络服务的简便方法,但是在操作上,它具有将Kubernetes群集中该IP范围内的每个服务都暴露给网络其余部分的缺点。如果您想更精细地控制提供哪些服务,或者想要分配真正面向公众的IP地址,Calico可以通过发布外部服务IP来解决这个问题。该方法类似,主要区别在于外部IP不由Kubernetes集群管理,必须手动分配给服务。

下一个示例将说明这一点。首先重新配置BGPConfiguration,以发布外部IP而不是内部IP(值得注意的是,您可以一次公开这两个集合,但是在此示例中,我们要关闭对内部网络的公共访问,同时仍提供对应用程序的访问)。

1
2
3
4
5
6
7
8
9
10
control :~$calicoctl create -f - <<EOF
apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
name: default
spec:
serviceClusterIPs:
serviceExternalIPs:
- cidr: 192.168.3.0/24
EOF

检查路由时,请注意已删除到群集IP范围的路由,并已添加到外部服务网络的路由。

1
2
3
4
5
6
7
$ ip r
...
192.168.3.0/24
nexthop via 192.168.1.10 dev eth2 weight 1
nexthop via 192.168.1.11 dev eth2 weight 1
nexthop via 192.168.1.12 dev eth2 weight 1
...

该服务的群集IP不再公开可见。

1
2
external :~$ curl -m 10 10.49.62.131
curl: (28) Connection timed out after 10001 milliseconds

我们现在在发布外部IP范围,但我们还需要为服务分配一个外部IP:

1
2
3
4
5
6
control:~$ kubectl patch svc nginx \
-p '{"spec": {"externalIPs": ["192.168.3.180"]}}'
control:~$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.49.0.1 <none> 443/TCP 152m
nginx NodePort 10.49.62.131 192.168.3.180 80:31890/TCP 109m

检查连接性:

1
2
3
4
5
6
external:~$ curl 192.168.3.180
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

到此为止,我们快速完成了Calico如何在Kuberentes群集之外发布服务和外部服务IP的快速浏览。对于具有启用BGP路由的本地云,这是一种简单的解决方案,无需提供安装和维护自定义Kubernetes负载均衡器或Ingress控制器的额外工作即可访问Kubernetes服务。如果您想了解更多有关此功能的信息,请查阅官方的Calico项目”Advertise Kubernetes Service IPs“。