NYC's Blog - Docker http://niyanchun.com/tag/docker/ Ubuntu16.04手动部署Kubernetes(3)——Dashboard和KubeDNS部署 http://niyanchun.com/deploy-kubernetes-step-by-step-on-trusty-section-3.html 2017-02-25T13:15:00+08:00 今天继续接着前文:《Ubuntu16.04手动部署Kubernetes(1)——Master和Node部署》《Ubuntu16.04手动部署Kubernetes(2)——Flannel网络部署》继续来部署Kubernetes。今天主要来部署Dashboard和KubeDNS,主要是后者。因为Kubernetes在每个Pod内都会起一个很小的“系统”容器pause-amd64:3.0来实现Pod内的网络,而这个容器默认会去从Google的Registry拉,但国内如果没有梯子访问不了。所以我pull了一个,传到了国内的镜像仓库,我们改下每个Node节点上的kubelet的配置,增加--pod_infra_container_image选项,这个选项可以指定从哪里pull这个基础镜像,我修改后的配置如下:KUBELET_ARGS="--api-servers=http://192.168.56.101:8080 --cluster-dns=169.169.0.2 --cluster-domain=cluster.local --hostname-override=192.168.56.101 --logtostderr=false --log-dir=/var/log/kubernetes --v=2 --pod_infra_container_image=hub.c.163.com/allan1991/pause-amd64:3.0"然后重启Kubelet:systemctl restart kubelet.service.另外,除了这个镜像外,还有很多镜像也会有这个问题,所以我都传到了国内的镜像仓库,本文介绍时都使用国内我自己传的。大家没有梯子的话,直接使用我用的这个就可以了。OK,下面开始今天的主题。部署DashboardKubernetes提供了一个基础的图形化界面,就是这个Dashboard。基本上使用kubectl可以实现的功能现在都可以通过界面去做了,本质上它俩都是调用Kubernetes API。而部署这个Dashboard也非常简答,就是创建一个Pod和一个Service。最新的创建dashboard的yaml文件可以去查看官方:https://rawgit.com/kubernetes/dashboard/master/src/deploy/kubernetes-dashboard.yaml。我的yaml文档如下:# Copyright 2015 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Configuration to deploy release version of the Dashboard UI. # # Example usage: kubectl create -f <this_file> kind: Deployment apiVersion: extensions/v1beta1 metadata: labels: app: kubernetes-dashboard name: kubernetes-dashboard namespace: kube-system spec: replicas: 1 selector: matchLabels: app: kubernetes-dashboard template: metadata: labels: app: kubernetes-dashboard # Comment the following annotation if Dashboard must not be deployed on master annotations: scheduler.alpha.kubernetes.io/tolerations: | [ { "key": "dedicated", "operator": "Equal", "value": "master", "effect": "NoSchedule" } ] spec: containers: - name: kubernetes-dashboard image: hub.c.163.com/allan1991/kubernetes-dashboard-amd64:v1.5.1 imagePullPolicy: IfNotPresent ports: - containerPort: 9090 protocol: TCP args: # Uncomment the following line to manually specify Kubernetes API server Host # If not specified, Dashboard will attempt to auto discover the API server and connect # to it. Uncomment only if the default does not work. - --apiserver-host=http://192.168.56.101:8080 livenessProbe: httpGet: path: / port: 9090 initialDelaySeconds: 30 timeoutSeconds: 30 --- kind: Service apiVersion: v1 metadata: labels: app: kubernetes-dashboard name: kubernetes-dashboard namespace: kube-system spec: type: NodePort ports: - port: 80 targetPort: 9090 selector: app: kubernetes-dashboard这里需要注意的是--apiserver-host默认是注释掉的,我有次部署时虽然找到了APIServer,但是连接被决绝,但是显式的去掉这个注释,写上APIServer的地址后就好了,所以我推荐还是写上比较好。然后使用kubectl create -f <this_file>就可以创建了。待这个Deployment和Service创建成功后,执行kubectl --namespace=kube-system get svc命令查看映射的端口号,当然我们也可以在yaml文件里面使用nodePort指定为我们想要的端口,这里我没有指定,让系统自己选择,因为选择好以后就会固定,不会再变了。我的创建好有如下:Master➜ kube-system kubectl --namespace=kube-system get svc NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes-dashboard 169.169.162.93 <nodes> 80:10977/TCP 3d这里需要注意执行命令时要加上--namespace=kube-system。前面我们也已经介绍过了,Kubernetes部署好后默认有两个namespace:default和kube-system。用户自己的默认在default下,kubectl默认也使用这个namespace;Kubernetes自己的系统模块在kube-system下面。这样我们的Dashboard已经创建好了,通过APIServer的IP加上面的端口号10977就可以访问界面了:界面还是非常的清爽的,使用起来也很方便和简单,所以这里就不详细介绍了,可以参考官方图文教程:https://kubernetes.io/docs/user-guide/ui/。部署KubeDNS在这之前我们先要搞清楚为什么需要DNS?是为了实现服务发现。而且DNS其实是Kubernetes 1.3开始才内置的功能,在这之前是利用Linux环境的方式来做的服务发现。这里我们举例说明一下利用环境变量做服务发现的过程:比如我们部署了几个服务,这些服务需要找到彼此。利用环境变量的方式就是在创建Pod的时候将Service的一些信息(主要是IP+端口)以环境变量的方式(环境变量按照一定的规则命名)注入到Pod上的容器内,这样就做到了服务发现。这种方式虽然简单,但显然有两个不足:随着服务越来越多,环境变量会越来越多。Pod必须在Service之后创建。所以后来就出现了DNS,利用这种方式去做服务发现就不会有上面所说的限制了。那DNS又是怎么做服务发现的呢,再举个例子:比如我们在Kubernetes的叫bar的namespace下面创建了一个Service叫foo。那么在bar下运行的Pod只需要做一个foo DNS查询便可获取到这个服务的IP和Port。如果是运行在其他namespace下的Pod想要查询foo这个服务,则只需要查询foo.bar即可。当然一般还会有一个域名(domain)。更详细的信息请参阅https://kubernetes.io/docs/admin/dns/。起初,Kubernetes的DNS是使用SkyDNS来实现的,网上现在的教程也基本是基于此的。但是新版本的Kubernetes已经不需要SkyDNS了,它有了自己的DNS模块——KubeDNS。这也是本文要介绍的。SkyDNS依赖于etcd,而KubeDNS不需要etcd。KubeDNS的部署也很简单,官方已经提供了基础的yaml文件:https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/dns。下面是我的yaml文件:kubedns-deployment.yaml:# Copyright 2016 The Kubernetes Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # TODO - At some point, we need to rename all skydns-*.yaml.* files to kubedns-*.yaml.* # Should keep target in cluster/addons/dns-horizontal-autoscaler/dns-horizontal-autoscaler.yaml # in sync with this file. # Warning: This is a file generated from the base underscore template file: skydns-rc.yaml.base apiVersion: extensions/v1beta1 kind: Deployment metadata: name: kube-dns namespace: kube-system labels: k8s-app: kube-dns kubernetes.io/cluster-service: "true" spec: # replicas: not specified here: # 1. In order to make Addon Manager do not reconcile this replicas parameter. # 2. Default is 1. # 3. Will be tuned in real time if DNS horizontal auto-scaling is turned on. strategy: rollingUpdate: maxSurge: 10% maxUnavailable: 0 selector: matchLabels: k8s-app: kube-dns template: metadata: labels: k8s-app: kube-dns annotations: scheduler.alpha.kubernetes.io/critical-pod: '' scheduler.alpha.kubernetes.io/tolerations: '[{"key":"CriticalAddonsOnly", "operator":"Exists"}]' spec: containers: - name: kubedns image: hub.c.163.com/allan1991/kubedns-amd64:1.9 resources: # TODO: Set memory limits when we've profiled the container for large # clusters, then set request = limit to keep this container in # guaranteed class. Currently, this container falls into the # "burstable" category so the kubelet doesn't backoff from restarting it. limits: memory: 170Mi requests: cpu: 100m memory: 70Mi livenessProbe: httpGet: path: /healthz-kubedns port: 8080 scheme: HTTP initialDelaySeconds: 60 timeoutSeconds: 5 successThreshold: 1 failureThreshold: 5 readinessProbe: httpGet: path: /readiness port: 8081 scheme: HTTP # we poll on pod startup for the Kubernetes master service and # only setup the /readiness HTTP server once that's available. initialDelaySeconds: 3 timeoutSeconds: 5 args: - --kube-master-url=http://192.168.56.101:8080 - --domain=cluster.local. - --dns-port=10053 - --config-map=kube-dns # This should be set to v=2 only after the new image (cut from 1.5) has # been released, otherwise we will flood the logs. - --v=0 # {{ pillar['federations_domain_map'] }} env: - name: PROMETHEUS_PORT value: "10055" ports: - containerPort: 10053 name: dns-local protocol: UDP - containerPort: 10053 name: dns-tcp-local protocol: TCP - containerPort: 10055 name: metrics protocol: TCP - name: dnsmasq image: hub.c.163.com/allan1991/kube-dnsmasq-amd64:1.4 livenessProbe: httpGet: path: /healthz-dnsmasq port: 8080 scheme: HTTP initialDelaySeconds: 60 timeoutSeconds: 5 successThreshold: 1 failureThreshold: 5 args: - --cache-size=1000 - --no-resolv - --server=127.0.0.1#10053 - --log-facility=- ports: - containerPort: 53 name: dns protocol: UDP - containerPort: 53 name: dns-tcp protocol: TCP # see: https://github.com/kubernetes/kubernetes/issues/29055 for details resources: requests: cpu: 150m memory: 10Mi # - name: dnsmasq-metrics # image: gcr.io/google_containers/dnsmasq-metrics-amd64:1.0 # livenessProbe: # httpGet: # path: /metrics # port: 10054 # scheme: HTTP # initialDelaySeconds: 60 # timeoutSeconds: 5 # successThreshold: 1 # failureThreshold: 5 # args: # - --v=2 # - --logtostderr # ports: # - containerPort: 10054 # name: metrics # protocol: TCP # resources: # requests: # memory: 10Mi - name: healthz image: hub.c.163.com/allan1991/exechealthz-amd64:1.2 resources: limits: memory: 50Mi requests: cpu: 10m # Note that this container shouldn't really need 50Mi of memory. The # limits are set higher than expected pending investigation on #29688. # The extra memory was stolen from the kubedns container to keep the # net memory requested by the pod constant. memory: 50Mi args: - --cmd=nslookup kubernetes.default.svc.cluster.local 127.0.0.1 >/dev/null - --url=/healthz-dnsmasq - --cmd=nslookup kubernetes.default.svc.cluster.local 127.0.0.1:10053 >/dev/null - --url=/healthz-kubedns - --port=8080 - --quiet ports: - containerPort: 8080 protocol: TCP dnsPolicy: Default # Don't use cluster DNS.这里我把dnsmasq-metrics这个container注释掉了,因为我没把这个镜像下载下来...不过没了这个也没什么影响,这个也是后来才新增的,原来是没有的。kubedns-svc.yaml:# Copyright 2016 The Kubernetes Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This file should be kept in sync with cluster/images/hyperkube/dns-svc.yaml # TODO - At some point, we need to rename all skydns-*.yaml.* files to kubedns-*.yaml.* # Warning: This is a file generated from the base underscore template file: skydns-svc.yaml.base apiVersion: v1 kind: Service metadata: name: kube-dns namespace: kube-system labels: k8s-app: kube-dns kubernetes.io/cluster-service: "true" kubernetes.io/name: "KubeDNS" spec: selector: k8s-app: kube-dns clusterIP: 169.169.0.2 ports: - name: dns port: 53 protocol: UDP - name: dns-tcp port: 53 protocol: TCP这里需要注意下clusterIP,你可以不设置,让系统自己去选择。如果显式的设置的话,指定的IP必须在APIServer的--service-cluster-ip-range参数指定的网段内。我们可以用以下命令去检查是否创建成功:Master➜ ~ kubectl --namespace=kube-system get deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE kube-dns 1 1 1 1 1d kubernetes-dashboard 1 1 1 1 3d Master➜ ~ kubectl --namespace=kube-system get pod NAME READY STATUS RESTARTS AGE kube-dns-2528035289-r7vrq 3/3 Running 0 19h kubernetes-dashboard-1077846755-lj7sc 1/1 Running 0 19h Master➜ ~ kubectl --namespace=kube-system get svc NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kube-dns 169.169.0.2 <none> 53/UDP,53/TCP 1d kubernetes-dashboard 169.169.162.93 <nodes> 80:10977/TCP 3dService、Deployment都OK后,我们需要在每个Node的Kubelet配置中加入DNS的信息,主要是--cluster-dns和--cluster-domain,比如我的配置如下:KUBELET_ARGS="--api-servers=http://192.168.56.101:8080 --cluster-dns=169.169.0.2 --cluster-domain=cluster.local --hostname-override=192.168.56.101 --logtostderr=false --log-dir=/var/log/kubernetes --v=2 --pod_infra_container_image=hub.c.163.com/allan1991/pause-amd64:3.0"这里我们使用的是默认的Domain cluster.local,当然你可以在kubedns-deployment.yaml将所有的cluster.local换为你想要的,比如time-track.cn。最后我们来验证一下,首先创建一个Pod,里面运行一个busybox容器,主要是为了使用里面的nslookup命令:busybox.yamlapiVersion: v1 kind: Pod metadata: name: busybox namespace: default spec: containers: - image: hub.c.163.com/library/busybox command: - sleep - "3600" imagePullPolicy: IfNotPresent name: busybox restartPolicy: Always然后我们再创建一个服务,比如mysql吧:mysql.yamlapiVersion: v1 kind: Service metadata: name: mysql spec: ports: - port: 3306 selector: app: mysql然后我们在先创建的busybox里面通过DNS查询后创建的mysql服务,看是否可以查到:Master➜ ~ kubectl exec busybox -c busybox -- nslookup mysql Server: 169.169.0.2 Address 1: 169.169.0.2 kube-dns.kube-system.svc.cluster.local Name: mysql Address 1: 169.169.96.10 mysql.default.svc.cluster.local Master➜ ~ kubectl describe svc mysql Name: mysql Namespace: default Labels: <none> Selector: app=mysql Type: ClusterIP IP: 169.169.96.10 Port: <unset> 3306/TCP Endpoints: <none> Session Affinity: None No events.可以看到,成功的查到了。这样通过DNS变做到了服务发现,而且没有先后顺序的限制了。 Ubuntu16.04手动部署Kubernetes(2)——Flannel网络部署 http://niyanchun.com/deploy-kubernetes-step-by-step-on-trusty-section-2.html 2017-02-24T17:30:00+08:00 在《Ubuntu16.04手动部署Kubernetes(1)——Master和Node部署》一文中,我们已经介绍了Kubernetes的Master和Node的手动部署,而且最后执行kubectl get node也成功看到了一个Node节点。本文接着上文继续部署,这次要部署的是网络。回想一下,Kubernetes有一个非常重要的特性就是任意Pod间都可以通过网络进行彼此访问,不管这些Pod是否在同一个Node上面。Flannel介绍我们知道docker默认的网络模型是创建一个虚拟网桥docker0,然后每创建一个容器,就创建一个虚拟的veth pair,其中一端关联到docker0这个网桥上,另一端使用Linux的网络命名空间技术映射到容器内的eth0设备,然后从网桥的地址段内给eth0接口分配一个IP地址。如下图(图片来自Google图片搜索,懒得画图了...):我们自己可以在机器上面创建几个容器,然后用ip a或者ifconfig命令看一下。比如我的:Master➜ others ip a | grep veth 28: veth9b8f2f6@if27: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue master docker0 state UP group default 30: veth70447ef@if29: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue master docker0 state UP group default 32: veth9f627b0@if31: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue master docker0 state UP group default 34: veth79f6b76@if33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue master docker0 state UP group default 36: vethd91d567@if35: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue master docker0 state UP group default这样的网络模型非常的简单,但不好处就是处于不同主机的容器之间无法直接通信。这里之所以说“无法直接通信”是因为通过一些手段比如端口映射等可以勉强实现不同主机上的容器通信,但非常不方便。所以Kubernetes在自己的网络模型设计方面做了一些硬性规定:所有容器间可以相互通信,且不是通过NAT方式。所有Node可以和任意容器通信(反过来也可以),且不是通过NAT方式。容器看到自己的IP和别人看到自己的IP是一样的。这个是Kubernetes集群网络的最基本要求,现在有很多软件都已经实现了这种网络模型,可参阅官方文档https://kubernetes.io/docs/admin/networking/。本文今天介绍的就是其中一种,而且非常的简单——Flannel,这是CoreOS专门为Kubernetes开发的一个网络工具,用于实现其网络模型。其模型图如下所示:flannel在每个host上面运行了一个代理程序flanneld,它可以为所在的host维护一个网段,并且为运行在这个host上的Kubernetes pods从这个网段里分配IP地址。多个hosts上的flanneld进程可以利用etcd机群互相协调以确保各自拥有不交叠的网段,这样一个集群上的pods各自拥有互不重复的IP地址。这种构筑在一个网络(hosts网络)之上的另一个网络(flanneld维护的pods网络),被称为 overlay network。上面的模型图也非常的清楚,容器里的网络流量先到docker0,然后会转到flannel0,再通过flanneld发到主机的某个网卡上(图中是eth0),然后再转发出去。流量到另外一台主机时,执行相反的过程。下面我们在上文已经搭建的Kubernetes上面部署flannel网络。Flannel部署具体部署之前先说明两个点:要测试网络至少需要两台主机,上文部署的Master和Node在同一台主机上面,所以我们需要按照上文部署Node的方法,再部署一台Node,部署好以后,会自动向Master注册。具体部署过程我就不赘述了(就是部署Kubelet、kube-proxy、docker即可),我这里已经再部署了一台。而且这两台的网络必须是通的。Flannel支持很多种模式:udp、vxlan、host-gw、aws-vpc、gce等。我们这里部署采用默认的模式即UDP。这种模式就是在应用层再对数据包做了一次封装,然后通过UDP发出去,这种方式下默认使用的端口是8258。这种应用层的再次封装转发必然会对性能有影响,而且因为是UDP,所以还可能丢包,但这些都不是本文要讨论的。etcd设置OK,现在我们来开始部署。从前面的介绍我们知道Flannel依赖于etcd,前文中我们已经部署好了,这里只需要再增加一条配置即可:etcdctl set /coreos.com/network/config '{ "Network": "10.1.0.0/16" }'这条指令我们指定了一个网段,后面Flannel在host上面构建子网的时候就会从这个网段中选取一个子网。注意,如果这个网段和你已有的网段冲突,那就更换成其他的。而这里/coreos.com/network是Flannel默认从etcd读取的前缀,可以改成其他的,然后启动Flannel时通过--etcd-prefix指定新的前缀即可,这里为了方便,直接使用默认的。这个动作只在Master上面执行。另外,etcd默认是只监听本地地址的,我们需要改为也监听其他IP,至少要监听其他Node主机可以访问到IP,这里我改为在本机所有IP上面监听,在/etc/default/etcd中增加以下两行:ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379" ETCD_ADVERTISE_CLIENT_URLS="http://0.0.0.0:2379"然后重启etcd:systemctl restart etcd.service。这样etcd部分就配置完了。flanneld配置然后从https://github.com/coreos/flannel/releases下载最新的Flannel,将解压得到的flanneld和mk-docker-opts.sh放到你的PATH里面,我继续放到/opt/bin目录下。然后和上文一样我们为flanneld创建service文件/lib/systemd/system/flanneld.service:[Unit] Description=flanneld overlay address etcd agent After=network.target Before=docker.service [Service] Type=notify ExecStart=/opt/bin/flanneld -etcd-endpoints=http://192.168.56.101:2379 --iface=enp0s8 --log_dir=/var/log/kubernetes -logtostderr=false -v=1 [Install] RequiredBy=docker.service WantedBy=multi-user.target这里有三点注意:-etcd-endpoints:这个是etcd的地址。可以在本节点上面执行curl http://192.168.56.101:2379/version来验证看是否可以访问到etcd。Flannel复用了docker的docker0网桥,所以必须在docker启动前就启动Flannel。最后要说的就是这个--iface=enp0s8。我使用Virtualbox创建了两个Ubuntu 16.04的虚拟机,默认采用的是NAT网络,一般对应enp0s3这个网卡。但为了让两个虚拟机有在同一网段的不同IP,我又使用host-only的模式增加了一个网卡enp0s8,这样一个节点的地址是192.168.56.101,另外一个是192.168.56.102,且这两个都是静态IP。默认flanneld会使用enp0s3,但两个节点通信其实是通过enp0s8,所以这里我需要显式的指定一下使用哪个网卡。如果你的docker在运行,必须先停止systemctl stop docker.service,因为flanneld会覆盖docker的docker0网桥。然后启动flanneld服务systemctl restart flanneld.service.此时执行ip a已经可以看到虚拟的flannel0了然后执行/opt/bin/mk-docker-opts.sh -i获取flanneld使用的网段,查看cat /run/flannel/subnet.env,比如我的是:FLANNEL_NETWORK=10.1.0.0/16 FLANNEL_SUBNET=10.1.35.1/24 FLANNEL_MTU=1472 FLANNEL_IPMASQ=false可见flanneld给本机分配的网段是10.1.35.1/24,我们需要将让docker在自己的docker0上面也使用这个网段,创建/etc/systemd/system/docker.service.d目录,并在里面创建任意conf后缀结尾的文件,如果已经有了,直接修改其内容,比如我的这个文件内容如下:Node➜ ~ cat /etc/systemd/system/docker.service.d/docker.conf [Service] ExecStart= ExecStart=/usr/bin/docker daemon -H fd:// --bip=10.1.35.1/24 --mtu=1472然后启动docker:systemctl start docker.service。这样docker0和flannel0就都配置好了,比如我的网络如下(如果起了容器的话,还会看到veth网络,这里未列出):Node➜ ~ ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 08:00:27:6e:c8:25 brd ff:ff:ff:ff:ff:ff inet 10.0.2.15/24 brd 10.0.2.255 scope global enp0s3 valid_lft forever preferred_lft forever inet6 fe80::a00:27ff:fe6e:c825/64 scope link valid_lft forever preferred_lft forever 3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 08:00:27:ca:35:d1 brd ff:ff:ff:ff:ff:ff inet 192.168.56.102/24 brd 192.168.56.255 scope global enp0s8 valid_lft forever preferred_lft forever inet6 fe80::a00:27ff:feca:35d1/64 scope link valid_lft forever preferred_lft forever 4: flannel0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1472 qdisc pfifo_fast state UNKNOWN group default qlen 500 link/none inet 10.1.35.0/16 scope global flannel0 valid_lft forever preferred_lft forever 5: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue state UP group default link/ether 02:42:6e:e0:81:0a brd ff:ff:ff:ff:ff:ff inet 10.1.35.1/24 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::42:6eff:fee0:810a/64 scope link valid_lft forever preferred_lft forever然后把这一节的操作在另外一台上面也执行一遍。我的另外一台部署好以后的网络如下:1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 08:00:27:af:51:42 brd ff:ff:ff:ff:ff:ff inet 10.0.2.15/24 brd 10.0.2.255 scope global enp0s3 valid_lft forever preferred_lft forever inet6 fe80::a00:27ff:feaf:5142/64 scope link valid_lft forever preferred_lft forever 3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 08:00:27:91:b8:d0 brd ff:ff:ff:ff:ff:ff inet 192.168.56.101/24 brd 192.168.56.255 scope global enp0s8 valid_lft forever preferred_lft forever inet6 fe80::a00:27ff:fe91:b8d0/64 scope link valid_lft forever preferred_lft forever 5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default link/ether 02:42:96:fd:14:ef brd ff:ff:ff:ff:ff:ff inet 10.1.85.1/24 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::42:96ff:fefd:14ef/64 scope link valid_lft forever preferred_lft forever 49: flannel0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1472 qdisc pfifo_fast state UNKNOWN group default qlen 500 link/none inet 10.1.85.0/16 scope global flannel0 valid_lft forever preferred_lft foreverflanneld使用了的子网在Master上面的etcd也会有记录:Master➜ ~ etcdctl ls /coreos.com/network/subnets --recursive /coreos.com/network/subnets/10.1.85.0-24 /coreos.com/network/subnets/10.1.35.0-24 Master➜ ~ etcdctl get /coreos.com/network/subnets/10.1.85.0-24 {"PublicIP":"192.168.56.101"} Master➜ ~ etcdctl get /coreos.com/network/subnets/10.1.35.0-24 {"PublicIP":"192.168.56.102"}验证最后我们验证一下:分别在每台上面ping一下另外一台的docker0,看是否可以ping通。如果通了再看2.我们在Kubernetes上面部署几个Pod,在一个Node上面ping另外一个Node上面的Pod的IP,如果可以ping通那几成功了。最后,如果没有通的话,看下各个服务是否启动正常,有没有报错之类的。再看下ufw防火墙和iptable是不是有问题。还是定位不出问题的话,根据Flannel的网络模型使用tcpdump分别在flannel0和docker0上面抓包,看是哪里的问题。 如何让docker daemon默认支持http的docker registry http://niyanchun.com/let-docker-daemon-support-non-tls-registry.html 2017-01-19T12:29:00+08:00 搭建过docker registry的人都知道,docker默认不支持http的registry,如果一定要支持,就需要配置--insecure-registry选项才可以,而且配置完以后需要重启docker daemon。本文从源代码角度分析docker daemon是如何限制的,以及如何去掉这个限制。我们搭建了一个私有的镜像仓库,地址是http://222.222.222.222,没有配置https。因为要支持http,需要配置docker daemon,所以基本可以判断是docker daemon做的限制。我们通过docker login登录http://222.222.222.222时docker daemon的日志会报如下错误:time="2017-01-16T09:30:33.349460061Z" level=debug msg="Calling POST /v1.24/auth" time="2017-01-16T09:30:33.350468050Z" level=debug msg="form data: {\"password\":\"*****\",\"serveraddress\":\"http://222.222.222.222/\",\"username\":\"test\"}" time="2017-01-16T09:30:33.351098322Z" level=debug msg="hostDir: /etc/docker/certs.d/222.222.222.222" time="2017-01-16T09:30:33.351607498Z" level=debug msg="hostDir: /etc/docker/certs.d/222.222.222.222" time="2017-01-16T09:30:33.353994569Z" level=debug msg="attempting v2 login to registry endpoint https://222.222.222.222/v2/" time="2017-01-16T09:30:33.383185881Z" level=info msg="Error logging in to v2 endpoint, trying next endpoint: Get https://222.222.222.222/v2/: dial tcp 222.222.222.222:443: getsockopt: connection refused" time="2017-01-16T09:30:33.383694112Z" level=debug msg="attempting v1 login to registry endpoint https://222.222.222.222/v1/" time="2017-01-16T09:30:33.411921859Z" level=info msg="Error logging in to v1 endpoint, trying next endpoint: Get https://222.222.222.222/v1/users/: dial tcp 222.222.222.222:443: getsockopt: connection refused" time="2017-01-16T09:30:33.412569676Z" level=error msg="Handler for POST /v1.24/auth returned error: Get https://222.222.222.222/v1/users/: dial tcp 222.222.222.222:443: getsockopt: connection refused" 然后我们给docker daemon配置了--insecure-registry选项后登录,观察日志:time="2017-01-17T09:56:52.267456792Z" level=debug msg="Calling POST /v1.24/auth" time="2017-01-17T09:56:52.268047734Z" level=debug msg="form data: {\"password\":\"*****\",\"serveraddress\":\"http://222.222.222.222\",\"username\":\"admin\"}" time="2017-01-17T09:56:52.268603531Z" level=debug msg="attempting v2 login to registry endpoint https://222.222.222.222/v2/" time="2017-01-17T09:56:52.300374963Z" level=info msg="Error logging in to v2 endpoint, trying next endpoint: Get https://222.222.222.222/v2/: dial tcp 222.222.222.222:443: getsockopt: connection refused" time="2017-01-17T09:56:52.301470933Z" level=debug msg="attempting v2 login to registry endpoint http://54.222.229.223/v2/"222.222.222.222我们发现没有配置--insecure-registry选项前只会尝试https://xxx,但是配置了以后,还会去尝试http://xxx。因为我们只有http服务,如果不去尝试http的话,肯定是登录不了的。然后我们去看下docker的代码。因为是跟认证相关的,所以我们很容易就可以锁定docker/daemon/auth.go这个文件,文件内容很少:package daemon import ( "golang.org/x/net/context" "github.com/docker/docker/api/types" "github.com/docker/docker/dockerversion" ) // AuthenticateToRegistry checks the validity of credentials in authConfig func (daemon *Daemon) AuthenticateToRegistry(ctx context.Context, authConfig *types.AuthConfig) (string, string, error) { return daemon.RegistryService.Auth(ctx, authConfig, dockerversion.DockerUserAgent(ctx)) }RegistryService的类型是registry.Service,或者根据代码跳转我们可以很容易的找到docker/registry/service.go这个文件,因为这个文件对于分析比较重要,这里列出全部内容:package registry import ( "crypto/tls" "fmt" "net/http" "net/url" "strings" "sync" "golang.org/x/net/context" "github.com/Sirupsen/logrus" "github.com/docker/distribution/registry/client/auth" "github.com/docker/docker/api/types" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/reference" ) const ( // DefaultSearchLimit is the default value for maximum number of returned search results. DefaultSearchLimit = 25 ) // Service is the interface defining what a registry service should implement. type Service interface { Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) ResolveRepository(name reference.Named) (*RepositoryInfo, error) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) ServiceConfig() *registrytypes.ServiceConfig TLSConfig(hostname string) (*tls.Config, error) LoadMirrors([]string) error LoadInsecureRegistries([]string) error } // DefaultService is a registry service. It tracks configuration data such as a list // of mirrors. type DefaultService struct { config *serviceConfig mu sync.Mutex } // NewService returns a new instance of DefaultService ready to be // installed into an engine. func NewService(options ServiceOptions) *DefaultService { return &DefaultService{ config: newServiceConfig(options), } } // ServiceConfig returns the public registry service configuration. func (s *DefaultService) ServiceConfig() *registrytypes.ServiceConfig { s.mu.Lock() defer s.mu.Unlock() servConfig := registrytypes.ServiceConfig{ InsecureRegistryCIDRs: make([]*(registrytypes.NetIPNet), 0), IndexConfigs: make(map[string]*(registrytypes.IndexInfo)), Mirrors: make([]string, 0), } // construct a new ServiceConfig which will not retrieve s.Config directly, // and look up items in s.config with mu locked servConfig.InsecureRegistryCIDRs = append(servConfig.InsecureRegistryCIDRs, s.config.ServiceConfig.InsecureRegistryCIDRs...) for key, value := range s.config.ServiceConfig.IndexConfigs { servConfig.IndexConfigs[key] = value } servConfig.Mirrors = append(servConfig.Mirrors, s.config.ServiceConfig.Mirrors...) return &servConfig } // LoadMirrors loads registry mirrors for Service func (s *DefaultService) LoadMirrors(mirrors []string) error { s.mu.Lock() defer s.mu.Unlock() return s.config.LoadMirrors(mirrors) } // LoadInsecureRegistries loads insecure registries for Service func (s *DefaultService) LoadInsecureRegistries(registries []string) error { s.mu.Lock() defer s.mu.Unlock() return s.config.LoadInsecureRegistries(registries) } // Auth contacts the public registry with the provided credentials, // and returns OK if authentication was successful. // It can be used to verify the validity of a client's credentials. func (s *DefaultService) Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error) { // TODO Use ctx when searching for repositories serverAddress := authConfig.ServerAddress if serverAddress == "" { serverAddress = IndexServer } if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") { serverAddress = "https://" + serverAddress } u, err := url.Parse(serverAddress) if err != nil { return "", "", fmt.Errorf("unable to parse server address: %v", err) } endpoints, err := s.LookupPushEndpoints(u.Host) if err != nil { return "", "", err } for _, endpoint := range endpoints { login := loginV2 if endpoint.Version == APIVersion1 { login = loginV1 } status, token, err = login(authConfig, endpoint, userAgent) if err == nil { return } if fErr, ok := err.(fallbackError); ok { err = fErr.err logrus.Infof("Error logging in to %s endpoint, trying next endpoint: %v", endpoint.Version, err) continue } return "", "", err } return "", "", err } // splitReposSearchTerm breaks a search term into an index name and remote name func splitReposSearchTerm(reposName string) (string, string) { nameParts := strings.SplitN(reposName, "/", 2) var indexName, remoteName string if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && !strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") { // This is a Docker Index repos (ex: samalba/hipache or ubuntu) // 'docker.io' indexName = IndexName remoteName = reposName } else { indexName = nameParts[0] remoteName = nameParts[1] } return indexName, remoteName } // Search queries the public registry for images matching the specified // search terms, and returns the results. func (s *DefaultService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) { // TODO Use ctx when searching for repositories if err := validateNoScheme(term); err != nil { return nil, err } indexName, remoteName := splitReposSearchTerm(term) // Search is a long-running operation, just lock s.config to avoid block others. s.mu.Lock() index, err := newIndexInfo(s.config, indexName) s.mu.Unlock() if err != nil { return nil, err } // *TODO: Search multiple indexes. endpoint, err := NewV1Endpoint(index, userAgent, http.Header(headers)) if err != nil { return nil, err } var client *http.Client if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" { creds := NewStaticCredentialStore(authConfig) scopes := []auth.Scope{ auth.RegistryScope{ Name: "catalog", Actions: []string{"search"}, }, } modifiers := DockerHeaders(userAgent, nil) v2Client, foundV2, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, scopes) if err != nil { if fErr, ok := err.(fallbackError); ok { logrus.Errorf("Cannot use identity token for search, v2 auth not supported: %v", fErr.err) } else { return nil, err } } else if foundV2 { // Copy non transport http client features v2Client.Timeout = endpoint.client.Timeout v2Client.CheckRedirect = endpoint.client.CheckRedirect v2Client.Jar = endpoint.client.Jar logrus.Debugf("using v2 client for search to %s", endpoint.URL) client = v2Client } } if client == nil { client = endpoint.client if err := authorizeClient(client, authConfig, endpoint); err != nil { return nil, err } } r := newSession(client, authConfig, endpoint) if index.Official { localName := remoteName if strings.HasPrefix(localName, "library/") { // If pull "library/foo", it's stored locally under "foo" localName = strings.SplitN(localName, "/", 2)[1] } return r.SearchRepositories(localName, limit) } return r.SearchRepositories(remoteName, limit) } // ResolveRepository splits a repository name into its components // and configuration of the associated registry. func (s *DefaultService) ResolveRepository(name reference.Named) (*RepositoryInfo, error) { s.mu.Lock() defer s.mu.Unlock() return newRepositoryInfo(s.config, name) } // APIEndpoint represents a remote API endpoint type APIEndpoint struct { Mirror bool URL *url.URL Version APIVersion Official bool TrimHostname bool TLSConfig *tls.Config } // ToV1Endpoint returns a V1 API endpoint based on the APIEndpoint func (e APIEndpoint) ToV1Endpoint(userAgent string, metaHeaders http.Header) (*V1Endpoint, error) { return newV1Endpoint(*e.URL, e.TLSConfig, userAgent, metaHeaders) } // TLSConfig constructs a client TLS configuration based on server defaults func (s *DefaultService) TLSConfig(hostname string) (*tls.Config, error) { s.mu.Lock() defer s.mu.Unlock() return newTLSConfig(hostname, isSecureIndex(s.config, hostname)) } // tlsConfig constructs a client TLS configuration based on server defaults func (s *DefaultService) tlsConfig(hostname string) (*tls.Config, error) { return newTLSConfig(hostname, isSecureIndex(s.config, hostname)) } func (s *DefaultService) tlsConfigForMirror(mirrorURL *url.URL) (*tls.Config, error) { return s.tlsConfig(mirrorURL.Host) } // LookupPullEndpoints creates a list of endpoints to try to pull from, in order of preference. // It gives preference to v2 endpoints over v1, mirrors over the actual // registry, and HTTPS over plain HTTP. func (s *DefaultService) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) { s.mu.Lock() defer s.mu.Unlock() return s.lookupEndpoints(hostname) } // LookupPushEndpoints creates a list of endpoints to try to push to, in order of preference. // It gives preference to v2 endpoints over v1, and HTTPS over plain HTTP. // Mirrors are not included. func (s *DefaultService) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) { s.mu.Lock() defer s.mu.Unlock() allEndpoints, err := s.lookupEndpoints(hostname) if err == nil { for _, endpoint := range allEndpoints { if !endpoint.Mirror { endpoints = append(endpoints, endpoint) } } } return endpoints, err } func (s *DefaultService) lookupEndpoints(hostname string) (endpoints []APIEndpoint, err error) { endpoints, err = s.lookupV2Endpoints(hostname) if err != nil { return nil, err } if s.config.V2Only { return endpoints, nil } legacyEndpoints, err := s.lookupV1Endpoints(hostname) if err != nil { return nil, err } endpoints = append(endpoints, legacyEndpoints...) return endpoints, nil } 代码比较简单,里面定义了一个Service接口,并且定义了一个DefaultService结构体实现了这个接口。我们主要关注一下Auth这个函数。细节我就不说了,我说一下和我们要分析的点比较相关的流程。重点在LookupPushEndpoints这个函数,从代码可以看到是这个函数构造了一个endpoints,里面是会去尝试登陆的url。那么我们的http那个应该也是在这个里面构造的。我们进到从LookupPushEndpoints跟踪到lookupEndpoints,再到lookupV2Endpoints。需要说明的是还有一个lookupV1Endpoints,这个主要是为了兼容以前V1版本的registry,现在已经很少了。而且V1和V2的代码逻辑基本是一样的,我们以V2为例分析。lookupV2Endpoints在docker/registry/service_v2.go中:package registry import ( "net/url" "strings" "github.com/docker/go-connections/tlsconfig" ) func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) { tlsConfig := tlsconfig.ServerDefault() if hostname == DefaultNamespace || hostname == IndexHostname { // v2 mirrors for _, mirror := range s.config.Mirrors { if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") { mirror = "https://" + mirror } mirrorURL, err := url.Parse(mirror) if err != nil { return nil, err } mirrorTLSConfig, err := s.tlsConfigForMirror(mirrorURL) if err != nil { return nil, err } endpoints = append(endpoints, APIEndpoint{ URL: mirrorURL, // guess mirrors are v2 Version: APIVersion2, Mirror: true, TrimHostname: true, TLSConfig: mirrorTLSConfig, }) } // v2 registry endpoints = append(endpoints, APIEndpoint{ URL: DefaultV2Registry, Version: APIVersion2, Official: true, TrimHostname: true, TLSConfig: tlsConfig, }) return endpoints, nil } tlsConfig, err = s.tlsConfig(hostname) if err != nil { return nil, err } endpoints = []APIEndpoint{ { URL: &url.URL{ Scheme: "https", Host: hostname, }, Version: APIVersion2, TrimHostname: true, TLSConfig: tlsConfig, }, } if tlsConfig.InsecureSkipVerify { endpoints = append(endpoints, APIEndpoint{ URL: &url.URL{ Scheme: "http", Host: hostname, }, Version: APIVersion2, TrimHostname: true, // used to check if supposed to be secure via InsecureSkipVerify TLSConfig: tlsConfig, }) } return endpoints, nil }到这里,我们已经已经看到真想了。可以看到要想生成http的endpoint,需要tlsConfig.InsecureSkipVerify这个为true才可以。那这个又是在哪里设置的?这段代码里面有两处涉及tlsConfig的地方:tlsConfig := tlsconfig.ServerDefault()和tlsConfig, err = s.tlsConfig(hostname)。但因为那个配置的选项是和hostname相关的,所以肯定是后者。依次进到tlsConfig、isSecureIndex,就看到了,在isSecureIndex里面,如果从配置项里面找到了,就返回该条的Secure值。这个Secure字段默认是true,但如果我们配置了--insecure-registry选项,这个字段就会被置为false,具体实现在LoadInsecureRegistries函数里面,有兴趣的可以看一下。我们已经明白了其中的原理,想让docker daemon默认就支持http的registry,改起来就非常简单了,最粗暴的做法就是将tlsConfig.InsecureSkipVerify这个恒置为true或者直接去掉那里的if,无条件执行下面的代码。当然,我觉得比较优雅的做法是增加一个配置项,比如--accept-insecure-registry,默认是false,然后再if那里和tlsConfig.InsecureSkipVerify进行或操作。这样的好处是和原来的相比这个是一种通用的配置,不会增加一项就要配置一次,重启一次docker daemon。另外,如果置为false,那就和docker原来的机制是完全一样的了,灵活性比较大。2017.1.24更新再看了下代码发现isSecureIndex函数里面支持解析CIDR,也就是说我们可以在配置--insecure-registry时配置子网,那么我们可以将这个选项配置为0.0.0.0/0就可以支持所有http的registry了。当然,还是那句话:http是不安全的,慎用。就算是私网内,也是可以自签名用https的。 从源码编译docker http://niyanchun.com/compile-docker-from-source.html 2017-01-18T13:49:00+08:00 如果你改docker的源码,那就必然需要自己从源码去编译docker,本文介绍如何从源码编译docker。编译docker的过程很复杂,但是庆幸的是docker官方已经将这个复杂的过程简单化了,它提供了一个Makefile和Dockerfile,将复杂的操作都封装起来了,对于我们可以不去太关注里面的细节。本文主要介绍一下编译流程以及对于中国用户来说如何解决一些源下载慢甚至下载不了的问题。硬件环境。编译docker的过程是挺耗费内存的,如果你使用虚拟机的话,建议至少分2GB的内存。我这里使用的OS是Ubuntu 14.04,硬件是8核16GB的服务器。安装docker。因为docker是在容器里面去编译的,所以我们需要先安装docker(如果下面的命令安装失败,请参考我之前的文章《Ubuntu安装Docker》)。apt-get install linux-image-extra-$(uname -r) linux-image-extra-virtual curl -sSL https://get.daocloud.io/docker | sh下载源码。git clone https://github.com/docker/docker.git cd docker构建编译docker的环境。docker的编译是在容器中进行的,构建这个容器的命令很简单,在docker目录执行make build即可。我们看Makefile文件可以发现,执行make build的时候其实值执行了如下命令:docker build ${BUILD_APT_MIRROR} ${DOCKER_BUILD_ARGS} -t "$(DOCKER_IMAGE)" -f "$(DOCKERFILE)" .`其实默认就是用Makefile同级目录下的Dockerfile构建出一个编译docker的镜像,这个镜像具备了编译docker的所有东西。内容如下:# This file describes the standard way to build Docker, using docker # Usage: # # # Assemble the full dev environment. This is slow the first time. # docker build -t docker . # # # Mount your source in an interactive container for quick testing: # docker run -v `pwd`:/go/src/github.com/docker/docker --privileged -i -t docker bash # # # Run the test suite: # docker run --privileged docker hack/make.sh test-unit test-integration-cli test-docker-py # # # Publish a release: # docker run --privileged \ # -e AWS_S3_BUCKET=baz \ # -e AWS_ACCESS_KEY=foo \ # -e AWS_SECRET_KEY=bar \ # -e GPG_PASSPHRASE=gloubiboulga \ # docker hack/release.sh # # Note: AppArmor used to mess with privileged mode, but this is no longer # the case. Therefore, you don't have to disable it anymore. # FROM debian:jessie # allow replacing httpredir or deb mirror ARG APT_MIRROR=deb.debian.org RUN sed -ri "s/(httpredir|deb).debian.org/$APT_MIRROR/g" /etc/apt/sources.list # Add zfs ppa COPY keys/launchpad-ppa-zfs.asc /go/src/github.com/docker/docker/keys/ RUN apt-key add /go/src/github.com/docker/docker/keys/launchpad-ppa-zfs.asc RUN echo deb http://ppa.launchpad.net/zfs-native/stable/ubuntu trusty main > /etc/apt/sources.list.d/zfs.list # Packaged dependencies RUN apt-get update && apt-get install -y \ apparmor \ apt-utils \ aufs-tools \ automake \ bash-completion \ binutils-mingw-w64 \ bsdmainutils \ btrfs-tools \ build-essential \ clang \ cmake \ createrepo \ curl \ dpkg-sig \ gcc-mingw-w64 \ git \ iptables \ jq \ less \ libapparmor-dev \ libcap-dev \ libltdl-dev \ libnl-3-dev \ libprotobuf-c0-dev \ libprotobuf-dev \ libsqlite3-dev \ libsystemd-journal-dev \ libtool \ libzfs-dev \ mercurial \ net-tools \ pkg-config \ protobuf-compiler \ protobuf-c-compiler \ python-dev \ python-mock \ python-pip \ python-websocket \ tar \ ubuntu-zfs \ vim \ vim-common \ xfsprogs \ zip \ --no-install-recommends \ && pip install awscli==1.10.15 # Get lvm2 source for compiling statically ENV LVM2_VERSION 2.02.103 RUN mkdir -p /usr/local/lvm2 \ && curl -fsSL "https://mirrors.kernel.org/sourceware/lvm2/LVM2.${LVM2_VERSION}.tgz" \ | tar -xzC /usr/local/lvm2 --strip-components=1 # See https://git.fedorahosted.org/cgit/lvm2.git/refs/tags for release tags # Compile and install lvm2 RUN cd /usr/local/lvm2 \ && ./configure \ --build="$(gcc -print-multiarch)" \ --enable-static_link \ && make device-mapper \ && make install_device-mapper # See https://git.fedorahosted.org/cgit/lvm2.git/tree/INSTALL # Configure the container for OSX cross compilation ENV OSX_SDK MacOSX10.11.sdk ENV OSX_CROSS_COMMIT a9317c18a3a457ca0a657f08cc4d0d43c6cf8953 RUN set -x \ && export OSXCROSS_PATH="/osxcross" \ && git clone https://github.com/tpoechtrager/osxcross.git $OSXCROSS_PATH \ && ( cd $OSXCROSS_PATH && git checkout -q $OSX_CROSS_COMMIT) \ && curl -sSL https://s3.dockerproject.org/darwin/v2/${OSX_SDK}.tar.xz -o "${OSXCROSS_PATH}/tarballs/${OSX_SDK}.tar.xz" \ && UNATTENDED=yes OSX_VERSION_MIN=10.6 ${OSXCROSS_PATH}/build.sh ENV PATH /osxcross/target/bin:$PATH # Install seccomp: the version shipped in trusty is too old ENV SECCOMP_VERSION 2.3.1 RUN set -x \ && export SECCOMP_PATH="$(mktemp -d)" \ && curl -fsSL "https://github.com/seccomp/libseccomp/releases/download/v${SECCOMP_VERSION}/libseccomp-${SECCOMP_VERSION}.tar.gz" \ | tar -xzC "$SECCOMP_PATH" --strip-components=1 \ && ( \ cd "$SECCOMP_PATH" \ && ./configure --prefix=/usr/local \ && make \ && make install \ && ldconfig \ ) \ && rm -rf "$SECCOMP_PATH" # Install Go # IMPORTANT: If the version of Go is updated, the Windows to Linux CI machines # will need updating, to avoid errors. Ping #docker-maintainers on IRC # with a heads-up. ENV GO_VERSION 1.7.4 RUN curl -fsSL "https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz" \ | tar -xzC /usr/local ENV PATH /go/bin:/usr/local/go/bin:$PATH ENV GOPATH /go # Compile Go for cross compilation ENV DOCKER_CROSSPLATFORMS \ linux/386 linux/arm \ darwin/amd64 \ freebsd/amd64 freebsd/386 freebsd/arm \ windows/amd64 windows/386 \ solaris/amd64 # Dependency for golint ENV GO_TOOLS_COMMIT 823804e1ae08dbb14eb807afc7db9993bc9e3cc3 RUN git clone https://github.com/golang/tools.git /go/src/golang.org/x/tools \ && (cd /go/src/golang.org/x/tools && git checkout -q $GO_TOOLS_COMMIT) # Grab Go's lint tool ENV GO_LINT_COMMIT 32a87160691b3c96046c0c678fe57c5bef761456 RUN git clone https://github.com/golang/lint.git /go/src/github.com/golang/lint \ && (cd /go/src/github.com/golang/lint && git checkout -q $GO_LINT_COMMIT) \ && go install -v github.com/golang/lint/golint # Install CRIU for checkpoint/restore support ENV CRIU_VERSION 2.9 RUN mkdir -p /usr/src/criu \ && curl -sSL https://github.com/xemul/criu/archive/v${CRIU_VERSION}.tar.gz | tar -v -C /usr/src/criu/ -xz --strip-components=1 \ && cd /usr/src/criu \ && make \ && make install-criu # Install two versions of the registry. The first is an older version that # only supports schema1 manifests. The second is a newer version that supports # both. This allows integration-cli tests to cover push/pull with both schema1 # and schema2 manifests. ENV REGISTRY_COMMIT_SCHEMA1 ec87e9b6971d831f0eff752ddb54fb64693e51cd ENV REGISTRY_COMMIT 47a064d4195a9b56133891bbb13620c3ac83a827 RUN set -x \ && export GOPATH="$(mktemp -d)" \ && git clone https://github.com/docker/distribution.git "$GOPATH/src/github.com/docker/distribution" \ && (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT") \ && GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \ go build -o /usr/local/bin/registry-v2 github.com/docker/distribution/cmd/registry \ && (cd "$GOPATH/src/github.com/docker/distribution" && git checkout -q "$REGISTRY_COMMIT_SCHEMA1") \ && GOPATH="$GOPATH/src/github.com/docker/distribution/Godeps/_workspace:$GOPATH" \ go build -o /usr/local/bin/registry-v2-schema1 github.com/docker/distribution/cmd/registry \ && rm -rf "$GOPATH" # Install notary and notary-server ENV NOTARY_VERSION v0.5.0 RUN set -x \ && export GOPATH="$(mktemp -d)" \ && git clone https://github.com/docker/notary.git "$GOPATH/src/github.com/docker/notary" \ && (cd "$GOPATH/src/github.com/docker/notary" && git checkout -q "$NOTARY_VERSION") \ && GOPATH="$GOPATH/src/github.com/docker/notary/vendor:$GOPATH" \ go build -o /usr/local/bin/notary-server github.com/docker/notary/cmd/notary-server \ && GOPATH="$GOPATH/src/github.com/docker/notary/vendor:$GOPATH" \ go build -o /usr/local/bin/notary github.com/docker/notary/cmd/notary \ && rm -rf "$GOPATH" # Get the "docker-py" source so we can run their integration tests ENV DOCKER_PY_COMMIT e2655f658408f9ad1f62abdef3eb6ed43c0cf324 RUN git clone https://github.com/docker/docker-py.git /docker-py \ && cd /docker-py \ && git checkout -q $DOCKER_PY_COMMIT \ && pip install -r test-requirements.txt # Install yamllint for validating swagger.yaml RUN pip install yamllint==1.5.0 # Install go-swagger for validating swagger.yaml ENV GO_SWAGGER_COMMIT c28258affb0b6251755d92489ef685af8d4ff3eb RUN git clone https://github.com/go-swagger/go-swagger.git /go/src/github.com/go-swagger/go-swagger \ && (cd /go/src/github.com/go-swagger/go-swagger && git checkout -q $GO_SWAGGER_COMMIT) \ && go install -v github.com/go-swagger/go-swagger/cmd/swagger # Set user.email so crosbymichael's in-container merge commits go smoothly RUN git config --global user.email 'docker-dummy@example.com' # Add an unprivileged user to be used for tests which need it RUN groupadd -r docker RUN useradd --create-home --gid docker unprivilegeduser VOLUME /var/lib/docker WORKDIR /go/src/github.com/docker/docker ENV DOCKER_BUILDTAGS apparmor pkcs11 seccomp selinux # Let us use a .bashrc file RUN ln -sfv $PWD/.bashrc ~/.bashrc # Add integration helps to bashrc RUN echo "source $PWD/hack/make/.integration-test-helpers" >> /etc/bash.bashrc # Register Docker's bash completion. RUN ln -sv $PWD/contrib/completion/bash/docker /etc/bash_completion.d/docker # Get useful and necessary Hub images so we can "docker load" locally instead of pulling COPY contrib/download-frozen-image-v2.sh /go/src/github.com/docker/docker/contrib/ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \ buildpack-deps:jessie@sha256:25785f89240fbcdd8a74bdaf30dd5599a9523882c6dfc567f2e9ef7cf6f79db6 \ busybox:latest@sha256:e4f93f6ed15a0cdd342f5aae387886fba0ab98af0a102da6276eaf24d6e6ade0 \ debian:jessie@sha256:f968f10b4b523737e253a97eac59b0d1420b5c19b69928d35801a6373ffe330e \ hello-world:latest@sha256:8be990ef2aeb16dbcb9271ddfe2610fa6658d13f6dfb8bc72074cc1ca36966a7 # See also "hack/make/.ensure-frozen-images" (which needs to be updated any time this list is) # Install tomlv, vndr, runc, containerd, tini, docker-proxy # Please edit hack/dockerfile/install-binaries.sh to update them. COPY hack/dockerfile/binaries-commits /tmp/binaries-commits COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy bindata # Wrap all commands in the "docker-in-docker" script to allow nested containers ENTRYPOINT ["hack/dind"] # Upload docker source COPY . /go/src/github.com/docker/docker 对于中国用户来说,如果没有科学上网工具,直接使用这个Dockerfile,基本上不会成功,因为有些下载不了。所以我们需要修改几个地方,特别是装软件的地方,需要下载很多东西。我的修改点如下:修改替换Debian源为中国的源(大约第30行)# 修改前 ARG APT_MIRROR=deb.debian.org # 修改后 ARG APT_MIRROR=ftp.cn.debian.org修改pip源为国内的源(大约第85行)# 修改前 && pip install awscli==1.10.15 # 修改后 && pip install awscli==1.10.15 -i http://mirrors.aliyun.com/pypi/simple修改下载go安装包的地址(大约133行)。这里需要注意GO_VERSION的版本,不同版本的地址可以去http://golangtc.com/download获取。# 修改前 ENV GO_VERSION 1.7.4 RUN curl -fsSL "https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz" \ | tar -xzC /usr/local # 修改后 ENV GO_VERSION 1.7.4 RUN curl -fsSL "http://golangtc.com/static/go/1.7.4/go1.7.4.linux-amd64.tar.gz" \ | tar -xzC /usr/local 其他的基本都是从docker hub和github下载东西,对于docker hub可以配置加速器,github没有被墙,但可能不稳定或者速度比较慢。整个容器的过程还是比较慢的,不过比起编译Linux内核和Gcc还是快了不少的。如果不出什么错的话,编译完之后我们就会有一个叫docker-dev的容器,我编译出来有2.439GB。这样我们就有编译docker源码的环境了。编译docker。这个也很简单,在刚才的目录执行make binary即可。如果没出什么错,我们可以看到如下信息:---> Making bundle: binary (in bundles/1.14.0-dev/binary) Building: bundles/1.14.0-dev/binary-client/docker-1.14.0-dev Created binary: bundles/1.14.0-dev/binary-client/docker-1.14.0-dev Building: bundles/1.14.0-dev/binary-daemon/dockerd-1.14.0-dev Created binary: bundles/1.14.0-dev/binary-daemon/dockerd-1.14.0-dev Copying nested executables into bundles/1.14.0-dev/binary-daemon可以看到编译出来的东西在bundles/1.14.0-dev/binary目录,有client(bundles/1.14.0-dev/binary-client/docker)和daemon(bundles/1.14.0-dev/binary-daemon/dockerd):至此编译结束了。 Kubernetes初体验 http://niyanchun.com/kubernetes-trial.html 2017-01-12T15:06:00+08:00 最近了解了一下Kubernetes,发现对于一个新手想要先简单体验一下Kubernetes,还是会遇到非常多的问题,所以我结合官方的Tutorials以及自己的摸索,写了本篇博客,一方面加深对Kubernetes的理解,另一方面也希望给别人带来一些帮助。另外:我使用的环境是MacOS Sierra 10.12.2+Virtualbox+minikube v0.15+kubectl v1.5.1,不同的环境可能遇到不同的问题,希望多学习交流。本文的定位主要是初学者体验Kubernetes,所以对于Kubernetes的Pod、Service、Label等核心概念我只是结合官方介绍与自己的了解做一个简单的介绍,能了解大概原理即可。作为初学者,理解难免有误,希望不吝指教。Kubernetes ClusterKubernetes is a production-grade, open-source platform that orchestrates the placement (scheduling) and execution of application containers within and across computer clusters.Kubernetes将底层的计算资源连接在一起对外体现为一个计算集群,并将资源高度抽象化。部署应用时Kubernetes会以更高效的方式自动的将应用分发到集群内的机器上面,并调度运行。几个Kubernetes集群包含两种类型的资源:Master节点:协调控制整个集群。Nodes节点:运行应用的工作节点。如下图:Masters manage the cluster and the nodes are used to host the running applications.Master负责管理整个集群,协调集群内的所有行为。比如调度应用,监控应用的状态等。Node节点负责运行应用,一般是一台物理机或者虚机。每个Node节点上面都有一个Kubelet,它是一个代理程序,用来管理该节点以及和Master节点通信。除此以外,Node节点上还会有一些管理容器的工具,比如Docker或者rkt等。生产环境中一个Kubernetes集群至少应该包含三个Nodes节点。当部署应用的时候,我们通知Master节点启动应用容器。然后Master会调度这些应用将它们运行在Node节点上面。Node节点和Master节点通过Master节点暴露的Kubernetes API通信。当然我们也可以直接通过这些API和集群交互。Kubernetes提供了一个轻量级的Minikube应用,利用它我们可以很容器的创建一个只包含一个Node节点的Kubernetes Cluster用于日常的开发测试。安装MinikubeMinikube的Github:https://github.com/kubernetes/minikubeMinikube提供OSX、Linux、Windows版本,本文测试环境为OSX,且以当时最新的版本为例。实际操作时,尽量到官网查看最新的版本及安装命令。MacOS安装:curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.15.0/minikube-darwin-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/Linux安装:curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.15.0/minikube-linux-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/Minikube要正常使用,还必须安装kubectl,并且放在PATH里面。kubectl是一个通过Kubernetes API和Kubernetes集群交互的命令行工具。安装方法如下:MACOS安装:wget https://storage.googleapis.com/kubernetes-release/release/v1.5.1/bin/darwin/amd64/kubectl chmod +x kubectl mv kubectl /usr/local/bin/kubectlLinux安装:wget https://storage.googleapis.com/kubernetes-release/release/v1.5.1/bin/linux/amd64/kubectl chmod +x kubectl mv kubectl /usr/local/bin/kubectl因为Minikube使用Docker Machine管理Kubernetes的虚机,所以我们还需要安装一些虚机的driver,这里推荐使用Virtualbox,因为安装简单(从官网下载安装即可),而且后面可以简单的登录到VM里面去进行一些操作。其它driver的安装请参考官方文档:https://github.com/kubernetes/minikube/blob/master/DRIVERS.md。安装完以后,我们可以使用一下命令进行一些信息查看:minikube version # 查看版本 minikube start --vm-driver=virtualbox # 启动Kubernetes Cluster,这里使用的driver是Virtualbox kubectl version # 查看版本 kubectl cluster-info # 查看集群信息 kubectl get nodes # 查看当前可用的Node,使用Minikube创建的cluster只有一个Node节点至此,我们已经用Minikube创建了一个Kubernetes Cluster,下面我们在这个集群上面部署一个应用。Only For Chinese在进行下一节部署具体应用前我们先要做一件事情。Kubernetes在部署容器应用的时候会先拉一个pause镜像,这个是一个基础容器,主要是负责网络部分的功能的,具体这里不展开讨论。最关键的是Kubernetes里面镜像默认都是从Google的镜像仓库拉的(就跟docker默认从docker hub拉的一样),但是因为GFW的原因,中国用户是访问不了Google的镜像仓库gcr.io的(如果你可以ping通,那恭喜你)。庆幸的是这个镜像被传到了docker hub上面,虽然中国用户访问后者也非常艰难,但通过一些加速器之类的还是可以pull下来的。如果没有VPN等科学上网的工具的话,请先做如下操作:minikube ssh # 登录到我们的Kubernetes VM里面去 docker pull hub.c.163.com/allan1991/pause-amd64:3.0 docker tag hub.c.163.com/allan1991/pause-amd64:3.0 gcr.io/google_containers/pause-amd64:3.0 我们先从其他镜像仓库(这里我使用的是HNA CloudOS容器云平台提供的镜像仓库)下载Kubernetes需要的基础网络容器pause,Mac OSX上面kubectl 1.5.1版本需要的是pause-amd64:3.0,然后我将其打成gcr.io/google_containers/pause-amd64:3.0。这样Kubernetes VM就不会从gcr.io拉镜像了,而是会直接使用本地的镜像。关于这个问题,Kubernetes上面还专门有个issue讨论,有兴趣的可以看看:https://github.com/kubernetes/kubernetes/issues/6888。OK,接着我们可以进行下一步了。Deploy an App在Kubernetes Cluster上面部署应用,我们需要先创建一个Kubernetes Deployment。这个Deployment负责创建和更新我们的应用实例。当这个Deployment创建之后,Kubernetes master就会将这个Deployment创建出来的应用实例部署到集群内某个Node节点上。而且自应用实例创建后,Deployment controller还会持续监控应用,直到应用被删除或者部署应用的Node节点不存在。A Deployment is responsible for creating and updating instances of your application这里我们依旧使用kubectl来创建Deployment,创建的时候需要制定容器镜像以及我们要启动的个数(replicas),当然这些信息后面可以再更新。这里我用Go写了一个简单的Webserver,返回“Hello World”,监听端口是8090.我们就来启动这个应用(镜像地址:registry.hnaresearch.com/public/hello-world:v1.0 备用镜像地址:hub.c.163.com/allan1991/hello-world:v1.0)kubectl run helloworld --image=registry.hnaresearch.com/public/hello-world:v1.0 --port=8090这条命令执行后master寻找一个合适的node来部署我们的应用实例(我们只有一个node)。我们可以使用kubectl get deployment来查看我们创建的Deployment:➜ ~ kubectl get deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE helloworld 1 1 1 1 53m默认应用部署好之后是只在Kubernetes Cluster内部可见的,有多种方法可以让我们的应用暴露到外部,这里先介绍一种简单的:我们可以通过kubectl proxy命令在我们的终端和Kubernetes Cluster直接创建一个代理。然后,打开一个新的终端,通过Pod名(Pod后面会有讲到,可以通过kubectl get pod查看Pod名字)就可以访问了:➜ ~ kubectl proxy Starting to serve on 127.0.0.1:8001 # 打开新的终端 ➜ ~ kubectl get pod NAME READY STATUS RESTARTS AGE helloworld-2080166056-4q88d 1/1 Running 0 1h ➜ ~ curl http://localhost:8001/api/v1/proxy/namespaces/default/pods/helloworld-2080166056-4q88d/ Hello world !OK,我们的第一个应用已经部署成功了。如果你的应用有问题(比如kubectl get deployment执行后看到AVAILABL是0),可以通过kubectl describe pod pod名字来查看信息(最后面的Events部分是该Pod的信息),看哪里出错了。好吧,这里我们已经提到了Pod,它是Kubernetes中一个非常重要的概念,也是区别于其他编排系统的一个设计,这里我们简单介绍一下。Pods和Nodes其实我们创建了Deployment之后,它并不是直接创建了容器实例,而是先在Node上面创建了Pod,然后再在Pod里面创建容器。那Pod到底是什么?Pod是Kubernetes里面抽象出来的一个概念,它是能够被创建、调度和管理的最小单元;每个Pod都有一个独立的IP;一个Pod由若干个容器构成。一个Pod之内的容器共享Pod的所有资源,这些资源主要包括:共享存储(以Volumes的形式)、共享网络、共享端口等。Kubernetes虽然也是一个容器编排系统,但不同于其他系统,它的最小操作单元不是单个容器,而是Pod。这个特性给Kubernetes带来了很多优势,比如最显而易见的是同一个Pod内的容器可以非常方便的互相访问(通过localhost就可以访问)和共享数据。A Pod is a group of one or more application containers (such as Docker or rkt) and includes shared storage (volumes), IP address and information about how to run them.Containers should only be scheduled together in a single Pod if they are tightly coupled and need to share resources such as disk.Services和Labels上一步我们已经创建了一个应用,并且通过proxy实现了集群外部可以访问,但这种Proxy的方式是不太适用于实际生产环境的。本节我们再介绍一个Kubernetes里面另外一个非常重要的概念———Service。Service是Kubernetes里面抽象出来的一层,它定义了由多个Pods组成的逻辑组(logical set),可以对组内的Pod做一些事情:对外暴露流量做负载均衡(load balancing)服务发现(service-discovery)。而且每个Service都有一个集群内唯一的私有IP和对外的端口,用于接收流量。如果我们想将一个Service暴露到集群外,有两种方法:LoadBalancer - 提供一个公网的IPNodePort - 使用NAT将Service的端口暴露出去。Minikube只支持这种方式。A Kubernetes Service is an abstraction layer which defines a logical set of Pods and enables external traffic exposure, load balancing and service discovery for those Pods.我们再来介绍一下Kubernetes里面的第三个比较重要的概念——Label。Service就是靠Label选择器(Label Selectors)来匹配组内的Pod的,而且很多命令都可以操作Label。Label是绑定在对象上(比如Pod)的键值对,主要用来把一些相关的对象组织在一起,并且对于用户来说label是有含义的,比如:Production environment (production, test, dev)Application version (beta, v1.3)Type of service/server (frontend, backend, database)当然,Label是随时可以更改的。Labels are key/value pairs that are attached to objects接着上的例子,我们使用Service的方式将我们之前部署的helloworld应用暴露到集群外部。因为Minikube只支持NodePort方式,所以这里我们使用NodePort方式。使用kubectl get service可以查看目前已有的service,Minikube默认创建了一个kubernetes Service。我们使用expose命令再创建一个Service:# 查看已有Service ➜ ~ kubectl get service NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE hello-nginx 10.0.0.163 <pending> 80:31943/TCP 1d helloworld 10.0.0.114 <nodes> 8090:30350/TCP 1h kubernetes 10.0.0.1 <none> 443/TCP 1d # 创建新的Service ➜ ~ kubectl expose deployment/helloworld --type="NodePort" --port 8090 service "helloworld" exposed # 查看某个Service的详细信息 ➜ ~ kubectl describe service/helloworld Name: helloworld Namespace: default Labels: run=helloworld Selector: run=helloworld Type: NodePort IP: 10.0.0.114 Port: <unset> 8090/TCP NodePort: <unset> 30350/TCP Endpoints: 172.17.0.3:8090 Session Affinity: None No events.这样内部应用helloworld的8090端口映射到了外部的30350端口。此时我们就可以通过外部访问里面的应用了:➜ ~ curl 192.168.99.100:30350 Hello world !这里的IP是minikube的Docker host的IP,可以通过minikube docker-env命令查看。再看Label的例子,创建Pod的时候默认会生产一个Label和Label Selector,我们可以使用kubectl label命令创建新的Label。➜ ~ kubectl describe pod helloworld-2080166056-4q88d Name: helloworld-2080166056-4q88d Namespace: default Node: minikube/192.168.99.100 Start Time: Tue, 10 Jan 2017 16:36:22 +0800 Labels: pod-template-hash=2080166056 # 默认的Label run=helloworld Status: Running IP: 172.17.0.3 Controllers: ReplicaSet/helloworld-2080166056 Containers: helloworld: Container ID: docker://a45e88e7cb9fdb193a2ed62539918e293aa381d5c8fedb57ed78e54df9ea502a Image: registry.hnaresearch.com/public/hello-world:v1.0 Image ID: docker://sha256:b3737bd8b3af68017bdf932671212be7211f4cdc47cd63b2300fbd71057ecf83 Port: 8090/TCP State: Running Started: Tue, 10 Jan 2017 16:36:32 +0800 Ready: True Restart Count: 0 Volume Mounts: /var/run/secrets/kubernetes.io/serviceaccount from default-token-zfl0m (ro) Environment Variables: <none> Conditions: Type Status Initialized True Ready True PodScheduled True Volumes: default-token-zfl0m: Type: Secret (a volume populated by a Secret) SecretName: default-token-zfl0m QoS Class: BestEffort Tolerations: <none> No events. # 新增一个Label ➜ ~ kubectl label pod helloworld-2080166056-4q88d app=v1 pod "helloworld-2080166056-4q88d" labeled ➜ ~ kubectl describe pod helloworld-2080166056-4q88d Name: helloworld-2080166056-4q88d Namespace: default Node: minikube/192.168.99.100 Start Time: Tue, 10 Jan 2017 16:36:22 +0800 Labels: app=v1 # 新增的Label pod-template-hash=2080166056 run=helloworld Status: Running IP: 172.17.0.3 Controllers: ReplicaSet/helloworld-2080166056 Containers: helloworld: Container ID: docker://a45e88e7cb9fdb193a2ed62539918e293aa381d5c8fedb57ed78e54df9ea502a Image: registry.hnaresearch.com/public/hello-world:v1.0 Image ID: docker://sha256:b3737bd8b3af68017bdf932671212be7211f4cdc47cd63b2300fbd71057ecf83 Port: 8090/TCP State: Running Started: Tue, 10 Jan 2017 16:36:32 +0800 Ready: True Restart Count: 0 Volume Mounts: /var/run/secrets/kubernetes.io/serviceaccount from default-token-zfl0m (ro) Environment Variables: <none> Conditions: Type Status Initialized True Ready True PodScheduled True Volumes: default-token-zfl0m: Type: Secret (a volume populated by a Secret) SecretName: default-token-zfl0m QoS Class: BestEffort Tolerations: <none> No events. # 使用Label的例子 ➜ ~ kubectl get service -l run=helloworld NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE helloworld 10.0.0.114 <nodes> 8090:30350/TCP 5m ➜ ~ kubectl get pod -l app=v1 NAME READY STATUS RESTARTS AGE helloworld-2080166056-4q88d 1/1 Running 0 1d删掉一个Service使用kubectl delete service命令:➜ ~ kubectl delete service helloworld删除Service后,我们就没法通过外部访问到内部应用了,但是Pod内的应用依旧是在正常运行的。Scale an AppPS:这个Scale不知道咋翻译才好,就用scale吧。Scaling is accomplished by changing the number of replicas in a Deployment.刚才我们部署了一个应用,并且增加了Service。但是这个应用只运行在一个Pod上面。随着流量的增加,我们可能需要增加我们应用的规模来满足用户的需求。Kubernetes的Scale功能就可以实现这个需求。You can create from the start a Deployment with multiple instances using the --replicas parameter for the kubectl run command扩大应用的规模时,Kubernetes将会在Nodes上面使用可用的资源来创建新的Pod,并运行新增加的应用,缩小规模时做相反的操作。Kubernetes也支持自动规模化Pod。当然我们也可以将应用的数量变为0,这样就会终止所有部署该应用的Pods。应用数量增加后,Service内的负载均衡就会变得非常有用了,为了表现出这个特性,我修改了一下程序,除了打印“Hello world”以外,还会打印主机名。我们先看一下现有的一些输出字段的含义:➜ ~ kubectl get deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE helloworld 1 1 1 1 2m可以看到,现在我们只有一个Pod,DESIRED字段表示我们配置的replicas的个数,即实例的个数。CURRENT字段表示目前处于running状态的replicas的个数。UP-TO-DATE字段表示表示和预先配置的期望状态相符的replicas的个数。AVAILABLE字段表示目前实际对用户可用的replicas的个数。下面我们使用kubectl scale命令将启动4个复制品,语法规则是kubectl scale deployment-type name replicas-number:‘➜ ~ kubectl scale deployment/helloworld --replicas=4 deployment "helloworld" scaled # 查看应用实例个数 ➜ ~ kubectl get deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE helloworld 4 4 4 4 9m # 查看Pod个数 ➜ ~ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE helloworld-2080166056-fn03c 1/1 Running 0 9m 172.17.0.2 minikube helloworld-2080166056-jlwz1 1/1 Running 0 32s 172.17.0.4 minikube helloworld-2080166056-mmpn4 1/1 Running 0 32s 172.17.0.3 minikube helloworld-2080166056-wh696 1/1 Running 0 32s 172.17.0.5 minikube可以看到,我们已经有4个应用实例了,而且Pod个数也变成4个了,每个都有自己的IP。当然,日志里面也有相关信息:➜ ~ kubectl describe deployment/helloworld Name: helloworld Namespace: default CreationTimestamp: Thu, 12 Jan 2017 13:37:47 +0800 Labels: run=helloworld Selector: run=helloworld Replicas: 4 updated | 4 total | 4 available | 0 unavailable StrategyType: RollingUpdate MinReadySeconds: 0 RollingUpdateStrategy: 1 max unavailable, 1 max surge Conditions: Type Status Reason ---- ------ ------ Available True MinimumReplicasAvailable OldReplicaSets: <none> NewReplicaSet: helloworld-2080166056 (4/4 replicas created) Events: FirstSeen LastSeen Count From SubObjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 11m 11m 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set helloworld-2080166056 to 1 2m 2m 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set helloworld-2080166056 to 4然后我们再看下之前我们创建的Service信息:➜ ~ kubectl describe service/helloworld Name: helloworld Namespace: default Labels: run=helloworld Selector: run=helloworld Type: NodePort IP: 10.0.0.170 Port: <unset> 8090/TCP NodePort: <unset> 31030/TCP Endpoints: 172.17.0.2:8090,172.17.0.3:8090,172.17.0.4:8090 + 1 more... Session Affinity: None No events.可以看到Service的信息也已经更新了。让我们验证一下这个Service是有负载均衡的:➜ ~ curl 192.168.99.100:31030 Hello world ! hostname:helloworld-2080166056-jlwz1 ➜ ~ curl 192.168.99.100:31030 Hello world ! hostname:helloworld-2080166056-mmpn4 ➜ ~ curl 192.168.99.100:31030 Hello world ! hostname:helloworld-2080166056-wh696 ➜ ~ curl 192.168.99.100:31030 Hello world ! hostname:helloworld-2080166056-mmpn4 ➜ ~ curl 192.168.99.100:31030 Hello world ! hostname:helloworld-2080166056-wh696接着我们将实例缩减为2个:➜ ~ kubectl scale deployment/helloworld --replicas=2 deployment "helloworld" scaled ➜ ~ kubectl get deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE helloworld 2 2 2 2 18m ➜ ~ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE helloworld-2080166056-fn03c 1/1 Running 0 18m 172.17.0.2 minikube helloworld-2080166056-wh696 1/1 Running 0 9m 172.17.0.5 minikubeUpdate an AppKubernetes还提供了一个非常有用的特性——滚动更新(Rolling update),这个特性的好处就是我们不用停止服务就可以实现应用更新。默认更新的时候是一个Pod一个Pod更新的,所以整个过程服务不会中断。当然你也可以设置一次更新的Pod的百分比。而且更新过程中,Service只会将流量转发到可用的节点上面。更加重要的是,我们可以随时回退到旧版本。Rolling updates allow Deployments' update to take place with zero downtime by incrementally updating Pods instances with new ones.If a Deployment is exposed publicly, the Service will load-balance the traffic only to available Pods during the update.OK,我们来实践一下。我们在原来程序的基础上,多输出一个v2作为新版本,使用set image命令指定新版本镜像。# 使用set image命令执行新版本镜像 ➜ ~ kubectl set image deployments/helloworld helloworld=registry.hnaresearch.com/public/hello-world:v2.0 deployment "helloworld" image updated ➜ ~ kubectl get pod NAME READY STATUS RESTARTS AGE helloworld-2080166056-fn03c 1/1 Running 0 28m helloworld-2161692841-8psgp 0/1 ContainerCreating 0 5s helloworld-2161692841-cmzg0 0/1 ContainerCreating 0 5s # 更新中 ➜ ~ kubectl get pods NAME READY STATUS RESTARTS AGE helloworld-2080166056-fn03c 1/1 Running 0 28m helloworld-2161692841-8psgp 0/1 ContainerCreating 0 20s helloworld-2161692841-cmzg0 0/1 ContainerCreating 0 20s # 已经更新成功 ➜ ~ kubectl get pods NAME READY STATUS RESTARTS AGE helloworld-2161692841-8psgp 1/1 Running 0 2m helloworld-2161692841-cmzg0 1/1 Running 0 2m ➜ ~ curl 192.168.99.100:31030 Hello world v2! # 输出以变为v2 hostname:helloworld-2161692841-cmzg0然后我们继续更新到不存在的v3版本:➜ ~ kubectl set image deployments/helloworld helloworld=registry.hnaresearch.com/public/hello-world:v3.0 deployment "helloworld" image updated # 更新中 ➜ ~ kubectl get pods NAME READY STATUS RESTARTS AGE helloworld-2161692841-cmzg0 1/1 Running 0 16m helloworld-2243219626-7qhzh 0/1 ContainerCreating 0 4s helloworld-2243219626-rfw64 0/1 ContainerCreating 0 4s # 更新时,Service只会讲请求转发到可用节点 ➜ ~ curl 192.168.99.100:31030 Hello world v2! hostname:helloworld-2161692841-cmzg0 ➜ ~ curl 192.168.99.100:31030 Hello world v2! hostname:helloworld-2161692841-cmzg0 # 因为镜像不存在,所以更新失败了。但仍然有一个Pod是可用的 ➜ ~ kubectl get pod NAME READY STATUS RESTARTS AGE helloworld-2161692841-cmzg0 1/1 Running 0 16m helloworld-2243219626-7qhzh 0/1 ErrImagePull 0 21s helloworld-2243219626-rfw64 0/1 ErrImagePull 0 21s因为我们指定了一个不存在的镜像,所以更新失败了。现在我们使用kubectl rollout undo命令回滚到之前v2的版本:➜ ~ kubectl rollout undo deployment/helloworld deployment "helloworld" rolled back ➜ ~ kubectl get pod NAME READY STATUS RESTARTS AGE helloworld-2161692841-cmzg0 1/1 Running 0 19m helloworld-2161692841-mw9g2 1/1 Running 0 5s ➜ ~ curl 192.168.99.100:31030 Hello world v2! hostname:helloworld-2161692841-cmzg0 ➜ ~ curl 192.168.99.100:31030 Hello world v2! hostname:helloworld-2161692841-mw9g2结束语Kubernetes是Google根据他们多年的生产经验以及已有系统Brog等开发的一套全新系统,虽然目前我还只是个初学者,但是能够感觉到Kubernetes在一些结构设计方面相比于其他容器编排系统有着独特的见解。至于Kubernetes是什么,官方也有比较详细的说明:https://kubernetes.io/docs/whatisk8s。后面我抽空会将这篇文章翻译一下,以加深理解。 哦,对,Kubernetes一词源于希腊语,是舵手(helmsman)、船员(pilot)的意思。因为首字母和最后一个字母中间有8个字母,所以又简称K8s。参考:http://kubernetes.io/docs/tutorials 构建最小的Go程序镜像 http://niyanchun.com/build-minimal-go-image.html 2017-01-02T14:22:00+08:00 我们知道构建一个Docker镜像的时候往往需要引入一些程序依赖的东西,最常见的就是引入一个基础操作系统镜像,但这样往往会使得编译出来的镜像特别大。但是对于go语言,我们可以使用静态编译的方式构建出超小的镜像。有人会问Go本身不就是静态编译吗?请接着往下看。示例程序package main import ( "fmt" "io/ioutil" "net/http" "os" ) func main() { resp, err := http.Get("http://baidu.com") check(err) body, err := ioutil.ReadAll(resp.Body) check(err) fmt.Println(len(body)) } func check(err error) { if err != nil { fmt.Println(err) os.Exit(1) } }这个程序很简单,就是访问一个网页,然后打印body的大小。构建docker镜像使用golang:onbuild镜像我们先简单介绍一下golang:onbuild镜像以及如何使用它。这个镜像是golang官方提供的一个用于编译运行go程序的镜像,它包含了很多ONBUILD触发器,可以涵盖大多数Go程序。其实它就是预置了一些自动执行的命令:COPY . /go/src/app RUN go get -d -v RNU go install -v它使用起来很简单:创建一个目录,把你的go文件放到该目录。然后增加Dockerfile文件,内容就一行:FROM golang:onbuild然后执行docker build命令构建镜像。这个golang:onbuild镜像就会自动将这个目录下的所有文件拷贝过去,并编译安装。比如对于本文的例子,我创建了一个app-onbuild目录,里面是Dockerfile和app.go文件:➜ app-onbuild ll total 8.0K -rw-r--r-- 1 Allan 20 Jan 2 12:21 Dockerfile -rw-r--r-- 1 Allan 291 Jan 2 12:20 app.go ➜ app-onbuild cat Dockerfile FROM golang:onbuild然后我执行编译镜像的命令docker build -t app-onbuild .,整个过程如下:➜ app docker build -t app-onbuild . Sending build context to Docker daemon 3.072 kB Step 1 : FROM golang:onbuild onbuild: Pulling from library/golang 75a822cd7888: Already exists 57de64c72267: Pull complete 4306be1e8943: Pull complete 4ba540d49835: Pull complete 5b23b80eb526: Pull complete 981c210a3af4: Pull complete 73f7f7662eed: Pull complete 520a90f1995e: Pull complete Digest: sha256:d3cbc855152e8672412fc32d7f19371816d686b0dfddedb8fce86245910b31ac Status: Downloaded newer image for golang:onbuild # Executing 3 build triggers... Step 1 : COPY . /go/src/app Step 1 : RUN go-wrapper download ---> Running in 4d183d7e1e8a + exec go get -v -d Step 1 : RUN go-wrapper install ---> Running in 31b6371f1a4f + exec go install -v app ---> 94cc8fb334ea Removing intermediate container f13df1977590 Removing intermediate container 4d183d7e1e8a Removing intermediate container 31b6371f1a4f Successfully built 94cc8fb334ea我们可以看到整个过程如前面所述。编译完以后,golang:onbuild镜像默认还包含CMD ["app"]执行来运行编译出来的镜像。当然如果你的程序有参数,我们可以在启动的时候加命令行参数。最后让我们来看看golang:onbuild的Dockerfile吧(这里以目前最新的1.8为例):FROM golang:1.8 RUN mkdir -p /go/src/app WORKDIR /go/src/app # this will ideally be built by the ONBUILD below ;) CMD ["go-wrapper", "run"] ONBUILD COPY . /go/src/app ONBUILD RUN go-wrapper download ONBUILD RUN go-wrapper install我们可以看到其实golang:onbuild镜像其实引用的还是golang标准镜像,只不过封装了一些自动执行的动作,使用使用起来更加方便而已。接下里我们看看如何直接基于标准的golang镜像来构建我们自己的镜像。使用golang:latest镜像相比于使用golang:onbuild的便利性,golang:latest给了我们更多的灵活性。我们以构建app镜像为例:创建app-golang目录,目录内容如下所示:➜ app-golang ll total 8.0K -rw-r--r-- 1 Allan 101 Jan 2 12:32 Dockerfile -rw-r--r-- 1 Allan 291 Jan 2 12:32 app.go ➜ app-golang cat Dockerfile FROM golang:latest RUN mkdir /app ADD . /app/ WORKDIR /app RUN go build -o main . CMD ["/app/main"]执行构建命令docker build -t app-golang .➜ app-golang docker build -t app-golang . Sending build context to Docker daemon 3.072 kB Step 1 : FROM golang:latest latest: Pulling from library/golang 75a822cd7888: Already exists 57de64c72267: Already exists 4306be1e8943: Already exists 4ba540d49835: Already exists 5b23b80eb526: Already exists 981c210a3af4: Already exists 73f7f7662eed: Already exists Digest: sha256:5787421a0314390ca8da11b26885502b58837ebdffda0f557521790c13ddb55f Status: Downloaded newer image for golang:latest ---> 6639f812dbc7 Step 2 : RUN mkdir /app ---> Running in a6f105ecc042 ---> f73030d40507 Removing intermediate container a6f105ecc042 Step 3 : ADD . /app/ ---> 3fcc194ce29d Removing intermediate container 013c2192f90e Step 4 : WORKDIR /app ---> Running in b8a2ca8d7ae0 ---> 853dfe15c6cd Removing intermediate container b8a2ca8d7ae0 Step 5 : RUN go build -o main . ---> Running in e0de5c273d7b ---> 28ef112e8c23 Removing intermediate container e0de5c273d7b Step 6 : CMD /app/main ---> Running in 82c67389d9ab ---> 139ad10f61dc Removing intermediate container 82c67389d9ab Successfully built 139ad10f61dc其实golang标准镜像还有一个非常方便的用途。比如我们需要开发go应用,但是又不想安装go环境或者还没有安装,那么我们可以直接使用golang标准镜像来在容器里面编译go程序。假设当前目录是我们的工程目录,那么我们可以使用如下命令来编译我们的go工程:$ docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp golang:latest go build -v这条命令的含义是把当前目录作为一个卷挂载到golang:latest镜像的/usr/src/myapp目录下,并将该目录设置为工作目录,然后在该目录下执行go build -v,这样就会在该目录下编译出一个名为myapp的可执行文件。当然默认编译出的是linux/amd64架构的二进制文件,如果我们需要编译其他系统架构的文件,可以加上相应的参数,比如我们要编译Windows下的32位的二进制文件,可执行如下命令:$ docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp -e GOOS=windows -e GOARCH=386 golang:latest go build -v当然,我们也可以shell脚本一次编译出多种OS下的文件:$ docker run --rm -it -v "$PWD":/usr/src/myapp -w /usr/src/myapp golang:latest bash $ for GOOS in darwin linux windows; do > for GOARCH in 386 amd64; do > go build -v -o myapp-$GOOS-$GOARCH > done > doneOK,言归正传,我们继续来进行本文要讨论的话题。我们看一下上述两种方式编译出来的镜像的大小:➜ app-golang docker images | grep -e golang -e app app-golang latest 139ad10f61dc 36 minutes ago 679.3 MB app-onbuild latest 94cc8fb334ea 39 minutes ago 679.3 MB golang onbuild a422f764b58c 2 weeks ago 674 MB golang latest 6639f812dbc7 2 weeks ago 674 MB可以看到,golang-onbuild和golang-latest两个基础镜像大小都为647MB,而我们编译出来的自己的镜像大小为679.3MB,也就是说我们自己的程序其实只有5.3MB。这是为什么呢?因为我们使用的两个基础镜像是通用镜像,他们包含了go依赖的所有东西。比如我们看一下golang-1.8的Dockerfile:FROM buildpack-deps:jessie-scm # gcc for cgo RUN apt-get update && apt-get install -y --no-install-recommends \ g++ \ gcc \ libc6-dev \ make \ pkg-config \ && rm -rf /var/lib/apt/lists/* ENV GOLANG_VERSION 1.8beta2 ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz ENV GOLANG_DOWNLOAD_SHA256 4cb9bfb0e82d665871b84070929d6eeb4d51af6bedbc8fdd3df5766e937ef84c RUN curl -fsSL "$GOLANG_DOWNLOAD_URL" -o golang.tar.gz \ && echo "$GOLANG_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c - \ && tar -C /usr/local -xzf golang.tar.gz \ && rm golang.tar.gz ENV GOPATH /go ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH" WORKDIR $GOPATH COPY go-wrapper /usr/local/bin/这样我们就不奇怪了,因为基础镜像里面包含了太多的东西,但其实我们的程序只使用了极少的一部分东西。下面我们就介绍一种方法可以只编译我们程序依赖的东西,这样编译出来的镜像非常的小。静态编译其实在生产环境中,对于Go应用我们往往是现在本地编译出一个可执行文件,然后将这个文件打进容器里面,而不是在容器里面进行编译,这样可以做到容器里面只有我们需要的东西,而不会引入一些只在编译过程中需要的文件。这种方式一般也有两种操作方法:第一种就是利用go build编译出二进制文件,然后在Dockerfile里面引入一个操作系统镜像作为程序运行环境。这样打出的镜像也比较大,但是却不会有编译过程中依赖的一些文件,所以相比较之前的方式,这种方式打出来的镜像也会小很多。在镜像大小不是特别重要的场景下,我比较喜欢这种方式,因为基础的操作系统往往带了一些基础的命令,如果我们的程序有些异常什么的,我们可以登录到这些容器里面去查看。比如可以使用ps、netstat等命令。但如果没有操作系统的话,这些命令就都没法使用了。当然,本文讨论的就是如何构建最小的镜像,所以接下来我们介绍第二种操作方法:利用scratch镜像构建最小的Go程序镜像。scratch镜像其实是一个特殊的镜像,为什么特殊呢?因为它是一个空镜像。但是它却是非常重要的。我们知道Dockerfile文件必须以FROM开头,但如果我们的镜像真的是不依赖任何其他东西的时候,我们就可以FROM scratch。在Docker 1.5.0之后,FROM scratch已经变成一个空操作(no-op),也就是说它不会再单独占一层了。OK,下面我们来进行具体的操作。我们先创建一个go-scratch目录,然后将先go build编译出来一个二进制文件拷贝到这个目录,并增加Dockerfile文件:➜ app-scratch ll total 5.1M -rw-r--r-- 1 Allan 36 Jan 2 13:44 Dockerfile -rwxr-xr-x 1 Allan 5.1M Jan 2 13:42 app ➜ app-scratch cat Dockerfile FROM scratch ADD app / CMD ["/app"]然后打镜像:➜ app-scratch docker build -t app-scratch . Sending build context to Docker daemon 5.281 MB Step 1 : FROM scratch ---> Step 2 : ADD app / ---> 65d4b96cf3a3 Removing intermediate container 2a6498e02c75 Step 3 : CMD /app ---> Running in c8f2958f09e2 ---> dcd05e331135 Removing intermediate container c8f2958f09e2 Successfully built dcd05e331135可以看到,FROM scratch并没有单独占一层。然后我们运行构建出来的镜像:➜ app-scratch docker images app-scratch REPOSITORY TAG IMAGE ID CREATED SIZE app-scratch latest 7ef9c5620b4f 5 minutes ago 5.259 MB只有5.259MB,和之前相比是不是超级小呢?NB(不是牛逼,是nota bene):我们知道Go的编译是静态的,但是如果你的Go版本是1.5之前的,那你编译你的程序时最好使用如下命令去编译:CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .因为Go 1.5之前Go依赖了一些C的一些库,Go编译的时候是动态链接这些库的。这样你使用scratch的方式打出来的镜像在运行时会因为找不到这些库而报错。CGO_ENABLED=0表示静态编译cgo,里面的GOOS改为你程序运行的目标机器的系统,-a的意思是重新编译所有包。参考:https://blog.codeship.com/building-minimal-docker-containers-for-go-applications/https://hub.docker.com/_/golang/ 搭建registry mirror http://niyanchun.com/deploy-registry-mirror.html 2016-12-19T22:51:00+08:00 Docker registry是专门用于存放docker镜像的,docker官方提供了docker hub,是全球最大的docker镜像存储中心。但是在中国既没有服务器也没有CDN,所以导致pull镜像特别的慢,而且很不稳定。解决这个问题的方式一般有两种:搭建自己私有的docker registry,存储镜像,并定期同步官方常用的镜像。搭建docker mirror。其实,选用哪一种或者both主要取决于自己的使用场景。如果你想要一个类似docker hub的私有registry,那肯定是第一种。如果你只是想解决从docker hub拉取镜像慢的问题,那就选择第二种,因为第一种的维护成本比较高。当然,不管是哪一种,我们都可以使用docker官方开源的registry镜像去实现。如果你想部署私有的registry,可以关注一下VMware开源的Harbor项目,其在开源registry的基础上增加了一些实际应用场景中需要的一些特性,比如项目权限、角色管理等。本文介绍如何使用部署registry mirror。在这之前,需要强调一下目前如果registry部署为mirror方式,将只能pull镜像而不能push镜像。registry mirror原理Docker Hub的镜像数据分为两部分:index数据和registry数据。前者保存了镜像的一些元数据信息,数据量很小;后者保存了镜像的实际数据,数据量比较大。平时我们使用docker pull命令拉取一个镜像时的过程是:先去index获取镜像的一些元数据,然后再去registry获取镜像数据。所谓registry mirror就是搭建一个registry,然后将docker hub的registry数据缓存到自己本地的registry。整个过程是:当我们使用docker pull去拉镜像的时候,会先从我们本地的registry mirror去获取镜像数据,如果不存在,registry mirror会先从docker hub的registry拉取数据进行缓存,再传给我们。而且整个过程是流式的,registry mirror并不会等全部缓存完再给我们传,而且边缓存边给客户端传。对于缓存,我们都知道一致性非常重要。registry mirror与docker官方保持一致的方法是:registry mirror只是缓存了docker hub的registry数据,并不缓存index数据。所以我们pull镜像的时候会先连docker hub的index获取镜像的元数据,如果我们registry mirror里面有该镜像的缓存,且数据与从index处获取到的元数据一致,则从registry mirror拉取;如果我们的registry mirror有该镜像的缓存,但数据与index处获取的元数据不一致,或者根本就没有该镜像的缓存,则先从docker hub的registry缓存或者更新数据。registry mirror的工作原理我们就介绍完了,下面介绍如何利用docker开源的registry镜像部署自己的registry mirror。registry mirror部署NB:假设我们将缓存的数据存放到/data目录。从官方拉取registry的镜像,目前最新的registry镜像是2.5版本(我使用的是2.5.0)。获取registry的默认配置:docker run -it --rm --entrypoint cat registry:2.5.0 /etc/docker/registry/config.yml > config.yml文件的内容大概是下面这样:version: 0.1 log: fields: service: registry storage: cache: blobdescriptor: inmemory filesystem: rootdirectory: /var/lib/registry http: addr: :5000 headers: X-Content-Type-Options: [nosniff] health: storagedriver: enabled: true interval: 10s threshold: 3我们在最后面加上如下配置:proxy: remoteurl: https://registry-1.docker.io username: [username] password: [password]username和password是可选的,如果配置了的话,那registry mirror除了可以缓存所有的公共镜像外,也可以访问这个用户所有的私有镜像。启动registry容器:docker run --restart=always -p 5000:5000 --name v2-mirror -v /data:/var/lib/registry -v $PWD/config.yml:/etc/registry/config.yml registry:2.5.0 /etc/registry/config.yml当然我们也可以使用docker-compose启动:version: '2' services: registry: image: library/registry:2.5.0 container_name: registry_mirror restart: always volumes: - /data:/var/lib/registry - ./config.yml:/etc/registry/config.yml ports: - 5000:5000 command: ["serve", "/etc/registry/config.yml"]当我们看到如下日志输出的时候就说明已经启动成功了:time="2016-12-19T14:22:35Z" level=warning msg="No HTTP secret provided - generated random secret. This may cause problems with uploads if multiple registries are behind a load-balancer. To provide a shared secret, fill in http.secret in the configuration file or set the REGISTRY_HTTP_SECRET environment variable." go.version=go1.6.3 instance.id=da5468c4-1ee1-4df2-95cf-1336127c87bb version=v2.5.0 time="2016-12-19T14:22:35Z" level=info msg="redis not configured" go.version=go1.6.3 instance.id=da5468c4-1ee1-4df2-95cf-1336127c87bb version=v2.5.0 time="2016-12-19T14:22:35Z" level=info msg="Starting upload purge in 39m0s" go.version=go1.6.3 instance.id=da5468c4-1ee1-4df2-95cf-1336127c87bb version=v2.5.0 time="2016-12-19T14:22:35Z" level=info msg="using inmemory blob descriptor cache" go.version=go1.6.3 instance.id=da5468c4-1ee1-4df2-95cf-1336127c87bb version=v2.5.0 time="2016-12-19T14:22:35Z" level=info msg="Starting cached object TTL expiration scheduler..." go.version=go1.6.3 instance.id=da5468c4-1ee1-4df2-95cf-1336127c87bb version=v2.5.0 time="2016-12-19T14:22:35Z" level=info msg="Registry configured as a proxy cache to https://registry-1.docker.io" go.version=go1.6.3 instance.id=da5468c4-1ee1-4df2-95cf-1336127c87bb version=v2.5.0 time="2016-12-19T14:22:35Z" level=info msg="listening on [::]:5000" go.version=go1.6.3 instance.id=da5468c4-1ee1-4df2-95cf-1336127c87bb version=v2.5.0至此,registrymirror就算部署完了。我们也可以用curl验证一下服务是否启动OK:curl -I http://registrycache.example.com:5000/v2/ HTTP/1.1 200 OK Content-Length: 2 Content-Type: application/json; charset=utf-8 Docker-Distribution-Api-Version: registry/2.0 Date: Thu, 17 Sep 2015 21:42:02 GMTregistry mirror使用要使用registry mirror,我们需要配置一下自己的docker daemon。对于Mac:在docker的客户端的Preferences——>Advanced——>Registry mirrors里面添加你的地址,然后重启。对于Ubuntu 14.04:在/etc/default/docker文件中添加DOCKER_OPTS="$DOCKER_OPTS --registry-mirror=http://registrycache.example.com:5000”,然后重启docker(services docker restart)。其他系统和发行版的配置方法请Google之。然后我们pull一个本地不存在的镜像,这时去查看registry mirror服务器的data目录下面已经有了数据。执行如下命令也可以看到效果:curl https://mycache.example.com:5000/v2/library/busybox/tags/list需要说明的是缓存的镜像的有效期默认是一周(168hour),而且如果registry被配置成mirror模式,这个时间是不能通过maintenance部分来改变的: maintenance: uploadpurging: enabled: true age: 168h interval: 24h dryrun: false readonly: enabled: false我研究了好久发现怎么改都不能生效,最后发现mirror模式下这个时间竟然在registry的代码里面写死了:// todo(richardscothern): from cache control header or config const repositoryTTL = time.Duration(24 * 7 * time.Hour)囧o(╯□╰)o~不过看这TODO,应该是后面会改的~当然如果你想你的registry mirror是https的话,在config.yml的http部分增加tls配置即可: http: addr: :5000 headers: X-Content-Type-Options: [nosniff] tls: certificate: /etc/registry/domain.crt key: /etc/registry/domain.keyOK,至此registry mirror就介绍完了。 Harbor多节点部署实践 http://niyanchun.com/harbor-multi-nodes-deployment-practices.html 2016-12-13T17:22:00+08:00 Harbor是VMware开源的一套企业级Registry解决方案,功能比较丰富,特别是增加了角色管理、镜像复制等在实际场景中非常有用的功能。项目地址见:https://github.com/vmware/harbor。本文介绍一种多节点配置方法,主要是实现HA。1. 部署架构这个架构所有Harbor共享存储。这个存储包含两个部分:(1)数据库存储(主要包括用户信息、工程信息、日志信息等)。(2)镜像信息。这里我使用的共享存储是AWS的RDS和S3,当然其他任何共享的存储都应该是可以的。2. 部署过程2.1 部署HarborHarbor的部署方式与单机部署一样,不过需要修改存储相关的部分为共享存储。修改点如下:修改数据库相关的存储:(1)修改harbor/make/common/templates/ui和harbor/make/common/templates/jobservice下面的env文件,将其中的MYSQL_HOST、MYSQL_PORT、MYSQL_USR等信息改为我的AWS RDS的信息。(2)修改harbor/make/harbor.cfg文件,将其中的db_password密码配置为RDS中用户的密码。修改镜像相关存储:修改harbor/make/common/templates/registry下的config.yml,将默认的本地存储改为S3.比如我的改完之后信息如下:version: 0.1 log: level: debug fields: service: registry storage: cache: layerinfo: inmemory s3: accesskey:****** secretkey:***** region: cn-north-1 bucket:***** rootdirectory: /s3/harbor/registry maintenance: uploadpurging: enabled: false delete: enabled: true http: addr: :5000 secret: placeholder debug: addr: localhost:5001 auth: token: issuer: registry-token-issuer realm: $ui_url:8080/service/token rootcertbundle: /etc/registry/root.crt service: token-service notifications: endpoints: - name: harbor disabled: false url: http://ui/service/notifications timeout: 3000ms threshold: 5 backoff: 1s重要:修改harbor/make/common/templates/nginx/nginx.http.conf文件,注释掉其中的location /v2/中的proxy_set_header X-Forwarded-Proto $$scheme;,不然到时候docker pull和push镜像的时候会有问题。集群内的Harbor都按照这种方式部署。2.2 部署LB我是用Nginx做了一个LB,其配置文件如下:worker_processes auto; events { worker_connections 1024; use epoll; multi_accept on; } http { tcp_nodelay on; # this is necessary for us to be able to disable request buffering in all cases proxy_http_version 1.1; upstream harbor { ip_hash; server xxx.xxx.xxx.xxx:8080; server xxx.xxx.xxx.yyy:8080; } server { listen 443; server_name xxxx.com; ssl on; #root html; #index index.html index.htm; ssl_certificate /***/***.pem; ssl_certificate_key /***/***.key; ssl_session_timeout 5m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers AESGCM:ALL:!DH:!EXPORT:!RC4:+HIGH:!MEDIUM:!LOW:!aNULL:!eNULL; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; # disable any limits to avoid HTTP 413 for large image uploads client_max_body_size 0; # required to avoid HTTP 411: see Issue #1486 (https://github.com/docker/docker/issues/1486) chunked_transfer_encoding on; location / { proxy_pass http://harbor/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # When setting up Harbor behind other proxy, such as an Nginx instance, remove the below line if the proxy already has similar settings. proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; proxy_request_buffering off; } } }我部署了两台harbor,监听端口都是8080,而且都是http的,而这个LB是https的。对于LB,有一个注意点就是upstream中需要使用ip_hash方式轮询,否则会有session问题。这样整个部署就完成了。此时你可以使用https://xxx.com去访问harbor了。3. 其他之前我的LB使用的也是http,不修改harbor/make/common/templates/nginx/nginx.http.conf文件也不会有什么问题,但换成https之后,不修改pull和push就会一直Retrying,还不知道是什么问题。如果后端存储使用S3的话,需要使用#1244之后的Harbor版本,不然pull和push的时候也会有问题。当然这种部署方式主要是为了HA:只要集群内有一台Harbor正常工作,系统就是可用的。但是因为后端使用的是共享存储,所以实际使用中可能在存储这块会有性能瓶颈。另外,这种多实例使用共享存储是否会有问题,也没有经过生产验证。本文的部署方式只是对Harbor多节点部署方式的一种探索与实践,暂时还不保证生产环境使用会有什么问题。4. 补充(更新于2017.1.24)最近发现之前方案部署时一些可能出现的问题,这里再补充一下。token问题之前的部署方案如果后台的Harbor没有公网IP的话,就会有问题。而多节点部署时,后台的Harbor都是在私网内,只有前端的LB暴露在公网。而registry得配置文件里面需要配置一个获取token的地址,这个地址必须是公网可以访问的。因为registry会把这个返回给访问registry的用户,让用户去获取token,如果公网没法访问,用户就没法docker login了。所以,如果后台的Harbor是在私网内,那在原来配置的基础上需要将harbor/make/common/templates/nginx/nginx.http.conf文件里面的realm配置为LB的公网IP(http(s)://LB公网IP或域名/service/token)。多节点部署的时候多个Harbor的private_key.pem文件要一样,否则可能出现在一台上面生成的token到另外一台验证不通过。需要注意的是执行./prepare命令默认会重新生成registry用来生成token的密钥文件,所以要么加参数让不重新生成pem文件,要么就将一台生成的拷贝到其他节点上去。多个服务器的时间也要完全一致,不然可能出现token过期或者时间未到的问题。 理解Docker的镜像和容器 http://niyanchun.com/images-and-containers.html 2016-11-02T22:03:00+08:00 本文参阅自Docker官方的一篇文档:《Understand images, containers, and storage drivers》。原文有些长,有些啰嗦,所以我翻译精简了一下。如果你的英语还可以,强烈建议读原文。Docker的口号是“Build, Ship and Run Any App, Anywhere.”,大概意思就是编译好一个应用后,可以在任何地方运行,不会像传统的程序一样,一旦换了运行环境,往往就会出现缺这个库,少那个包的问题。那么Docker是怎么做到这点的呢?简单说就是它在编译应用的时候把这个应用依赖的所有东西都编译到了镜像里面(有点像程序的静态编译——只是像而已)。我们把这个编译好的静态的东西叫docker镜像(Image),然后当docker deamon(docker的守护进程/服务进程)运行这个镜像的时候,我们称其为docker容器(Container)。好吧,你可以简单理解docker镜像和docker容器的关系就像是程序和进程的关系一样(当然实质是不一样的)。等读完此文,你会对它们的细节有一个更深入的了解,也会对docker有一个非常深入的了解(然而,如果你想深入学习docker,这还是远远不够滴...^_^)。1. Images和Layers每个Docker镜像(Image)都引用了一些只读的(read-only)层(layer),不同的文件系统layer也不同。这些layer堆叠在一起构成了容器(Container)的根文件系统(root filesystem)。下图是Ubuntu 15.04的镜像,共由4个镜像层(image layer)组成:Docker的storage driver负责堆叠这些layer,并对外提供一个统一的视图。当你基于Ubuntu 15.04创建一个新的容器的时候,你其实只是在它的上层又增加了一个新的、薄的、可写层。这个新增的可写层称为容器层(container layer)。当这个新的容器运行时,所有的改动(比如创建新文件、修改已有文件、删除文件等)都会写到这一层。下图展示了一个新容器的结构:新旧版本Content addressable storage的变化:“Content addressable storage”是什么意思呢?其实主要是说layer的在磁盘存储时地址/名字选取的策略。Docker 1.10之前会给每个layer随机生成一个UUID作为其标识,这个UUID也是存储该layer层数据的目录名字。Docker1.10版本引入了一种新的方式——内容哈希(content hash),这种方式更加安全,而且通过内部实现避免了ID冲突的问题,还可以保证pull、push、load、save等操作之后数据的完整性(integrity)。除此以外,它可以让不同Image之间更好的共享一些公共的layer,哪怕他们不是从同一个Dockerfile构建来的(layer共享的问题后面会讲)。下图展示了新版本的这个变化:我们可以看到所有的镜像层的ID都是由密码散列而来,而容器层的ID依旧是随机生成的UUID。这个变化看似只是名字的变化,但其实二者是不兼容的。所以旧版本升级到新版本后,数据也必须做迁移才可以使用。当然官方提供了迁移工具,原文也给了例子,这里就不翻译了,有兴趣的去看原文。2. Container和Layers容器和镜像的主要区别就是顶部的那个可写层(即之前说的那个“container layer”)。容器运行时做的所有操作都会写到这个可写层里面,当容器删除的时候,这个可写层也会被删掉,但底层的镜像依旧保持不变。所以,不同的容器都有自己的可写层,但可以共享同一个底层镜像。下图展示了多个容器共享同一个Ubuntu 15.04镜像。Docker的storage driver负责管理只读的镜像层和可写的容器层,当然不同的driver实现的方式也不同,但其后都有两项关键技术:可堆叠的镜像层(stackable image layer)和写时拷贝技术(copy-on-write, CoW)。3. 写时拷贝策略系统内需要同一份数据的进程共享同一份数据实例,只有当某个进程需要对这些数据做修改时,它才会把数据拷贝一份做修改,而这个修改也只对它自己可见,其它的进程依旧使用的是原来的数据。Docker使用这个技术极大的减小了镜像对于磁盘空间的占用以及容器的启动时间(如果某个容器的镜像层已经被其他容器启动了,那它就只需启动自己的容器层就可以了)。比如对于AUFS和OverlayFS这两个storage driver,他们的CoW流程大致如下:从最新的layer自上而下,一层一层寻找需要更新的镜像层。当找到需要更新的文件的时候,就执行“copy-up”操作。这个操作其实就是把这个文件拷贝到自己的可写层/容器层去。修改自己可写层的这个拷贝。当然,当文件比较大,或者镜像的层数比较多等情况下,这个“copy-up”操作成为性能负担。不过好在对于同一个文件的多次修改只会拷贝一次。4. 数据持久化刚开始的时候,docker一般只适用于无状态的计算场景使用。但随着发展,docker通过data volume技术也可以做到数据持久化了。data volume就是我们将主机的某个目录挂载到容器里面,这个data volume不受storage driver的控制,所有对这个data volume的操作会绕过storage driver直接其操作,其性能也只受本地主机的限制。而且我们可以挂载任意多个data volume到容器中,不同容器也可以共享同一个data volume。下图展示了一个Docker主机上面运行着两个容器.每一个容器在主机上面都有着自己的地址空间(/var/lib/docker/...),除此以外,它们还共享着主机上面的同一个/data目录。更多关于data volume的信息,请参阅 Manage data in containers. Ubuntu安装Docker http://niyanchun.com/install-docker-on-ubuntu.html 2016-09-09T19:06:00+08:00 1. 先决条件(1)Docker现在支持以下版本的Ubuntu:Ubuntu Xenial 16.04 LTSUbuntu Wily 15.10Ubuntu Trusty 14.04 LTSUbuntu Precise 12.04 LTSPS:虽然Ubuntu 14.10和15.04在Docker的APT源里面也有,但是官方已经不再提供支持了。(2)Docker只支持64位系统,且内核版本不低于3.10.如果该要求不满足,请更换64位Ubuntu系统或者升级内核版本。(3)对于Ubuntu 14.04、15.10、16.04版本,推荐安装linux-image-extra-*内核包,这样就可以使用aufs存储器。安装方法如下:$ sudo apt-get update $ sudo apt-get install linux-image-extra-$(uname -r) linux-image-extra-virtual2. 增加Docker的源(1)使apt支持https以及增加根证书。$ sudo apt-get update $ sudo apt-get install apt-transport-https ca-certificates(2)增加GPG key。$ sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D(3)打开/etc/apt/sources.list.d/docker.list文件(不存在则创建)并清空。然后增加下面的内容:Ubuntu 12.04deb https://apt.dockerproject.org/repo ubuntu-precise mainUbuntu 14.04deb https://apt.dockerproject.org/repo ubuntu-trusty mainUbuntu 15.10deb https://apt.dockerproject.org/repo ubuntu-wily mainUbuntu 16.04deb https://apt.dockerproject.org/repo ubuntu-xenial main保存关闭。(4)然后执行下面的命令。$ sudo apt-get update # 更新源 $ sudo apt-get purge lxc-docker # 清除旧的仓库 $ apt-cache policy docker-engine # 查看获取docker的源是否正确3. 安装分别执行下面的命令:$ sudo apt-get update # 更新源 $ sudo apt-get install docker-engine # 安装Docker $ sudo service docker start # 启动docker守护进程检查docker是否安装正确:$ sudo docker run hello-world 这个命令会下载最新的测试镜像并在容器里面运行,运行时会打印一条信息,然后退出。4. 可选配置4.1 用户组配置因为docker守护进程使用的是Unix套接字,默认情况下unix套接字属于root,其他用户只能通过sudo访问。所以docker守护进程总是以root用户运行。为了避免我们每次使用docker命令都要加sudo,我们可以单独创建一个docker组,然后把使用docker的用户加到这个组里面。当docker守护进程启动的时候,它让该组对Unix套接字具有读写权限(当然,这会带来一定的安全风险)。命令如下:$ sudo groupadd docker # 组可能已经存在 $ sudo usermod -aG docker $USER # 把当前用户加入到docker组然后注销该用户重新登录,我们以该用户执行docker命令就不需要再加sudo了。4.2 配置Docker开机自启Ubuntu 15.04及之后的版本使用systemctl控制自启。所以执行如下命令即可:$ sudo systemctl enable dockerUbuntu 14.10及之前版本安装时docker会自动配置upstart开机自启。4.3 升级Docker$ sudo apt-get upgrade docker-engine4.4 卸载Docker$ sudo apt-get purge docker-engine # 卸载Docker包 $ sudo apt-get autoremove --purge docker-engine # 卸载Docker包依赖的一些不再使用的包 $ sudo rm -rf /var/lib/docker # 删除所有的镜像及容器等原文网址:https://docs.docker.com/engine/installation/linux/ubuntulinux/5. 其它因为墙的原因,很可能从docker官方安装docker特别缓慢,甚至下载不下来。这时可以从国内的一些源安装,比如可以使用下面命令安装docker-engine:curl -sSL https://get.daocloud.io/docker | shdocker安装好以后,默认下载docker镜像是从Docker Hub下载的,对于国内用户,极其不稳定,而且速度堪忧。所以国内一些公司提供了镜像(Docker Mirror),其原理就是如果你需要下载的镜像它那里已经存在,就会直接从它那里下载,如果它那里不存在,它就先替你下载,然后你再从它那里下载。目前国内提供该服务的公司很多,比如阿里云加速器、DaoCloud加速器、灵雀云加速器等。你注册以后,会给你生成一个url,将这个url加入你的docker配置文件中,重启docker守护进程即可生效:对于upstart系统(比如Ubuntu 14.04),编辑/etc/default/docker文件(将其中的url替换为你的url):DOCKER_OPTS="--registry-mirror=https://xxxx.mirror.aliyuncs.com"对于其他系统请自行Google修改方法。