基于 Docker 环境及 Calico 网络的 Consul + Consul Template + Registrator + nginx 容器服务注册和发现

前言

由于公司的基础环境为 Docker,有项目需要做 ZooKeeper 或者 Spring Cloud 的服务注册,服务注册的 IP 及端口均为 Docker 内部提供,所以需要搭建 Calico 网路来实现宿主机访问。
另测试环境的服务均为 IP + 端口的方式访问,一旦更换宿主机则导致无法调用的情况,所以需要配置域名来访问,而测试环境发布频繁,所以需要服务能自动注册及发现来生成域名。

介绍

简介

  • Docker+Calico+etcd:详见 Calico 搭建篇
  • Consul:服务注册、服务发现、键值存储、及健康检查
  • Consul Template:订阅 Consul 服务注册中心的服务消息,来生成 nginx 配置及操作 nginx(reload)
  • Registrator:注册器,将宿主机上提供端口服务的容器信息自动在 Consul 上自动注册或注销

架构图

实验架构网络

Markdown

Consul 架构图

avatar

Calico 架构图

Markdown

环境准备

机器及组件信息

主机名 IP 系统 组件
W708-ATMQZLPR-1 172.29.150.202 Centos 7.2 consul-Server,registrator,consul-template,nginx-ingress,nginx-host
W708-ATMQZLPR-2 172.29.150.203 Centos 7.2 consul-Server,registrator,consul-template,nginx-ingress,nginx-host
W708-ATMQZLPR-3 172.29.150.204 Centos 7.2 consul-Server,registrator,consul-template,nginx-ingress,nginx-host
w708-payyhuat-3 172.29.150.199 Centos 7.2 consul-client,registrator

前三台为 Server 服务器,实际上不需要安装 Registrator,此为测试环境,后面的实验过程也是在前 2 台上实现。

Docker + Calico 配置

具体配置详见 Calico 搭建篇
注意:

  1. 这里要说明下,因为后期需要固定 nginx-ingress 的容器 IP 地址。实验在创建 Calico 网络之后会有个 BUG,即 2.6.2 之后版本在创建指定 IP 的容器的时候会报错,所以 Calico 的版本必须为 <=2.6.2。
  2. 因为需要针对 nginx-ingress 容器单独做端口打通,所以需要在 Calico 的 calico.env 和 calico-node.service 添加参数 CALICO_LIBNETWORK_LABEL_ENDPOINTS=true,允许 Calico 策略读取 Docker 容器的 label 标签。

Server 端搭建配置

以下所有的组件安装均为 Docker 容器

一、consul-server

1.1 概述

Consul 是 Google 开源的一个使用 Go 语言开发的服务发现、配置管理中心服务。内置了服务注册与发现框架、分布一致性协议实现、健康检查、Key/Value 存储、多数据中心方案,不再需要依赖其他工具(比如 ZooKeeper 等)。服务部署简单,只有一个可运行的二进制的包。每个节点都需要运行 Agent,他有两种运行模式 Server 和 Client。每个数据中心官方建议需要 3 或 5 个 Server 节点以保证数据安全,同时保证 server-leader 的选举能够正确的进行。

  • Client
    Client 表示 Consul 的 Client 模式,就是客户端模式。是 Consul 节点的一种模式,这种模式下,所有注册到当前节点的服务会被转发到 Server,本身是不持久化这些信息。
  • Server
    Server表示 Consul 的 Server 模式,表明这个 Consul 是个 Server,这种模式下,功能和 Client 都一样,唯一不同的是,它会把所有的信息持久化的本地,这样遇到故障,信息是可以被保留的。
  • server-leader
    中间那个 Server 下面有 LEADER 的字眼,表明这个 Server 是它们的老大,它和其它 Server 不一样的一点是,它需要负责同步注册的信息给其它的 Server,同时也要负责各个节点的健康监测。
  • Raft
    server 节点之间的数据一致性保证,一致性协议使用的是 Raft,而 ZooKeeper 用的 Paxos,etcd 采用的也是 Raft。
  • 服务发现协议
    Consul 采用 HTTP 和 DNS 协议,etcd 只支持 HTTP。
  • 服务注册
    Consul 支持两种方式实现服务注册,一种是通过 Consul 的服务注册 HTTP API,由服务自己调用 API 实现注册,另一种方式是通过json个是的配置文件实现注册,将需要注册的服务以 JSON 格式的配置文件给出。Consul 官方建议使用第二种方式。
  • 服务发现
    Consul 支持两种方式实现服务发现,一种是通过 HTTP API 来查询有哪些服务,另外一种是通过 Consul agent 自带的 DNS(8600 端口),域名是以 NAME.service.consul 的形式给出,NAME 即在定义的服务配置文件中,服务的名称。DNS 方式可以通过 check 的方式检查服务。
  • 服务间的通信协议
    Consul 使用 Gossip 协议管理成员关系、广播消息到整个集群,他有两个 Gossip pool(LAN pool 和 WAN pool),LAN pool 是同一个数据中心内部通信的,WAN pool 是多个数据中心通信的,LAN pool 有多个,WAN pool 只有一个。

1.2 安装配置

下载镜像

docker pull docker.io/consul:latest

创建 Consul 配置

vim /opt/platform/consul/server.json
{
    "datacenter": "quark-consul",
    "data_dir": "/consul/data",
    "server": true,
    "ui": true,
    "bind_addr": "172.29.150.202",
    "client_addr": "127.0.0.1",
    "bootstrap_expect": 3,
    "retry_join": ["172.29.150.202","172.29.150.203","172.29.150.204"],
    "retry_interval": "10s",
    "rejoin_after_leave": true,
    "skip_leave_on_interrupt": true
}

1.3 配置说明

官方在启动容器的时候是将一部分配置作为 docker run 的参数,而我追求简洁以及配置落地,所以都写到了配置文件里了。

  • datacenter:数据中心名称(库名)
  • data_dir:数据存储目录
  • server:运行在server模式
  • ui:使用UI界面
  • bind_addr:内部集群通信绑定的地址。默认是0.0.0.0,如果有多块网卡,需要指定,否则启动报错
  • client_addr:客户端接口绑定的地址,默认是127.0.0.1
  • bootstrap_expect:集群预期的 Server 个数,这里我们有 3 台 Server,设置为 3;不能和 bootstrap 参数一同使用。这里需要说明下:bootstrap_expect 指定的个数代表了 Server 加入到集群之后,集群中的机器必须 =>3 的情况才开始选举 server-leader,而如果配置 bootstrap 的节点,则启动的时候默认自己为 leader。
  • retry_join:重新加入集群
  • retry_interval:重试时间
  • rejoin_after_leave:在离开集群之后才重试加入
  • skip_leave_on_interrupt:在启动后,是否 Ctrl+C 优雅退出,我们是容器模式,所以不用管,直接 true 就好了。

额外配置说明(可有可无)

  • start_join:这个参数是在启动的时候选择加入到哪个集群,但是官方推荐 retry_join 取代他,因为效果更好。
  • enable_syslog:启用则 Consul 的日志会写进系统的 syslog 里,但是如果是在 Windows 上配置改参数就会报错。
  • enable_script_checks:是否启用监控检测脚本,这里没有对 Consul 集群做监控,略。

默认端口说明

  • 8300:Consul agent 服务 relplaction、rpc(client-server)
  • 8301:LAN Gossip
  • 8302:WAN Gossip
  • 8500:HTTP API 及 UI 端口
  • 8600:DNS服务端口

1.4 启动 consul-server

docker run -d \
--net=host \
--name consul \
-v /opt/platform/consul/config:/consul/config \
-v /opt/platform/consul/data:/consul/data \
consul agent

启动参数说明

这里需要将 config 和 data 挂载出来,以防容器删除的同时,数据被一并删除

1.5 命令

查看集群成员

docker exec consul consul member

查看节点身份

docker exec consul consul info

其他参数自己摸索吧,docker exec consul consul 能看到 help 信息

1.6 Web 访问

浏览器访问http://172.29.150.202:8500/ui

只要是 Server 端开启了 UI,任何一个 Server 都能访问 Web 界面。

二、Consul Template + nginx-ingress

2.1 概述

Consul Template 是 HashiCorp 基于 Consul 所提供的可扩展的工具,通过监听 Consul 中的数据变化,动态地修改一些配置文件中地模板。常用于在 Nginx、HAProxy 上动态配置健康状态下的客户端反向代理信息。
Consul Template 和 nginx 必须安装在同一台机器上,因为 Consul Template 需要动态修改 nginx 的配置文件 nginx.conf,然后执行 nginx -s reload 命令进行路由更新,达到动态负载均衡的目的。
nginx-ingress 是我自己命名的(实现原理跟 k8s 的 ingress 一样),因为此 nginx 主要是负责访问 Calico 网络内的负载均衡,且 Calico 不支持 HTTP 协议的穿透,所以外部需要跟 ingress 通讯的话必须在中间再创建一个 nginx-host 作为转发,后面会介绍。

2.2 原理

  • 通过 Nginx 自身实现负载均衡和请求转发;
  • 通过 Consul-template 的 config 功能实时监控 Consul 集群节点的服务和数据的变化;
  • 实时的用 Consul 节点的信息替换 Nginx 配置文件的模板,并重新加载配置文件;

2.3 创建 nginx-consul-template 镜像

1. 下载 nginx 镜像
docker pull docker.io/nginx:latest
2. 创建 nginx 脚本

官方及网上大部分的启动 nginx-consul-template 容器最后 ENTRYPOINT 都为 nginx -s reload,但是因为在重制镜像的时候会将 nginx 镜像中 ENTRYPOINT 的 nginx -g 'daemon off' 给覆盖掉,导致容器在启动的时候 nginx 没有启动,而 nginx -s reload 会去读 /run/nginx.pid,如果没有则r eload 失败,所以这里新建了一个 nginx 启动及重启的脚本。

# vim nginx.sh

#!/bin/bash
if nginx -t>/dev/null; then
    if [[ -s /var/run/nginx.pid ]]; then
        nginx -s reload
        if [[ $? != 0 ]]; then
            rm -f /var/run/nginx.pid
            nginx -c /etc/nginx/nginx.conf
        fi
    else
        nginx -c /etc/nginx/nginx.conf
    fi
fi

这里做了 3 层判断,先检查 nginx 配置是否正确,然后查看检查 nginx.pid 是否存在且不为空。容器如果退出,会导致 nginx.pid 里面的 ID 号不对,再次启动 nginx 的时候,nginx -s reload 会报错,所以需要再判断 nginx -s reload 是否正确。

3. 创建 nginx-consul-template 的 docker file
# vim nginx-consul-template.df

FROM nginx
MAINTAINER Qingwen Zhang <qingwenzhang@quarkfinance.com>

RUN apt-get update && \
    apt-get install --no-install-recommends --no-install-suggests -y unzip && \
    rm -r /var/lib/apt/lists/*

ENV CONSUL_TEMPLATE_VERSION 0.19.4
ADD https://releases.hashicorp.com/consul-template/${CONSUL_TEMPLATE_VERSION}/consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip /tmp/consul-template.zip
ADD nginx.sh /tmp/nginx.sh
RUN chmod +x /tmp/nginx.sh
RUN unzip /tmp/consul-template.zip -d /usr/bin && \
    chmod +x /usr/bin/consul-template && \
    rm /tmp/consul-template.zip
RUN mkdir /etc/ctmpl
WORKDIR /etc/ctmpl

ENTRYPOINT ["/usr/bin/consul-template"]
4. 创建镜像
docker build -t 172.29.150.223:5000/nginx-consul-template -f /opt/dockerfile/nginx-consul-template.df .
5. 推送镜像到仓库
docker push 172.29.150.223:5000/nginx-consul-template

2.4 运行 nginx-consul-template

docker run -d \
--restart=always \
--net=calico \
--ip=10.233.49.100 \
--label org.projectcalico.label.role=nginx \
-v /opt/platform/nginx-calico/conf:/etc/nginx \
-v /opt/platform/nginx-calico/modules:/usr/lib/nginx/modules \
-v /opt/platform/nginx-calico/html:/usr/share/nginx/html \
-v /opt/platform/nginx-calico/logs:/var/log/nginx \
-v /opt/platform/nginx-calico/ctmpl:/etc/ctmpl \
--name=calico-nginx1-consul-template \
172.29.150.223:5000/nginx-consul-template \
-consul-addr=172.29.150.202:8500 -wait=5s \
-template="/etc/ctmpl/ctmpl:/etc/nginx/conf.d/app.conf:/tmp/nginx.sh"

启动参数说明

--ip:
nginx-host 反向代理的时候需要固定的 IP,所以这里需要指定 IP。
-v:
因为是 nginx 配置,所有需要将 nginx 可能需要保存的配置及日志等映射出来。
--label:
org.projectcalico.label.role 这里需要配置标签(label),Calico 的 policy 策略需要根据容器的label来做控制流量,后面有针对 policy 详述。
-consul-addr:
指定 Consul 地址。
-template:
ctmpl 为配置模板,app.conf 为生成的 nginx 配置,nginx.sh 为启动及重启 nginx 的脚本。

2.5 创建 ctmpl 模板

vim /opt/platform/nginx-calico/ctmpl
{{range services}}{{ if in .Tags "calico" }}{{$name := .Name}}{{$service := service .Name}}upstream {{$name}} {
    zone upstream-{{$name}} 64k;
{{range $service}}    server {{.Address}}:{{.Port}}  max_fails=3 fail_timeout=60 weight=1;
{{end}}}

server {
    listen 80;
    charset utf-8;
    server_name {{$name|toLower|split "-"|join "."}}.flyclock.cn;
    access_log  /var/log/nginx/{{.Name}}.log;
    location / {
        proxy_pass        http://{{$name}};
        proxy_set_header  Host            $host;
        proxy_set_header  X-Real-IP        $remote_addr;
        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_connect_timeout  10s;
        proxy_send_timeout      150s;
        proxy_read_timeout      150s;
        proxy_next_upstream error timeout invalid_header http_404 http_502 http_504 http_500;
    }
}
{{end}}{{end}}

模板说明

注意:registrator 在向 consul 注册的时候是把所有提供端口服务的容器信息都注册上去,包括了 host 及 bridge 网络下的容器,consul-template 会抓取 consul 上所有的服务信息,而 nginx-consul-template 是在 calico 网络,所以 nginx 只能反向代理到同网络下的服务。因此需要在开头的位置做一层判断过滤 {{ if in .Tags "calico" }},只生成反向代理到带有 calico tag 容器的配置,而 calico 网络下的容器在启动的时候也必须添加 -e SERVICE_TAG=calico,不然不会被解析nginx-ingress(calico网络)里面。

至于 ctmpl 的语法结构会在文末贴出官方 git 教程。

最终生成的 nginx 配置如下:

upstream nginx-test {
    zone upstream-nginx-test 64k;
    server 10.233.49.254:80  max_fails=3 fail_timeout=60 weight=1;
    server 10.233.61.129:80  max_fails=3 fail_timeout=60 weight=1;
}

server {
    listen 80;
    charset utf-8;
    server_name nginx.test.flyclock.cn;
    access_log  /var/log/nginx/nginx-test.log;
    location / {
        proxy_pass        http://nginx-test;
        proxy_set_header  Host            $host;
        proxy_set_header  X-Real-IP        $remote_addr;
        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_connect_timeout  10s;
        proxy_send_timeout      150s;
        proxy_read_timeout      150s;
        proxy_next_upstream error timeout invalid_header http_404 http_502 http_504 http_500;
    }
}

2.6 创建 Calico policy

Calico 网络有 2 种控制流量的方式,profile 跟 policy。
profile 设置思路是针对网络的,默认配置名称是跟创建网络名字一样。如果针对容器做策略,需要先创建策略,然后针对容器的网络 workloadEndpoint 来应用,而容器只要重启,则相应的workloadEndpoint 也会变化,所以不建议用 profile 来做。
policy 默认可以精细到具体容器,因为他是针对容器的 label 来做的,所以只需要在启动容器的时候添加 --label org.projectcalico.label.role 即容器的默认策略应用的是 policy 的。

vim nginx-policy.yaml
apiVersion: v1
kind: policy
metadata:
  name: allow-tcp-80
spec:
  selector: role == 'nginx'
  types:
  - ingress
  - egress
  ingress:
  - action: allow
    protocol: tcp
    source:
      nets:
        - 10.233.0.0/16
        - 172.0.0.0/8
    destination:
      ports:
      - 80
  egress:
  - action: allow

配置说明

ingress 是入口,egress 是出口,我们这里需要外部的 nginx-host 反向代理到 Calico 网络的 nginx-ingress,所以只需要 ingress 放行指定 IP 的 TCP 80 即可。

三、nginx-host

3.1 前言

由于 Calico 网络不支持 HTTP 协议,所以即使你在 iptables 中配置了 NAT 路由,将访问宿主机 80 端口的请求都转发到 nginx-consul-template,外部也无法访问 nginx-ingress。于是我们需要额外启动一个 nginx(docker host 网络),来做反向代理到 nginx-ingress。
另如果你需要不同网卡下面的网络通讯的话,你只需要在 iptables 中添加 NAT 条目即可:

iptables -t nat -N expose-ports
iptables -t nat -A OUTPUT -j expose-ports
iptables -t nat -A PREROUTING -j expose-ports
iptables -t nat -A expose-ports -p tcp --destination 192.0.2.1 --dport 80 -j DNAT --to 192.168.7.4:8080

3.2 运行 nginx-host

docker run -d \
--net=host \
-v /opt/platform/nginx-host/conf:/etc/nginx \
-v /opt/platform/nginx-host/modules:/usr/lib/nginx/modules \
-v /opt/platform/nginx-host/html:/usr/share/nginx/html \
-v /opt/platform/nginx-host/logs:/var/log/nginx \
--name=nginx-host nginx

3.3 配置 nginx

vim /opt/platform/nginx-host/conf/conf.d/default.conf
upstream nginx-calico {
    server 10.233.49.100:80;
}
server {
    listen 80;
    charset utf-8;
    server_name *.flyclock.cn;
    location / {
        proxy_pass        http://nginx-calico;
        proxy_set_header  Host            $host;
        proxy_set_header  X-Real-IP        $remote_addr;
        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_connect_timeout  10s;
        proxy_send_timeout      150s;
        proxy_read_timeout      150s;
        proxy_next_upstream error timeout invalid_header http_404 http_502 http_504 http_500;
    }
}

Client 端搭建配置

一、consul-client

Consul 的概念上面提过,不做复述

1.1 创建 client.json

vim /opt/platform/consul/config/client.json
{
    "datacenter": "quark-consul",
    "data_dir": "/consul/data",
    "server": false,
    "ui": false,
    "bind_addr": "172.29.150.199",
    "client_addr": "127.0.0.1",
    "bootstrap_expect": 0,
    "retry_join": ["172.29.150.202","172.29.150.203","172.29.150.204"],
    "retry_interval": "10s",
    "rejoin_after_leave": true,
    "skip_leave_on_interrupt": true
}

配置说明

这里 Client 不需要开启 UI 及 Server,所以均为 false,以及 bootstrap_expect 设为 0 不参与 leader 节点竞争。

1.2 启动 consul-client

docker run -d \
--net=host \
--name consul \
-v /opt/platform/consul/config:/consul/config \
-v /opt/platform/consul/data:/consul/data \
consul agent

二、Registrator

2.1 概述

一个由 Go 语言编写的,针对 Docker 使用的,通过检查本机容器进程在线或者停止运行状态,去注册服务的工具。所以我们要做的实验,所有的工具都是在 Docker上运行的,就是因为 Registrator 是通过检查 Docker 容器的状态来判断服务状态的,这样就和我们的代码实现完全解耦了,对上层透明化,无感知。它有如下特点

  • 通过 docker socket 直接监听容器 event,根据容器启动/停止等 event 来注册/注销服务
  • 每个容器的每个 exposed 端口对应不同的服务
  • 支持可插拔的 registry backend,默认支持 Consul, etcd and SkyDNS
  • 自身也是 docker 化的,可以容器方式启动
  • 用户可自定义配置,如服务 TTL(time-to-live)、服务名称、服务 tag 等

2.2 下载镜像

这里必须要注意:Registrator 的 lastest 版本已经2年没更新了,他的最新主板本是 master,一定要注意,因为旧的版本无法发现跟自己不是同一个网络的容器

docker pull docker.io/gliderlabs/registrator:master

2.3 启动 Registrator

docker run -d \
    --name=registrator \
    --net=host \
    -v /var/run/docker.sock:/tmp/docker.sock \
    gliderlabs/registrator:master \
        consul://127.0.0.1:8500

启动参数说明

  1. 这里 Consul 需要读取 docker socket 信息,所以需要映射 docker.sock 路径
  2. Consul 的 API 地址最好填写和 registrator 在一台机器上的 Consul 地址,因为如果你填了远端的 IP,则在 consul 端口显示你的服务地址为远端+本地端口,这个也是 Consul 设计问题或者说是 BUG
  3. 如果 Consul API 地址填了远端的,并且注册成功,然后因为一些原因改成本地的,重新注册,就会出现一个服务重复注册的情况,造成这个情况的原因是 service 的 key 与 serviceid 无关,所在集群中会造成服务的重复情况(坑爹),所以切记 Consul 地址一定要填本地,安全起见,全部写成 127.0.0.1。以及在启动容器的时候添加 -e SERVICE_ID 参数,指定 Service ID,然后在注册前做判断。如果出现重复情况,可以尝试清空出现数据异常的 Consul 节点的 data 目录。

相关参考命令

Docker

查看网络信息及容器 workloadEndpoint

查看网络

docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
e8447d6e6ca4        bridge              bridge              local              
b69fb4a79dfb        calico              calico              global              
b9b2cc4386d6        host                host                local              
ecf16013a029        none                null                local

查看具体信息

docker network inspect b69fb4a79dfb

其中在 Containers 下就有容器的信息,其中 EndpointID 就是下面提到的 workloadEndpoint

创建指定 IP 地址段的 Calico 网络

docker network create --driver calico --ipam-driver calico-ipam --subnet=10.233.0.0/16 calico

Calico

清除无用 calico workloadEndpoint

容器在 calico 服务停止的情况下被删除了,calico 服务再次启动的时候注册到 etcd 里的数据并没会刷新掉被删除容器的信息,如果你启动的是固定IP的容器,则会提示 workloadEndpoint 信息冲突,需要手动去删除

查看

calicoctl get workloadEndpoint

NODE              ORCHESTRATOR  WORKLOAD    NAME                                                              
W708-ATMQZLPR-1  libnetwork    libnetwork  1b0a02b382530a28e517e2a5099e3cb9cfb1e3d143f4bc2dc60ad31d394c58bb  
W708-ATMQZLPR-1  libnetwork    libnetwork  517810dea92addc92d02cf37012d9b2945e329fb1e2aa4bf7275ac3bd626dcba  
W708-ATMQZLPR-1  libnetwork    libnetwork  694840734bffcb79db7b28ece11f2ea9802cce764ebe0d45c55269fc728b3bae  
W708-ATMQZLPR-1  libnetwork    libnetwork  6e23c48397e6819351f9e0723ee8224d7c59b784029e0bd987095bf51608b077

删除

calicoctl delete workloadEndpoint 8173e77ea4b8dd69f68d21d846e99b27e57140dfdca28346ceeea50d4abc7e84 --node=W708-ATMQZLPR-1 --orchestrator=libnetwork --workload=libnetwork

参考链接:

Consul

Docker image:https://hub.docker.com/r/library/consul/
Consul 配置:https://www.consul.io/docs/agent/options.html
Consul Service配置:https://www.consul.io/docs/agent/services.html
Consul HTTP API:https://blog.csdn.net/u010246789/article/details/51871051
Consul 填坑:https://my.oschina.net/u/553243/blog/1634206?p=1&temp=1526522296342#blog-comments-list
博客参考:
http://www.cnblogs.com/cuishuai/p/8194345.html
https://blog.csdn.net/mn960mn/article/details/51753893
https://kevinguo.me/2017/09/01/docker-consul-consul-template-registrator-nginx/#consul
https://www.jianshu.com/p/d8ac9ad495a7
https://blog.csdn.net/socho/article/details/75434733

Consul Template

GitHub(包涵启动及配置):https://github.com/hashicorp/consul-template
配置参考:https://blog.csdn.net/lizhenhe/article/details/80030051
博客参考:https://www.hi-linux.com/posts/36431.html

Registrator

官网:https://gliderlabs.com/registrator/latest/

Calico

官网:https://www.projectcalico.org/
博客参考:http://cizixs.com/2017/10/19/docker-calico-network

 

 

P.S. 本文是帮朋友代为发表,非博主所写。

4 条评论

发表评论

*