Kubernetes-秘籍-三-

80 阅读52分钟

Kubernetes 秘籍(三)

原文:zh.annas-archive.org/md5/6F444487B7AC74DB6092F54D9EA36B7A

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章:构建高可用性集群

在本章中,我们将涵盖以下配方:

  • 集群化 etcd

  • 构建多个主节点

介绍

避免单点故障是一个我们需要时刻牢记的概念。在本章中,您将学习如何在 Kubernetes 中构建具有高可用性的组件。我们还将介绍构建一个三节点 etcd 集群和多节点主节点的步骤。

集群化 etcd

etcd 在 Kubernetes 中存储网络信息和状态。任何数据丢失都可能是至关重要的。在生产环境中强烈建议对 etcd 进行集群化。etcd 支持集群化;N 个成员的集群可以容忍最多(N-1)/2 个故障。通常有三种创建 etcd 集群的机制。它们如下:

  • 静态

  • etcd 发现

  • DNS 发现

如果我们在启动之前已经为所有 etcd 成员进行了配置,静态是一种简单的引导 etcd 集群的方法。然而,如果我们使用现有的 etcd 集群来引导新成员,那么发现方法就会发挥作用。发现服务使用现有集群来引导自身。它允许 etcd 集群中的新成员找到其他现有成员。在这个配方中,我们将讨论如何通过静态和 etcd 发现手动引导 etcd 集群。

我们在第一章中学习了如何使用 kubeadm 和 kubespray,构建您自己的 Kubernetes 集群。在撰写本文时,kubeadm 中的 HA 工作仍在进行中。官方文档建议定期备份您的 etcd 节点。另一个我们介绍的工具 kubespray 则原生支持多节点 etcd。在本章中,我们还将描述如何在 kubespray 中配置 etcd。

准备就绪

在我们学习更灵活的设置 etcd 集群之前,我们应该知道 etcd 目前有两个主要版本,即 v2 和 v3。etcd3 是一个旨在更稳定、高效和可靠的更新版本。以下是一个简单的比较,介绍它们实现中的主要区别:

etcd2etcd3
协议httpgRPC
密钥过期TTL 机制租约
观察者通过 HTTP 进行长轮询通过双向 gRPC 流

etcd3 旨在成为 etcd2 的下一代。etcd3 默认支持 gRPC 协议。gRPC 使用 HTTP2,允许在 TCP 连接上进行多个 RPC 流。然而,在 etcd2 中,每个 HTTP 请求必须在其进行的每个请求中建立连接。对于处理密钥到期,在 etcd2 中,TTL 附加到密钥;客户端应定期刷新密钥以查看是否有任何密钥已过期。这将建立大量连接。

在 etcd3 中,引入了租约的概念。租约可以附加多个键;当租约到期时,它将删除所有附加的键。对于观察者,etcd2 客户端通过 HTTP 创建长轮询-这意味着每次观察都会打开一个 TCP 连接。然而,etcd3 使用双向 gRPC 流实现,允许多个流共享同一个连接。

尽管 etcd3 更受青睐。但是,一些部署仍在使用 etcd2。我们仍然会介绍如何使用这些工具来实现集群,因为 etcd 中的数据迁移有很好的文档记录并且顺利。有关更多信息,请参考coreos.com/blog/migrating-applications-etcd-v3.html上的升级迁移步骤。

在我们开始构建 etcd 集群之前,我们必须决定需要多少成员。etcd 集群的规模取决于您想要创建的环境。在生产环境中,建议至少有三个成员。然后,集群可以容忍至少一个永久性故障。在本教程中,我们将使用三个成员作为开发环境的示例:

名称/主机名IP 地址
ip-172-31-3-80172.31.3.80
ip-172-31-14-133172.31.14.133
ip-172-31-13-239172.31.13.239

其次,etcd 服务需要端口 23794001用于旧版本)用于 etcd 客户端通信,端口 2380用于对等通信。这些端口必须在您的环境中暴露。

如何做...

有很多方法可以提供 etcd 集群。通常,您会使用 kubespray、kops(在 AWS 中)或其他提供工具。

在这里,我们将简单地向您展示如何执行手动安装。这也很容易:

// etcd installation script
$ cat install-etcd.sh
ETCD_VER=v3.3.0

# ${DOWNLOAD_URL} could be ${GOOGLE_URL} or ${GITHUB_URL}
GOOGLE_URL=https://storage.googleapis.com/etcd
GITHUB_URL=https://github.com/coreos/etcd/releases/download
DOWNLOAD_URL=${GOOGLE_URL}

# delete tmp files
rm -f /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz
rm -rf /tmp/etcd && rm -rf /etc/etcd && mkdir -p /etc/etcd

curl -L ${DOWNLOAD_URL}/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -o /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz
tar xzvf /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz -C /etc/etcd --strip-components=1
rm -f /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz

# check etcd version
/etc/etcd/etcd --version

此脚本将在/etc/etcd文件夹下放置etcd二进制文件。您可以自由地将它们放在不同的位置。在这种情况下,我们需要sudo来将它们放在/etc下:

// install etcd on linux
# sudo sh install-etcd.sh

etcd Version: 3.3.0
Git SHA: c23606781
Go Version: go1.9.3
Go OS/Arch: linux/amd64

我们现在使用的版本是 3.3.0。在检查etcd二进制文件在您的机器上工作后,我们可以将其附加到默认的$PATH上。然后我们就不需要每次执行etcd命令时都包含/etc/etcd路径了:

$ export PATH=/etc/etcd:$PATH

$ export ETCDCTL_API=3

您还可以将其放入您的.bashrc.bash_profile中,以便默认设置它。

在至少有三个 etcd 服务器供应后,是时候让它们配对了。

静态机制

静态机制是设置集群的最简单方式。但是,每个成员的 IP 地址都应该事先知道。这意味着如果在云提供商环境中引导 etcd 集群,则静态机制可能不太实用。因此,etcd 还提供了一种发现机制,可以从现有集群中引导自己。

为了使 etcd 通信安全,etcd 支持 TLS 通道来加密对等方之间以及客户端和服务器之间的通信。每个成员都需要有一个唯一的密钥对。在本节中,我们将向您展示如何使用自动生成的证书来构建一个集群。

在 CoreOs GitHub 中,有一个方便的工具,我们可以用来生成自签名证书(github.com/coreos/etcd/tree/v3.2.15/hack/tls-setup)。克隆存储库后,我们必须修改config/req-csr.json下的配置文件。这是一个例子:

// sample config, put under $repo/config/req-csr.json
$ cat config/req-csr.json
{
  "CN": "etcd",
  "hosts": [
    "172.31.3.80",
    "172.31.14.133",
    "172.31.13.239"
  ],
  "key": {
    "algo": "ecdsa",
    "size": 384
  },
  "names": [
    {
      "O": "autogenerated",
      "OU": "etcd cluster",
      "L": "the internet"
    }
  ]
}

在下一步中,我们需要安装并设置 Go(golang.org/)和设置$GOPATH

$ export GOPATH=$HOME/go

$ make

然后证书将在./certs/下生成。

首先,我们必须设置一个引导配置来声明集群中将有哪些成员:

// set as environment variables, or alternatively, passing by-initial-cluster and-initial-cluster-state parameters inside launch command.
# ETCD_INITIAL_CLUSTER="etcd0=http://172.31.3.80:2380,etcd1=http://172.31.14.133:2380,etcd2=http://172.31.13.239:2380"
ETCD_INITIAL_CLUSTER_STATE=new

在所有三个节点中,我们都需要单独启动 etcd 服务器:

// first node: 172.31.3.80
# etcd --name etcd0 --initial-advertise-peer-urls https://172.31.3.80:2380 \
  --listen-peer-urls https://172.31.3.80:2380 \
  --listen-client-urls https://172.31.3.80:2379,https://127.0.0.1:2379 \
  --advertise-client-urls https://172.31.3.80:2379 \
  --initial-cluster-token etcd-cluster-1 \
  --initial-cluster etcd0=https://172.31.3.80:2380,etcd1=https://172.31.14.133:2380,etcd2=https://172.31.13.239:2380 \
  --initial-cluster-state new \
  --auto-tls \
  --peer-auto-tls

然后,您将看到以下输出:

2018-02-06 22:15:20.508687 I | etcdmain: etcd Version: 3.3.0
2018-02-06 22:15:20.508726 I | etcdmain: Git SHA: c23606781
2018-02-06 22:15:20.508794 I | etcdmain: Go Version: go1.9.3
2018-02-06 22:15:20.508824 I | etcdmain: Go OS/Arch: linux/amd64

2018-02-06 22:15:21.439067 N | etcdserver/membership: set the initial cluster version to 3.0
2018-02-06 22:15:21.439134 I | etcdserver/api: enabled capabilities for version 3.0

让我们唤醒第二个etcd服务:

// second node: 172.31.14.133
$ etcd --name etcd1 --initial-advertise-peer-urls https://172.31.14.133:2380 \
  --listen-peer-urls https://172.31.14.133:2380 \
  --listen-client-urls https://172.31.14.133:2379,https://127.0.0.1:2379 \
  --advertise-client-urls https://172.31.14.133:2379 \
  --initial-cluster-token etcd-cluster-1 \
  --initial-cluster etcd0=https://172.31.3.80:2380,etcd1=https://172.31.14.133:2380,etcd2=https://172.31.13.239:2380 \
  --initial-cluster-state new \
  --auto-tls \
  --peer-auto-tls

您将在控制台中看到类似的日志:

2018-02-06 22:15:20.646320 I | etcdserver: starting member ce7c9e3024722f01 in cluster a7e82f7083dba2c1
2018-02-06 22:15:20.646384 I | raft: ce7c9e3024722f01 became follower at term 0
2018-02-06 22:15:20.646397 I | raft: newRaft ce7c9e3024722f01 [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]
2018-02-06 22:15:20.646403 I | raft: ce7c9e3024722f01 became follower at term 1

2018-02-06 22:15:20.675928 I | rafthttp: starting peer 25654e0e7ea045f8...
2018-02-06 22:15:20.676024 I | rafthttp: started HTTP pipelining with peer 25654e0e7ea045f8
2018-02-06 22:15:20.678515 I | rafthttp: started streaming with peer 25654e0e7ea045f8 (writer)
2018-02-06 22:15:20.678717 I | rafthttp: started streaming with peer 25654e0e7ea045f8 (writer)

它开始与我们之前的节点(25654e0e7ea045f8)配对。让我们在第三个节点中触发以下命令:

// third node: 172.31.13.239
$ etcd --name etcd2 --initial-advertise-peer-urls https://172.31.13.239:2380 \
  --listen-peer-urls https://172.31.13.239:2380 \
  --listen-client-urls https://172.31.13.239:2379,https://127.0.0.1:2379 \
  --advertise-client-urls https://172.31.13.239:2379 \
  --initial-cluster-token etcd-cluster-1 \
  --initial-cluster etcd0=https://172.31.3.80:2380,etcd1=https://172.31.14.133:2380,etcd2=https://172.31.13.239:2380 \
  --initial-cluster-state new \
  --auto-tls \
  --peer-auto-tls

// in node2 console, it listens and receives new member (4834416c2c1e751e) added.
2018-02-06 22:15:20.679548 I | rafthttp: starting peer 4834416c2c1e751e...
2018-02-06 22:15:20.679642 I | rafthttp: started HTTP pipelining with peer 4834416c2c1e751e
2018-02-06 22:15:20.679923 I | rafthttp: started streaming with peer 25654e0e7ea045f8 (stream Message reader)
2018-02-06 22:15:20.680190 I | rafthttp: started streaming with peer 25654e0e7ea045f8 (stream MsgApp v2 reader)
2018-02-06 22:15:20.680364 I | rafthttp: started streaming with peer 4834416c2c1e751e (writer)
2018-02-06 22:15:20.681880 I | rafthttp: started peer 4834416c2c1e751e
2018-02-06 22:15:20.681909 I | rafthttp: added peer 4834416c2c1e751e
After all nodes are in, it'll start to elect the leader inside the cluster, we could find it in the logs:
2018-02-06 22:15:21.334985 I | raft: raft.node: ce7c9e3024722f01 elected leader 4834416c2c1e751e at term 27
...
2018-02-06 22:17:21.510271 N | etcdserver/membership: updated the cluster version from 3.0 to 3.3
2018-02-06 22:17:21.510343 I | etcdserver/api: enabled capabilities for version 3.3

集群已经设置好了。我们应该检查一下它是否正常工作:

$ etcdctl cluster-health
member 25654e0e7ea045f8is healthy: got healthy result from http://172.31.3.80:2379
member ce7c9e3024722f01 is healthy: got healthy result from http://172.31.14.133:2379
member 4834416c2c1e751e is healthy: got healthy result from http://172.31.13.239:2379

发现机制

发现提供了一种更灵活的方式来创建集群。它不需要预先知道其他对等 IP。它使用现有的 etcd 集群来引导一个新的集群。在本节中,我们将演示如何利用它来启动一个三节点的 etcd 集群:

  1. 首先,我们需要有一个具有三节点配置的现有集群。幸运的是,etcd官方网站提供了一个发现服务(https://discovery.etcd.io/new?size=n);n 将是您的etcd集群中节点的数量,它已经准备好使用:
// get a request URL
# curl -w "n" 'https://discovery.etcd.io/new?size=3'
https://discovery.etcd.io/f6a3fb54b3fd1bb02e26a89fd40df0e8
  1. 然后我们可以使用 URL 轻松地引导一个集群。命令行基本上与静态机制中的相同。我们需要做的是将-initial-cluster改为-discovery,用于指定发现服务的 URL:
// in node1, 127.0.0.1 is used for internal client listeneretcd -name ip-172-31-3-80 -initial-advertise-peer-urls http://172.31.3.80:2380  -listen-peer-urls http://172.31.3.80:2380  -listen-client-urls http://172.31.3.80:2379,http://127.0.0.1:2379  -advertise-client-urls http://172.31.3.80:2379  -discovery https://discovery.etcd.io/f6a3fb54b3fd1bb02e26a89fd40df0e8

// in node2, 127.0.0.1 is used for internal client listener
etcd -name ip-172-31-14-133 -initial-advertise-peer-urls http://172.31.14.133:2380  -listen-peer-urls http://172.31.14.133:2380  -listen-client-urls http://172.31.14.133:2379,http://127.0.0.1:2379  -advertise-client-urls http://172.31.14.133:2379  -discovery https://discovery.etcd.io/f6a3fb54b3fd1bb02e26a89fd40df0e8

// in node3, 127.0.0.1 is used for internal client listener
etcd -name ip-172-31-13-239 -initial-advertise-peer-urls http://172.31.13.239:2380  -listen-peer-urls http://172.31.13.239:2380  -listen-client-urls http://172.31.13.239:2379,http://127.0.0.1:2379  -advertise-client-urls http://172.31.13.239:2379  -discovery https://discovery.etcd.io/f6a3fb54b3fd1bb02e26a89fd40df0e8
  1. 让我们仔细看一下 node1 的日志:
2018-02-10 04:58:03.819963 I | etcdmain: etcd Version: 3.3.0
...
2018-02-10 04:58:03.820400 I | embed: listening for peers on http://172.31.3.80:2380
2018-02-10 04:58:03.820427 I | embed: listening for client requests on
127.0.0.1:2379
2018-02-10 04:58:03.820444 I | embed: listening for client requests on 172.31.3.80:2379
2018-02-10 04:58:03.947753 N | discovery: found self f60c98e749d41d1b in the cluster
2018-02-10 04:58:03.947771 N | discovery: found 1 peer(s), waiting for 2 more
2018-02-10 04:58:22.289571 N | discovery: found peer 6645fe871c820573 in the cluster
2018-02-10 04:58:22.289628 N | discovery: found 2 peer(s), waiting for 1 more
2018-02-10 04:58:36.907165 N | discovery: found peer 1ce61c15bdbb20b2 in the cluster
2018-02-10 04:58:36.907192 N | discovery: found 3 needed peer(s)
...
2018-02-10 04:58:36.931319 I | etcdserver/membership: added member 1ce61c15bdbb20b2 [http://172.31.13.239:2380] to cluster 29c0e2579c2f9563
2018-02-10 04:58:36.931422 I | etcdserver/membership: added member 6645fe871c820573 [http://172.31.14.133:2380] to cluster 29c0e2579c2f9563
2018-02-10 04:58:36.931494 I | etcdserver/membership: added member f60c98e749d41d1b [http://172.31.3.80:2380] to cluster 29c0e2579c2f9563
2018-02-10 04:58:37.116189 I | raft: f60c98e749d41d1b became leader at term 2

我们可以看到第一个节点等待其他两个成员加入,并将成员添加到集群中,在第 2 个任期的选举中成为了领导者:

  1. 如果您检查其他服务器的日志,您可能会发现一些成员投票给了当前的领导者:
// in node 2
2018-02-10 04:58:37.118601 I | raft: raft.node: 6645fe871c820573 elected leader f60c98e749d41d1b at term 2
  1. 我们还可以使用成员列表来检查当前的领导者:
# etcdctl member list
1ce61c15bdbb20b2: name=ip-172-31-13-239 peerURLs=http://172.31.13.239:2380 clientURLs=http://172.31.13.239:2379 isLeader=false
6645fe871c820573: name=ip-172-31-14-133 peerURLs=http://172.31.14.133:2380 clientURLs=http://172.31.14.133:2379 isLeader=false
f60c98e749d41d1b: name=ip-172-31-3-80 peerURLs=http://172.31.3.80:2380 clientURLs=http://172.31.3.80:2379 isLeader=true
  1. 然后我们可以确认当前的领导者是172.31.3.80。我们还可以使用etcdctl来检查集群的健康状况:
# etcdctl cluster-health
member 1ce61c15bdbb20b2 is healthy: got healthy result from http://172.31.13.239:2379
member 6645fe871c820573 is healthy: got healthy result from http://172.31.14.133:2379
member f60c98e749d41d1b is healthy: got healthy result from http://172.31.3.80:2379
cluster is healthy
  1. 如果我们通过etcdctl命令删除当前的领导者:
# etcdctl member remove f60c98e749d41d1b
  1. 我们可能会发现当前的领导者已经改变了:
# etcdctl member list
1ce61c15bdbb20b2: name=ip-172-31-13-239 peerURLs=http://172.31.13.239:2380 clientURLs=http://172.31.13.239:2379 isLeader=false
6645fe871c820573: name=ip-172-31-14-133 peerURLs=http://172.31.14.133:2380 clientURLs=http://172.31.14.133:2379 isLeader=true

通过使用etcd发现,我们可以轻松地设置一个集群,etcd还为我们提供了许多 API 供我们使用。我们可以利用它来检查集群的统计信息:

  1. 例如,使用/stats/leader来检查当前的集群视图:
# curl http://127.0.0.1:2379/v2/stats/leader
{"leader":"6645fe871c820573","followers":{"1ce61c15bdbb20b2":{"latency":{"current":0.002463,"average":0.0038775,"standardDeviation":0.0014144999999999997,"minimum":0.002463,"maximum":0.005292},"counts":{"fail":0,"success":2}}}}

有关 API 的更多信息,请查看官方 API 文档:coreos.com/etcd/docs/latest/v2/api.html

在 EC2 中构建集群

CoreOS 在 AWS 中构建了 CloudFormation 来帮助您动态地引导集群。我们所需要做的就是启动一个 CloudFormation 模板并设置参数,然后就可以开始了。模板中的资源包括自动扩展设置和网络入口(安全组)。请注意,这些 etcd 正在 CoreOS 上运行。要登录到服务器,首先您需要在 KeyPair 参数中设置您的密钥对名称,然后使用命令ssh –i $your_keypair core@$ip登录到服务器。

kubeadm

如果您正在使用 kubeadm(github.com/kubernetes/kubeadm)来引导您的 Kubernetes 集群,不幸的是,在撰写本书时,HA 支持仍在进行中(v.1.10)。集群将作为单个主节点和单个配置的 etcd 创建。您需要定期备份 etcd 以保护您的数据。请参考官方 Kubernetes 网站上的 kubeadm 限制获取更多信息(kubernetes.io/docs/setup/independent/create-cluster-kubeadm/#limitations)。

kubespray

另一方面,如果您正在使用 kubespray 来配置服务器,kubespray 原生支持多节点 etcd。您需要在配置文件(inventory.cfg)的 etcd 部分中添加多个节点:

# cat inventory/inventory.cfg
my-master-1 ansible_ssh_host=<master_ip>
my-node-1 ansible_ssh_host=<node_ip>
my-etcd-1 ansible_ssh_host=<etcd1_ip>
my-etcd-2 ansible_ssh_host=<etcd2_ip>
my-etcd-3 ansible_ssh_host=<etcd3_ip>

[kube-master]
my-master-1

[etcd]
my-etcd-1
my-etcd-2
my-etcd-3

[kube-node]
my-master-1
my-node-1

然后,您可以使用三节点 etcd 来配置一个集群:

// provision a cluster 
$ ansible-playbook -b -i inventory/inventory.cfg cluster.yml

在启动 ansible playbook 后,它将配置角色,创建用户,检查第一个主节点是否已生成所有证书,并生成和分发证书。在部署结束时,ansible 将检查每个组件是否处于健康状态。

Kops

Kops 是在 AWS 中创建 Kubernetes 集群的最有效方式。通过 kops 配置文件,您可以轻松在云上启动自定义集群。要构建一个 etcd 多节点集群,您可以在 kops 配置文件中使用以下部分:

etcdClusters:
  - etcdMembers:
    - instanceGroup: my-master-us-east-1a
      name: my-etcd-1
    - instanceGroup: my-master-us-east-1b
      name: my-etcd-2
    - instanceGroup: my-master-us-east-1c
      name: my-etcd-3

通常,instanceGroup 意味着一个自动扩展组。您还需要在配置文件中声明一个相关的 intanceGroup my-master-us-east-1x。我们将在第六章中了解更多信息,在 AWS 上构建 Kubernetes。默认情况下,kops 在撰写本书时仍然使用 etcd2;您可以在每个 instanceGroup 下的 kops 配置文件中添加一个版本键,例如 version: 3.3.0

另请参阅

  • 通过使用 kubespray 在 Linux 上设置 Kubernetes 集群构建您自己的 Kubernetes 集群中的第一章。

  • 本章的构建多个主节点部分

  • 第六章,在 AWS 上构建 Kubernetes

  • 在第九章中处理 etcd 日志日志和监控

构建多个主节点

主节点在 Kubernetes 系统中充当内核组件。其职责包括以下内容:

  1. 从 etcd 服务器推送和拉取信息

  2. 作为请求的门户

  3. 将任务分配给节点

  4. 监控正在运行的任务

三个主要的守护程序使主节点能够完成前面的任务;以下图表显示了上述要点的活动:

Kubernetes 主节点与其他组件之间的交互

正如你所看到的,主节点是工作节点和客户端之间的通信者。因此,如果主节点崩溃,这将是一个问题。多主 Kubernetes 系统不仅具有容错能力,而且负载均衡。如果其中一个崩溃,也不会有问题,因为其他主节点仍然会处理工作。我们将这种基础设施设计称为高可用性,缩写为 HA。为了支持 HA 结构,将不再只有一个 API 服务器用于访问数据存储和处理请求。在分离的主节点中有几个 API 服务器将有助于同时解决任务并缩短响应时间。

准备就绪

关于构建多主系统,有一些简要的想法你应该了解:

  • 在主节点前添加一个负载均衡器服务器。负载均衡器将成为节点和客户端访问的新端点。

  • 每个主节点都运行自己的 API 服务器。

  • 系统中只有一个调度程序和一个控制器管理器有资格工作,这可以避免不同守护程序之间的冲突方向,同时管理容器。为了实现这一设置,我们在调度程序和控制器管理器中启用了--leader-elect标志。只有获得租约的人才能担任职务。

在这个配方中,我们将通过kubeadm构建一个双主系统,它具有类似的方法,同时可以扩展更多的主节点。用户也可以使用其他工具来构建高可用的 Kubernetes 集群。我们的目标是阐明一般概念。

在开始之前,除了主节点,您还应该在系统中准备其他必要的组件:

  • 两台 Linux 主机,稍后将设置为主节点。这些机器应配置为 kubeadm 主节点。请参考第一章中的kubeadm 配方在 Linux 上设置 Kubernetes 集群构建您自己的 Kubernetes 集群。您应该在两台主机上完成软件包安装和系统配置先决条件部分。

  • 主服务器的负载均衡器。如果你在公共云上工作,比如 AWS 的 ELB 和 GCE 的负载均衡,那将会更容易。

  • 一个 etcd 集群。请在本章中检查集群化etcd 的配方。

如何做…

我们将使用一个配置文件来运行定制的守护程序执行 kubeadm。请按照下一节的步骤将多个主节点作为一个组。

设置第一个主服务器

首先,我们将设置一个主服务器,为 HA 环境做好准备。与使用 kubeadm 运行集群的初始步骤一样,重要的是在开始时在主服务器上启用并启动 kubelet。然后它可以在kube-system命名空间中作为 pod 运行的守护程序:

// you are now in the terminal of host for first master
$ sudo systemctl enable kubelet && sudo systemctl start kubelet

接下来,让我们使用自定义的 kubeadm 配置文件启动主服务:

$ cat custom-init-1st.conf
apiVersion: kubeadm.k8s.io/v1alpha1
kind: MasterConfiguration
api:
  advertiseAddress: "<FIRST_MASTER_IP>"
etcd:
  endpoints:
  - "<ETCD_CLUSTER_ENDPOINT>"
apiServerCertSANs:
- "<FIRST_MASTER_IP>"
- "<SECOND_MASTER_IP>"
- "<LOAD_BALANCER_IP>"
- "127.0.0.1"
token: "<CUSTOM_TOKEN: [a-z0-9]{6}.[a-z0-9]{16}>"
tokenTTL: "0"
apiServerExtraArgs:
  endpoint-reconciler-type: "lease"

这个配置文件有多个值需要与您的环境设置匹配。IP 的设置很直接。请注意,您现在正在设置第一个主服务器;<FIRST_MASTER_IP>变量将是您当前位置的物理 IP。<ETCD_CLUSTER_ENDPOINT>将以"http://<IP>:<PORT>"的格式,这将是 etcd 集群的负载均衡器。<CUSTOM_TOKEN>应该以指定的格式有效(例如,123456.aaaabbbbccccdddd)。在您分配所有变量以适应您的系统后,现在可以运行它了:

$ sudo kubeadm init --config=custom-init-1st.conf

你可能会收到“不支持交换”的错误消息。在kubeadm init命令中添加额外的--ignore-preflight-errors=Swap标志以避免这种中断。

确保在主服务器的两个文件中更新。

我们需要通过以下命令完成客户端功能:

$ mkdir -p $HOME/.kube

$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config

$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

就像在使用 kubeadm 运行单个主服务器集群时一样,如果没有容器网络接口,附加的kube-dns将始终处于挂起状态。我们将在演示中使用 CNI Calico。也可以应用适合 kubeadm 的其他 CNI:

$ kubectl apply -f https://docs.projectcalico.org/v2.6/getting-started/kubernetes/installation/hosted/kubeadm/1.6/calico.yaml

现在您可以添加更多的主节点了。

使用现有证书设置其他主服务器

与上一节类似,让我们首先启动并启用kubelet

// now you're in the second master
$ sudo systemctl enable kubelet && sudo systemctl start kubelet

在我们设置好第一个主服务器之后,我们应该与整个系统共享新生成的证书和密钥。这可以确保主服务器以相同的方式进行安全设置:

$ sudo scp -r root@$FIRST_MASTER_IP:/etc/kubernetes/pki/* /etc/kubernetes/pki/

您会发现一些文件,如证书或密钥,被复制到/etc/kubernetes/pki/目录中,只有 root 用户才能访问。但是,我们将删除apiserver.crtapiserver.key文件。这是因为这些文件应该根据第二个主节点的主机名和 IP 生成,但共享的客户端证书ca.crt也参与了生成过程:

$ sudo rm /etc/kubernetes/pki/apiserver.*

接下来,在执行主节点初始化命令之前,请更改第二个主节点的配置文件中的 API 广告地址。它应该是第二个主节点的 IP,即您当前的主机。第二个主节点的配置文件与第一个主节点的配置文件非常相似。

不同之处在于我们应该指示etcd服务器的信息,并避免创建新的etcd集:

// Please modify the change by your case
$ cat custom-init-2nd.conf
apiVersion: kubeadm.k8s.io/v1alpha1
kind: MasterConfiguration
api:
  advertiseAddress: "<SECOND_MASTER_IP>"
...

继续执行kubeadm init命令,记录init命令的最后一行显示的kubeadm join命令,以便稍后添加节点,并启用客户端 API 权限:

$ sudo kubeadm init --config custom-init-2nd.conf
// copy the "kubeadm join" command showing in the output

$ mkdir -p $HOME/.kube

$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config

$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

然后,检查当前节点;您会发现有两个主节点:

$ kubectl get nodes
NAME       STATUS    ROLES     AGE       VERSION
master01   Ready     master    8m        v1.10.2
master02   Ready     master    1m        v1.10.2

在 HA 集群中添加节点

一旦主节点准备就绪,您可以将节点添加到系统中。此节点应完成先决条件配置,作为 kubeadm 集群中的工作节点。并且,在开始时,您应该像主节点一样启动 kubelet:

// now you're in the second master
$ sudo systemctl enable kubelet && sudo systemctl start kubelet

之后,您可以继续并推送您复制的加入命令。但是,请将主节点 IP 更改为负载均衡器的 IP:

// your join command should look similar to following one
$ sudo kubeadm join --token <CUSTOM_TOKEN> <LOAD_BALANCER_IP>:6443 --discovery-token-ca-cert-hash sha256:<HEX_STRING>

然后,您可以跳转到第一个主节点或第二个主节点,以检查节点的状态:

// you can see the node is added
$ kubectl get nodes
NAME       STATUS    ROLES     AGE       VERSION
master01   Ready     master    4h        v1.10.2
master02   Ready     master    3h        v1.10.2
node01     Ready     <none>    22s       v1.10.2

它是如何工作的...

验证我们的 HA 集群,请查看kube-system命名空间中的 pod:

$ kubectl get pod -n kube-system
NAME                                      READY     STATUS    RESTARTS   AGE
calico-etcd-6bnrk                         1/1       Running   0          1d
calico-etcd-p7lpv                         1/1       Running   0          1d
calico-kube-controllers-d554689d5-qjht2   1/1       Running   0          1d
calico-node-2r2zs                         2/2       Running   0          1d
calico-node-97fjk                         2/2       Running   0          1d
calico-node-t55l8                         2/2       Running   0          1d
kube-apiserver-master01                   1/1       Running   0          1d
kube-apiserver-master02                   1/1       Running   0          1d
kube-controller-manager-master01          1/1       Running   0          1d
kube-controller-manager-master02          1/1       Running   0          1d
kube-dns-6f4fd4bdf-xbfvp                  3/3       Running   0          1d
kube-proxy-8jk69                          1/1       Running   0          1d
kube-proxy-qbt7q                          1/1       Running   0          1d
kube-proxy-rkxwp                          1/1       Running   0          1d
kube-scheduler-master01                   1/1       Running   0          1d
kube-scheduler-master02                   1/1       Running   0          1d

这些 pod 作为系统守护程序运行:Kubernetes 系统服务,如 API 服务器,Kubernetes 附加组件,如 DNS 服务器,以及 CNI 组件;在这里我们使用了 Calico。但等等!当您仔细查看 pod 时,您可能会好奇为什么控制器管理器和调度器在两个主节点上都在运行。在 HA 集群中不是只有一个吗?

正如我们在前一节中了解的那样,我们应该避免在 Kubernetes 系统中运行多个控制器管理器和多个调度器。这是因为它们可能同时尝试接管请求,这不仅会创建冲突,而且还会浪费计算资源。实际上,在使用 kubeadm 启动整个系统时,默认情况下会启动具有leader-elect标志的控制器管理器和调度器:

// check flag leader-elect on master node
$ sudo cat /etc/kubernetes/manifests/kube-controller-manager.yaml
apiVersion: v1
kind: Pod
metadata:
  annotations:
    scheduler.alpha.kubernetes.io/critical-pod: ""
  creationTimestamp: null
  labels:
    component: kube-controller-manager
    tier: control-plane
  name: kube-controller-manager
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-controller-manager
...
    - --leader-elect=true...

您可能会发现调度程序也已设置为leader-elect。然而,为什么还会有多个 pod 呢?事实上,具有相同角色的其中一个 pod 是空闲的。我们可以通过查看系统端点获取详细信息:

// ep is the abbreviation of resource type "endpoints"
$ kubectl get ep -n kube-system
NAME                      ENDPOINTS                                   AGE
calico-etcd               192.168.122.201:6666,192.168.122.202:6666   1d
kube-controller-manager   <none>                                      1d
kube-dns                  192.168.241.67:53,192.168.241.67:53         1d
kube-scheduler            <none>                                      1d

// check endpoint of controller-manager with YAML output format
$ kubectl get ep kube-controller-manager -n kube-system -o yaml
apiVersion: v1
kind: Endpoints
metadata:
  annotations:
    control-plane.alpha.kubernetes.io/leader: '{"holderIdentity":"master01_bf4e22f7-4f56-11e8-aee3-52540048ed9b","leaseDurationSeconds":15,"acquireTime":"2018-05-04T04:51:11Z","renewTime":"2018-05-04T05:28:34Z","leaderTransitions":0}'
  creationTimestamp: 2018-05-04T04:51:11Z
  name: kube-controller-manager
  namespace: kube-system
  resourceVersion: "3717"
  selfLink: /api/v1/namespaces/kube-system/endpoints/kube-controller-manager
  uid: 5e2717b0-0609-11e8-b36f-52540048ed9b

kube-controller-manager的端点为例:它没有任何 pod 或服务的虚拟 IP(与kube-scheduler相同)。如果我们深入研究这个端点,我们会发现kube-controller-manager的端点依赖于annotations来记录租约信息;它还依赖于resourceVersion来进行 pod 映射和传递流量。根据kube-controller-manager端点的注释,我们的第一个主节点控制了情况。让我们检查两个主节点上的控制器管理器:

// your pod should be named as kube-controller-manager-<HOSTNAME OF MASTER>
$ kubectl logs kube-controller-manager-master01 -n kube-system | grep "leader"
I0504 04:51:03.015151 1 leaderelection.go:175] attempting to acquire leader lease kube-system/kube-controller-manager...
...
I0504 04:51:11.627737 1 event.go:218] Event(v1.ObjectReference{Kind:"Endpoints", Namespace:"kube-system", Name:"kube-controller-manager", UID:"5e2717b0-0609-11e8-b36f-52540048ed9b", APIVersion:"v1", ResourceVersion:"187", FieldPath:""}): type: 'Normal' reason: 'LeaderElection' master01_bf4e22f7-4f56-11e8-aee3-52540048ed9b became leader

正如您所看到的,只有一个主节点作为领导者处理请求,而另一个节点持续存在,获取租约,但不执行任何操作。

为了进行进一步的测试,我们尝试删除当前的领导者 pod,看看会发生什么。通过kubectl请求删除系统 pod 的部署时,kubeadm Kubernetes 会创建一个新的,因为它保证会启动/etc/kubernetes/manifests目录下的任何应用程序。因此,为了避免 kubeadm 的自动恢复,我们将配置文件从清单目录中移除。这会使停机时间足够长,以放弃领导权:

// jump into the master node of leader
// temporary move the configuration file out of kubeadm's control
$ sudo mv /etc/kubernetes/manifests/kube-controller-manager.yaml ./
// check the endpoint
$ kubectl get ep kube-controller-manager -n kube-system -o yaml
apiVersion: v1
kind: Endpoints
metadata:
  annotations:
    control-plane.alpha.kubernetes.io/leader: '{"holderIdentity":"master02_4faf95c7-4f5b-11e8-bda3-525400b06612","leaseDurationSeconds":15,"acquireTime":"2018-05-04T05:37:03Z","renewTime":"2018-05-04T05:37:47Z","leaderTransitions":1}'
  creationTimestamp: 2018-05-04T04:51:11Z
  name: kube-controller-manager
  namespace: kube-system
  resourceVersion: "4485"
  selfLink: /api/v1/namespaces/kube-system/endpoints/kube-controller-manager
  uid: 5e2717b0-0609-11e8-b36f-52540048ed9b
subsets: null

/etc/kubernetes/manifests目录在 kubelet 中由--pod-manifest-path标志定义。检查/etc/systemd/system/kubelet.service.d/10-kubeadm.conf,这是 kubelet 的系统守护程序配置文件,以及 kubelet 的帮助消息,以获取更多详细信息。

现在,轮到另一个节点唤醒其控制器管理器并让其工作了。一旦放回控制器管理器的配置文件,您会发现旧的领导者现在正在等待租约:

$ kubectl logs kube-controller-manager-master01 -n kube-system
I0504 05:40:10.218946 1 controllermanager.go:116] Version: v1.10.2
W0504 05:40:10.219688 1 authentication.go:55] Authentication is disabled
I0504 05:40:10.219702 1 insecure_serving.go:44] Serving insecurely on 127.0.0.1:10252
I0504 05:40:10.219965 1 leaderelection.go:175] attempting to acquire leader lease kube-system/kube-controller-manager...

另请参阅

在阅读本文之前,您应该已经掌握了通过 kubeadm 进行单主安装的基本概念。请参考这里提到的相关食谱,以了解如何自动构建多主系统的想法:

  • 在 Linux 上通过 kubeadm 设置 Kubernetes 集群*在第一章中,构建您自己的 Kubernetes 集群

  • etcd 集群

第五章:构建持续交付流水线

在本章中,我们将涵盖以下内容:

  • 从单片转移到微服务

  • 使用私有 Docker 注册表

  • 与 Jenkins 集成

介绍

Kubernetes 是微服务架构应用程序的完美匹配。然而,大多数旧应用程序都是以单片样式构建的。我们将为您介绍如何从单片转移到微服务世界。对于微服务,如果您手动进行部署,将会变得很麻烦。我们将学习如何通过协调 Jenkins、Docker 注册表和 Kubernetes 来构建我们自己的持续交付流水线。

从单片转移到微服务

通常,应用程序架构是包含模型-视图-控制器MVC)的单片设计,每个组件都在一个单一的大二进制文件中。单片设计有一些好处,比如组件之间的延迟较小,都在一个简单的包中,易于部署和测试。

然而,单片设计也有一些缺点,因为二进制文件会变得越来越大。在添加或修改代码时,您总是需要注意副作用,因此发布周期会变得更长。

容器和 Kubernetes 在使用微服务时提供了更大的灵活性。微服务架构非常简单,可以分为一些模块或一些服务类以及 MVC 一起:

单片和微服务设计

每个微服务都使用 RESTful 或一些标准网络 API 提供远程过程调用RPC)给其他微服务。好处是每个微服务都是独立的。在添加或修改代码时副作用很小。独立发布周期,因此它完全符合敏捷软件开发方法论,并允许重用这些微服务来构建另一个构建微服务生态系统的应用程序。

准备就绪

准备简单的微服务程序。为了推送和拉取您的微服务,请提前注册 Docker hub(hub.docker.com/)以创建您的免费 Docker ID。

如果将 Docker 镜像推送到 Docker hub,它将是公开的;任何人都可以拉取您的镜像。因此,请不要将任何机密信息放入镜像中。

一旦成功登录到您的 Docker ID,您将被重定向到您的仪表板页面如下:

登录到 Docker hub 后

如何做...

准备微服务和前端 WebUI 作为 Docker 镜像。然后,使用 Kubernetes 复制控制器和服务部署它们。

微服务

通过以下步骤构建一个提供简单数学函数的微服务:

  1. 这是使用 Python Flask (flask.pocoo.org/)的简单微服务:
$ cat entry.py from flask import Flask, request app = Flask(__name__) @app.route("/")
def hello():
 return "Hello World!" @app.route("/power/<int:base>/<int:index>")
def power(base, index): 
return "%d" % (base ** index) @app.route("/addition/<int:x>/<int:y>")
def add(x, y):
 return "%d" % (x+y) @app.route("/substraction/<int:x>/<int:y>")
def substract(x, y):
 return "%d" % (x-y) if __name__ == "__main__":
    app.run(host='0.0.0.0') 
  1. 准备一个Dockerfile如下以构建 Docker 镜像:
$ cat Dockerfile
FROM ubuntu:14.04 # Update packages
RUN apt-get update -y  # Install Python Setuptools RUN apt-get install -y python-setuptools git telnet curl # Install pip RUN easy_install pip # Bundle app source
ADD . /src
WORKDIR /src    # Add and install Python modules RUN pip install Flask  # Expose EXPOSE 5000  # Run CMD ["python", "entry.py"]
  1. 然后,使用docker build命令构建 Docker 镜像如下:
//name as “your_docker_hub_id/my-calc”

$ sudo docker build -t hidetosaito/my-calc .
Sending build context to Docker daemon 3.072 kB
Step 1 : FROM ubuntu:14.04

 ---> 6cc0fc2a5ee3
Step 2 : RUN apt-get update -y

 ---> Using cache

(snip)

Step 8 : EXPOSE 5000

 ---> Running in 7c52f4bfe373

 ---> 28f79bb7481f
Removing intermediate container 7c52f4bfe373
Step 9 : CMD python entry.py

 ---> Running in 86b39c727572

 ---> 20ae465bf036
Removing intermediate container 86b39c727572
Successfully built 20ae465bf036

//verity your image

$ sudo docker images
REPOSITORY            TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
hidetosaito/my-calc   latest              20ae465bf036        19 seconds ago      284 MB
ubuntu                14.04               6cc0fc2a5ee3        3 weeks ago         187.9 MB
  1. 然后,使用docker login命令登录到 Docker hub:
//type your username, password and e-mail address in Docker hub
$ sudo docker login
Username: hidetosaito
Password:
Email: hideto.saito@yahoo.com
WARNING: login credentials saved in /home/ec2-user/.docker/config.json
Login Succeeded
  1. 最后,使用docker push命令注册到您的 Docker hub 存储库如下:
//push to your docker index
$ sudo docker push hidetosaito/my-calc
The push refers to a repository [docker.io/hidetosaito/my-calc] (len: 1)
20ae465bf036: Pushed

(snip)

92ec6d044cb3: Pushed
latest: digest: sha256:203b81c5a238e228c154e0b53a58e60e6eb3d1563293483ce58f48351031a474 size: 19151
  1. 访问 Docker hub 后,您可以在存储库中看到您的微服务:

您的微服务 Docker 镜像在 Docker hub 上

前端 WebUI

构建使用前述微服务的 WebUI 的步骤:

  1. 这是一个简单的前端 WebUI,也使用 Python Flask
$ cat entry.py
import os
import httplib
from flask import Flask, request, render_template

app = Flask(__name__)

@app.route("/")
def index():
    return render_template('index.html')

@app.route("/add", methods=['POST'])
def add():
    #
    # from POST parameters
    #
    x = int(request.form['x'])
    y = int(request.form['y'])

    #
    # from Kubernetes Service(environment variables)
    #
    my_calc_host = os.environ['MY_CALC_SERVICE_SERVICE_HOST']
    my_calc_port = os.environ['MY_CALC_SERVICE_SERVICE_PORT']

    #
    # REST call to MicroService(my-calc)
    #
    client = httplib.HTTPConnection(my_calc_host, my_calc_port)
    client.request("GET", "/addition/%d/%d" % (x, y))
    response = client.getresponse()
    result = response.read()
    return render_template('index.html', add_x=x, add_y=y,
add_result=result)

if __name__ == "__main__":
    app.debug = True
    app.run(host='0.0.0.0')

Kubernetes 服务会将 Kubernetes 服务名称和端口号作为环境变量提供给其他 pod。因此,环境变量的名称和 Kubernetes 服务名称必须一致。在这种情况下,my-calc服务名称必须是my-calc-service

  1. 前端 WebUI 使用Flask HTML 模板;它类似于 PHP 和 JSP,entry.py将参数传递给模板(index.html)以渲染 HTML:
$ cat templates/index.html
<html>
   <body>
   <div>
         <form method="post" action="/add">
           <input type="text" name="x" size="2"/>
           <input type="text" name="y" size="2"/>
           <input type="submit" value="addition"/>
       </form>
       {% if add_result %}
       <p>Answer : {{ add_x }} + {{ add_y }} = {{ add_result }}</p>
       {% endif %}
   </div>
   </body>
</html>
  1. Dockerfile与微服务my-calc完全相同。因此,最终的文件结构将如下所示。请注意,index.html是一个 jinja2 模板文件;因此,将其放在/templates目录下:
/Dockerfile /entry.py /templates/index.html
  1. 然后,按以下方式构建 Docker 镜像并推送到 Docker hub:

为了将您的镜像推送到 Docker hub,您需要使用Docker login命令登录。这只需要一次;系统会检查~/.docker/config.json从那里读取。

//build frontend Webui image 
$ sudo docker build -t hidetosaito/my-frontend .

//login to docker hub
$ sudo docker login

//push frontend webui image
$ sudo docker push hidetosaito/my-frontend
  1. 访问 Docker hub 后,您可以在存储库中看到您的 WebUI 应用程序:

Docker Hub 上的微服务和前端 WebUI 镜像

它是如何工作的...

让我们准备两个 YAML 配置,使用 Kubernetes 启动微服务容器和前端 WebUI 容器。

微服务

微服务(my-calc)使用 Kubernetes 部署和服务,但它只需要与其他 pod 通信。换句话说,无需将其暴露给外部 Kubernetes 网络。因此,服务类型设置为ClusterIP

$ cat my-calc.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-calc-deploy
spec:
  replicas: 2
  selector:
    matchLabels:
      run: my-calc
  template:
    metadata:
      labels:
        run: my-calc
    spec:
      containers:
      - name: my-calc
        image: hidetosaito/my-calc
---

apiVersion: v1
kind: Service
metadata:
  name: my-calc-service
spec:
  ports:
    - protocol: TCP
      port: 5000
  type: ClusterIP
  selector:
     run: my-calc

使用kubectl命令加载my-calc pod 如下:

$ kubectl create -f my-calc.yaml 
deployment.apps "my-calc-deploy" created
service "my-calc-service" created

前端 WebUI

前端 WebUI 也使用部署和服务,但它暴露端口(TCP 端口30080)以便从外部 Web 浏览器访问:

$ cat my-frontend.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-frontend-deploy
spec:
  replicas: 2
  selector:
    matchLabels:
      run: my-frontend
  template:
    metadata:
      labels:
        run: my-frontend
    spec:
      containers:
      - name: my-frontend
        image: hidetosaito/my-frontend
---

apiVersion: v1
kind: Service
metadata:
  name: my-frontend-service
spec:
  ports:
    - protocol: TCP
      port: 5000
      nodePort: 30080
  type: NodePort
  selector:
     run: my-frontend

$ kubectl create -f my-frontend.yaml 
deployment.apps "my-frontend-deploy" created
service "my-frontend-service" created

让我们尝试使用 Web 浏览器访问my-frontend-service。您可以访问任何 Kubernetes 节点的 IP 地址;指定端口号 30080。如果您使用 minikube,只需键入minikube service my-frontend-service即可访问。然后您可以看到my-frontend应用程序如下:

访问前端 WebUI

当您单击添加按钮时,它将向微服务(my-calc)转发一个参数。微服务计算加法(是的,只是加法!),然后将结果返回给前端 WebUI 如下:

从微服务获取结果并呈现 HTML

现在,可以轻松地独立扩展前端 WebUI 和微服务的 pod。例如,将 WebUI pod 从2扩展到8,将微服务 pod 从2扩展到16,如下所示:

$ kubectl get deploy
NAME                 DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE my-calc-deploy       2         2         2            2           30m my-frontend-deploy   2         2         2            2           28m 
$ kubectl scale deploy my-frontend-deploy --replicas=8
deployment "my-frontend-deploy" scaled

$ kubectl scale deploy my-calc-deploy --replicas=16
deployment "my-calc-deploy" scaled

$ kubectl get deploy NAME                 DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE my-calc-deploy       16        16        16           16          31m my-frontend-deploy   8         8         8            8           29m 

此外,如果需要修复一些错误,例如,如果前端需要验证

输入参数以检查它是数字还是字符串(是的,如果您输入字符串并提交,它将显示一个错误!),这不会影响构建和部署周期。

然后提交,它将显示一个错误!),这不会影响构建和部署周期

微服务:

前端和微服务 pod 和服务

此外,如果您想添加另一个微服务,例如减法微服务,您可能需要创建另一个 Docker 映像,并与另一个部署和服务一起部署,以便它独立于当前的微服务。然后,您可以不断积累自己的微服务生态系统以便在另一个应用程序中重用。

使用私有 Docker 注册表

一旦您开始通过 Docker 构建微服务应用程序,您将需要一个 Docker 注册表来存放您的容器映像。Docker hub 为您提供免费的公共存储库,但在某些情况下,您可能希望由于业务需求或组织政策而将您的映像设为私有。

Docker hub 提供私有存储库,只允许经过身份验证的用户推送和拉取您的图像,并且对其他用户不可见。但是,免费计划只有一个配额(存储库)。您可以支付以增加私有存储库的数量,但是如果您采用微服务架构,您将需要大量的私有存储库:

Docker hub 私有存储库价格表

Docker hub 的付费计划是设置私有注册表的最简单方法,但是还有其他一些方法可以在您的网络内设置无限的 Docker 镜像配额的私有 Docker 注册表。此外,您还可以使用其他云提供的注册表服务来管理您的私有注册表。

准备工作

在这个配方中,我们将向您展示三种不同的方法来设置您自己的私有注册表:

当使用 Kubernetes 设置私有注册表时,您可以在私有或公共云上使用自己的 Kubernetes 集群,这样您就可以完全控制并充分利用您的物理资源。

另一方面,当使用公共云提供的服务,如 AWS 或 GCP 时,您可以摆脱服务器和存储的管理。无论您需要什么,这些公共云都为您提供弹性资源。我们只需要设置 Kubernetes 的凭据并让节点知道。以下配方将介绍这三种不同的选项。

使用 Kubernetes 运行 Docker 注册表服务器

如果您想使用 Kubernetes 启动一个私有注册表服务器,您需要自己的 Kubernetes 集群。在探索本书时,您将设置自己的 Kubernetes。如果您还没有这样做,请阅读第一章,构建您自己的 Kubernetes 集群,选择最简单的方法。

请注意,Docker 注册表将存储一些 Docker 镜像。您必须有一个PersistentVolume通过 Kubernetes 管理您的存储。此外,我们应该期望多个 pod 将读取和写入相同的PersistentVolume,因为可扩展性。因此,您必须具有PersistentVolumeReadWriteManyRWX)访问模式,例如 GlusterFS 或 NFS。

PersistentVolume的详细信息在第二章的使用卷部分中有描述,深入了解 Kubernetes 概念。让我们创建一个使用 NFS 和名称pvnfs01来分配100GB 的PersistentVolume

//my NFS server(10.138.0.5) shares /nfs directory
$ showmount -e 10.138.0.5
Export list for 10.138.0.5:
/nfs *

//please change spec.nfs.path and spec.nfs.server to yours
$ cat pv_nfs.yaml 
apiVersion: "v1"
kind: "PersistentVolume"
metadata:
  name: pvnfs01
spec:
  capacity:
    storage: "100Gi"
  accessModes:
    - "ReadWriteMany"
  nfs:
    path: "/nfs"
    server: "10.138.0.5"

$ kubectl create -f pv_nfs.yaml 
persistentvolume "pvnfs01" created

$ kubectl get pv
NAME    CAPACITY ACCESS MODES RECLAIM POLICY STATUS    CLAIM STORAGECLASS REASON AGE
pvnfs01 100Gi    RWX          Retain         Available                           5s

如果无法准备 RWX PersistentVolume,您仍然可以通过 Kubernetes 设置 Docker 注册表,但只能启动一个 pod(副本:一个)。或者,您可以使用 AWS S3 或 GCP PD 作为私有注册表后端存储;请访问docs.docker.com/registry/configuration/了解如何为您的注册表配置后端存储。

接下来,创建PersistentVolumeClaim,将 NFSPersistentVolume和 pod 配置解耦。让我们创建一个名为pvc-1PersistentVolumeClaim。确保accessModesReadWriteMany,并且创建后STATUS变为Bound

$ cat pvc-1.yml 
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
    name: pvc-1
spec:
  storageClassName: ""
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 100Gi

$ kubectl create -f pvc-1.yml 
persistentvolumeclaim "pvc-1" created

$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc-1 Bound pvnfs01 100Gi RWX 5s

这已经足够设置您的私有注册表。它有一些先决条件;或者,使用公共云要简单得多。

使用 Amazon 弹性容器注册表

Amazon 弹性容器注册表ECR)是 Amazon 弹性容器服务ECS)的一部分。这个教程不会涉及 ECS 本身;而是只使用 ECR 作为私有注册表。

要使用 Amazon ECR,您必须拥有 AWS 账户并在您的机器上安装 AWS CLI。这将在第六章中更详细地描述,在 AWS 上构建 Kubernetes。您将需要创建一个带有ACCESS KEY IDSECRET ACCESS KEY的 IAM 用户,并关联AmazonEC2ContainerRegistryFullAccess策略,该策略允许对 Amazon ECR 进行完全管理员访问:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecr:*"
      ],
      "Resource": "*"
    }
  ]
}

然后通过aws configure命令配置 AWS CLI 的默认设置:

$ aws configure AWS Access Key ID [None]: <Your AWS ACCESS KEY ID> AWS Secret Access Key [None]: <Your AWS SECRET ACCESS KEY> Default region name [None]: us-east-1 Default output format [None]: 

然后我们可以开始使用 Amazon ECR。

使用 Google 云注册表

Google 容器注册表cloud.google.com/container-registry/)是 GCP 的一部分。与 AWS 类似,需要拥有 GCP 帐户,以及 Cloud SDK(cloud.google.com/sdk/),这是 GCP 中的命令行界面。有关 GCP 的更多详细信息将在第七章中描述,在 GCP 上构建 Kubernetes

在 GCP 上,我们只需要创建一个项目并为我们的项目启用计费和容器注册表 API。否则,gcloud中的任何操作都会显示错误:

$ gcloud container images list 
ERROR: (gcloud.container.images.list) Bad status during token exchange: 403 

为了启用计费和容器注册表 API,请访问 GCP Web 控制台(console.cloud.google.com),转到计费页面和容器注册表页面,然后启用它们。激活完成后,您可以使用gcloud container命令:

$ gcloud container images list 
Listed 0 items.  

现在我们可以开始使用 Google 容器注册表。

如何做…

我们已经完成了准备工作。让我们逐步看看如何配置您的私有注册表。

使用 Kubernetes 启动私有注册表服务器

为了启动私有注册表,需要配置这些文件以配置具有适当安全设置的私有注册表:

  • SSL 证书

  • HTTP 密钥

  • HTTP 基本身份验证文件

创建自签名 SSL 证书

有一个陷阱——人们倾向于在开始时设置一个纯 HTTP(禁用 TLS)注册表而不进行身份验证。然后还需要配置 Docker 客户端(Kubernetes 节点)以允许不安全的注册表等。这是一个不好的做法,需要许多步骤来设置不安全的环境。

最佳做法始终使用由证书颁发机构颁发的官方 SSL 证书。但是,自签名证书在测试阶段特别方便。官方证书可以等到我们定义了 FQDN。因此,本教程将向您展示如何使用 OpenSSL 通过以下步骤创建自签名 SSL 证书:

  1. 创建一个secrets目录:
$ mkdir secrets
  1. 运行openssl命令指定选项在secrets目录下生成证书(domain.crt)和私钥(domain.key)。请注意,您可以输入.跳过输入位置和电子邮件信息:
$ openssl req -newkey rsa:4096 -nodes -sha256 -keyout secrets/domain.key -x509 -days 365 -out secrets/domain.crt
Generating a 4096 bit RSA private key
.............................................++
...........................................................++
writing new private key to 'secrets/domain.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) []:us
State or Province Name (full name) []:California
Locality Name (eg, city) []:Cupertino
Organization Name (eg, company) []:packtpub
Organizational Unit Name (eg, section) []:chapter5
Common Name (eg, fully qualified host name) []:.
Email Address []:.
  1. 检查证书和私钥是否都生成在secrets目录下:
$ ls secrets/
domain.crt domain.key

创建 HTTP 密钥

关于 HTTP 秘钥,私有注册表实例在启动时默认会随机生成。然而,如果运行多个 pod,则可能会出现每个 pod 具有不同 HTTP 秘钥的问题,这会导致 Docker 客户端推送或拉取镜像时出错。因此,我们明确声明所有 pod 将使用相同的 HTTP 秘钥,通过以下步骤:

  1. 使用openssl命令在secrets目录下创建一个http.secret文件:
//create 8 byte random HEX string by OpenSSL 
$ openssl rand -hex -out secrets/http.secret 8
  1. 检查secrets目录,现在有三个文件:
$ ls secrets/
domain.crt domain.key http.secret

创建 HTTP 基本身份验证文件

最后,关于 HTTP 基本身份验证文件,如果您设置了私有注册表,则在与 Docker 注册表交互时需要进行身份验证。在推送和拉取镜像时,您将需要执行docker login以获取令牌。为了创建一个 HTTP 基本身份验证文件,使用 Apache2 提供的htpasswd命令是最简单的。通过以下步骤创建 HTTP 基本身份验证文件:

  1. 使用 Apache2 Docker 镜像(httpd)运行htpasswd命令,并带有bcrypt-B)选项,在secrets目录下生成一个基本身份验证文件(registry_passwd):
//set user=user01, passwd=my-super-secure-password
$ docker run -i httpd /bin/bash -c 'echo my-super-secure-password | /usr/local/apache2/bin/htpasswd -nBi user01' > secrets/registry_passwd 
  1. 检查secrets目录,现在有四个文件:
$ ls secrets/ domain.crt  domain.key  http.secret  registry_passwd

创建一个用于存储安全文件的 Kubernetes 秘钥

有四个文件。我们使用Kubernetes Secret,以便所有 pod 可以通过环境变量或挂载卷访问它并作为文件访问。有关秘钥的更多细节,请参阅第二章中的使用秘钥部分,深入了解 Kubernetes 概念。您可以使用kubectl命令通过以下步骤加载这四个文件并存储到 Kubernetes 秘钥中:

  1. 使用kubectl create命令并带有--from-file参数来指定 secrets 目录:
$ kubectl create secret generic registry-secrets --from-file secrets/
secret "registry-secrets" created
  1. 通过kubectl describe命令检查状态:
$ kubectl describe secret registry-secrets Name:         registry-secrets Namespace:    default Labels:       <none> Annotations:  <none> Type:  Opaque Data ==== domain.key:       3243 bytes http.secret:      17 bytes registry_passwd:  69 bytes domain.crt:       1899 bytes

配置私有注册表以加载 Kubernetes 秘钥

另一方面,私有注册表本身支持将 HTTP 秘钥作为字符串格式的环境变量进行读取。它还可以支持指定 SSL 证书和 HTTP 基本身份验证文件的文件路径作为环境变量:

环境变量名称描述示例值
REGISTRY_HTTP_SECRETHTTP 秘钥字符串valueFrom:    secretKeyRef:``        name: registry-secrets``        key: http.secret
REGISTRY_HTTP_TLS_CERTIFICATE证书的文件路径(domain.crt/mnt/domain.crt
REGISTRY_HTTP_TLS_KEY私钥的文件路径(domain.key/mnt/domain.key
REGISTRY_AUTH_HTPASSWD_REALM注册服务器进行身份验证的领域basic-realm
REGISTRY_AUTH_HTPASSWD_PATHhtpasswd文件(registry_passwd)的文件路径/mnt/registry_passwd
REGISTRY_HTTP_HOST指定 Kubernetes 节点 IP 和nodePort之一10.138.0.3:30500

理想情况下,您应该有一个负载均衡器,并设置一个 Kubernetes 服务类型为LoadBalancer。然后REGISTRY_HTTP_HOST可以是负载均衡器的 IP 和端口号。为简单起见,我们将在这个示例中只使用NodePort。有关LoadBalancer的更多信息,请参阅第二章中的与服务一起工作部分,以及第三章中的转发容器端口部分,玩转容器

我们将对一个 Kubernetes YAML 文件进行部署,用于创建一个注册表,并在其中包含前述变量,以便注册表 pod 可以使用它们。现在我们有PersistentVolumeClaim作为pvc-1,它提供容器映像存储,并通过 Secret registry-secrets挂载 SSL 证书文件(domain.crtdomain.key)和 HTTP 基本认证文件(registry_passwd)。以及通过 Secret registry-secrets将 HTTP Secret 字符串读取为环境变量。整个 YAML 配置如下:

$ cat private_registry.yaml apiVersion: apps/v1 kind: Deployment metadata:
 name: my-private-registry spec:
 replicas: 1 selector: matchLabels: run: my-registry template: metadata: labels: run: my-registry spec: containers: - name: my-registry image: registry env: - name: REGISTRY_HTTP_HOST value: 10.138.0.3:30500 - name: REGISTRY_HTTP_SECRET valueFrom: secretKeyRef: name: registry-secrets key: http.secret - name: REGISTRY_HTTP_TLS_CERTIFICATE value: /mnt/domain.crt - name: REGISTRY_HTTP_TLS_KEY value: /mnt/domain.key - name: REGISTRY_AUTH_HTPASSWD_REALM value: basic-realm - name: REGISTRY_AUTH_HTPASSWD_PATH value: /mnt/registry_passwd ports: - containerPort: 5000 volumeMounts: - mountPath: /var/lib/registry name: registry-storage - mountPath: /mnt name: certs volumes: - name: registry-storage persistentVolumeClaim: claimName: "pvc-1" - name: certs secret: secretName: registry-secrets items: - key: domain.key path: domain.key - key: domain.crt path: domain.crt - key: registry_passwd path: registry_passwd  --- apiVersion: v1 kind: Service metadata:
 name: private-registry-svc spec:
 ports: - protocol: TCP port: 5000 nodePort: 30500 type: NodePort selector: run: my-registry $ kubectl create -f private_registry.yaml deployment.apps "my-private-registry" created service "private-registry-svc" created  //can scale to multiple Pod (if you have RWX PV set) $ kubectl scale deploy my-private-registry --replicas=3 deployment "my-private-registry" scaled   $ kubectl get deploy NAME                  DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE my-private-registry   3         3         3            3           2m  

现在您自己的私有注册表已经准备好使用了!

在 AWS 弹性容器注册表上创建存储库

为了将容器映像推送到 Amazon ECR,您需要事先创建一个存储库。与 Docker hub 或私有注册表不同,当第一次推送映像时,Amazon ECR 不会自动创建存储库。因此,如果您想推送三个容器映像,您必须事先创建三个存储库:

使用aws ecr create-repository命令指定存储库名称很简单:

$ aws ecr create-repository --repository-name my-nginx
{
    "repository": {
        "registryId": "************", 
        "repositoryName": "my-nginx", 
        "repositoryArn": "arn:aws:ecr:us-east-1:************:repository/my-nginx", 
        "createdAt": 1516608220.0, 
        "repositoryUri": "************.dkr.ecr.us-east-1.amazonaws.com/my-nginx"
    }
}

就是这样!您需要记住repositoryUri(在前面的情况下,************.dkr.ecr.us-east-1.amazonaws.com/my-nginx),这将被用作私有映像 URL。

前面的 URL 被掩盖为************作为 ID,它与您的 AWS 账户 ID 绑定。

另一方面,如果您看到以下错误消息,表示您的 IAM 用户没有CreateRepository操作的权限。在这种情况下,您需要附加一个来自AmazonEC2ContainerRegistryFullAccess的 IAM 策略:

$ aws ecr create-repository --repository-name chapter5 An error occurred (AccessDeniedException) when calling the CreateRepository operation: User: arn:aws:iam::************:user/ecr-user is not authorized to perform: ecr:CreateRepository on resource: *

确定 Google 容器注册表上的存储库 URL

为了将容器映像推送到 Google 容器注册表,有一个关于存储库 URL 的重要考虑因素。首先,有几个 Google 容器注册表区域主机可用:

  • gcr.io(目前是美国地区)

  • us.gcr.io(美国地区)

  • eu.gcr.io(欧洲地区)

  • asia.gcr.io(亚洲地区)

请注意,这些区域主机是出于网络延迟目的,并不意味着限制在特定区域。它们仍然可以在全球范围内访问。

其次,当您标记容器映像时,还需要在启用计费和 API 的project-id上指定。因此,整个存储库 URL 可以是:

<gcr region>/<project-id>/<image name>:tag

在我的情况下,我使用了默认的美国地区,项目 ID 是kubernetes-cookbook,映像名称是my-nginx;因此,我的存储库 URL 是:

gcr.io/kubernetes-cookbook/my-nginx:latest

除此之外,Google 容器注册表现在已经准备好使用!

它是如何工作的...

当您开始使用 Kubernetes 的私有注册表时,必须正确配置凭据。Amazon ECR 和 Google 云注册表需要特别考虑。让我们为私有注册表、Amazon ECR 和 Google 云注册表配置凭据。

从您的私有注册表中推送和拉取映像

现在您可以将容器映像推送到您的私有注册表。因为我们已经设置了 HTTP 基本身份验证,您需要先执行docker login。否则,您会收到no basic auth credentials错误:

//just tag nginx to your own private image
$ docker tag nginx 10.138.0.3:30500/my-nginx

//will be failed when push without login information. using complete image name with private registry as prefix
$ docker push 10.138.0.3:30500/my-nginx
The push refers to a repository [10.138.0.3:30500/my-nginx]
a103d141fc98: Preparing 
73e2bd445514: Preparing 
2ec5c0a4cb57: Preparing 
no basic auth credentials

因此,您需要使用docker login来指定用户名和密码,这些用户名和密码设置在registry_passwd文件上:

//docker login
$ docker login 10.138.0.3:30500
Username: user01
Password: 
Login Succeeded

//successfully to push
$ docker push 10.138.0.3:30500/my-nginx
The push refers to a repository [10.138.0.3:30500/my-nginx]
a103d141fc98: Pushed 
73e2bd445514: Pushed 
2ec5c0a4cb57: Pushed 
latest: digest: sha256:926b086e1234b6ae9a11589c4cece66b267890d24d1da388c96dd8795b2ffcfb size: 948

另一方面,对于从私有注册表中拉取映像,Kubernetes 节点还需要为您的私有注册表配置凭据。但是在每个节点上使用docker login命令是不现实的。相反,Kubernetes 支持将这些凭据存储为 Kubernetes 秘密,并且每个节点在拉取映像时将使用这些凭据。

为此,我们需要创建一个需要指定的docker-registry资源:

  • --docker-server:在这个例子中,10.138.0.3:30500

  • --docker-username:在这个例子中,user01

  • --docker-password:在这个例子中,my-super-secure-password

  • --docker-email:您的电子邮件地址

//create secret named "my-private-credential" $ kubectl create secret docker-registry my-private-credential \
> --docker-server=10.138.0.3:30500 \
> --docker-username=user01 \
> --docker-password=my-super-secure-password \
> --docker-email=hideto.saito@example.com secret "my-private-credential" created

//successfully to created $ kubectl get secret my-private-credential NAME TYPE DATA AGE my-private-credential kubernetes.io/dockerconfigjson 1 18s

最后,您可以从指定了my-private-credential秘钥的私有注册表中拉取您的私有图像。为此,请将spec.imagePullSecrets设置如下:

$ cat private-nginx.yaml apiVersion: v1 kind: Pod metadata:
 name: private-nginx spec:
 containers: - name: private-nginx image: **10.138.0.3:30500/my-nginx** imagePullSecrets: - name: **my-private-credential** $ kubectl create -f private-nginx.yaml pod "private-nginx" created  //successfully to launch your Pod using private image
$ kubectl get pods private-nginx NAME            READY     STATUS    RESTARTS   AGE private-nginx   1/1       Running   0          10s  

恭喜!现在您可以随意将私有图像推送到由 Kubernetes 运行的私有注册表中。还可以从 Kubernetes 中拉取图像。随时可以根据客户流量进行扩展。

从 Amazon ECR 推送和拉取图像

Amazon ECR 具有身份验证机制,可提供对私有存储库的访问权限。 AWS CLI 具有使用aws ecr get-login命令生成访问令牌的功能:

$ aws ecr get-login --no-include-email 

它输出带有 ID 和密码的docker login命令:

docker login -u AWS -p eyJwYXlsb2FkIjoiNy(very long strings)... https://************.dkr.ecr.us-east-1.amazonaws.com

因此,只需复制并粘贴到您的终端以从 AWS 获取令牌。然后尝试docker push将您的 Docker 图像上传到 ECR:

$ docker tag nginx ************.dkr.ecr.us-east-1.amazonaws.com/my-nginx

$ docker push ************.dkr.ecr.us-east-1.amazonaws.com/my-nginx
The push refers to repository [************.dkr.ecr.us-east-1.amazonaws.com/my-nginx]
a103d141fc98: Pushed 
73e2bd445514: Pushing 8.783MB/53.23MB
2ec5c0a4cb57: Pushing 4.333MB/55.26MB

另一方面,从 ECR 到 Kubernetes 拉取图像的步骤与使用 Kubernetes 秘钥存储令牌的私有注册表完全相同:

$ kubectl create secret docker-registry my-ecr-secret \
> --docker-server=https://************.dkr.ecr.us-east-1.amazonaws.com \
> --docker-email=hideto.saito@example.com \
> --docker-username=AWS \
> --docker-password=eyJwYXlsb2FkIjoiS... secret "my-ecr-secret" created $ kubectl get secret my-ecr-secret NAME            TYPE                             DATA      AGE my-ecr-secret   kubernetes.io/dockerconfigjson   1         10s

现在,spec.imagePullSecrets需要指定my-ecr-secret。除了图像 URL 之外,它还指定了 ECR 存储库:

$ cat private-nginx-ecr.yaml apiVersion: v1 kind: Pod metadata:
 name: private-nginx-ecr spec:
 containers: - name: private-nginx-ecr image: **************.dkr.ecr.us-east-1.amazonaws.com/my-nginx** imagePullSecrets: - name: **my-ecr-secret** $ kubectl create -f private-nginx-ecr.yaml pod "private-nginx-ecr" created $ kubectl get pods private-nginx-ecr NAME                READY     STATUS    RESTARTS   AGE private-nginx-ecr   1/1       Running   0          1m

请注意,此令牌的生命周期很短:有效期为 12 小时。因此,12 小时后,您需要再次运行aws ecr get-login来获取新的令牌,然后更新秘钥my-ecr-secret。这绝对不是理想的做法。

好消息是,Kubernetes 支持通过CloudProvider自动更新 ECR 令牌。但是,这要求您的 Kubernetes 在 AWS 环境上运行,例如 EC2。此外,EC2 实例必须具有等同或高于AmazonEC2ContainerRegistryReadOnly策略的 IAM 角色。这将在第六章中描述,在 AWS 上构建 Kubernetes

如果您真的想要通过从 ECR 存储库中拉取图像来使用 Kubernetes 集群,那么需要面临的挑战是您需要每 12 小时更新一次 ECR 令牌。也许您可以使用定时作业或采用一些自动化工具来完成这项工作。

有关更多详细信息,请访问 AWS 在线文档docs.aws.amazon.com/AmazonECR/latest/userguide/Registries.html

从 Google 云注册表推送和拉取图像

根据 GCP 文档(cloud.google.com/container-registry/docs/advanced-authentication),有几种方式可以推送/拉取到容器注册表。

使用 gcloud 来包装 Docker 命令

gcloud命令有一个包装函数来运行docker命令来推送和拉取。例如,如果你想推送镜像gcr.io/kubernetes-cookbook/my-nginx,使用gcloud命令:

$ gcloud docker -- push gcr.io/kubernetes-cookbook/my-nginx 

从你的机器推送镜像就足够了,但是如果你要与 Kubernetes 集成,这并不理想。这是因为在 Kubernetes 节点上不容易包装gcloud命令。

幸运的是,有一个解决方案可以创建一个 GCP 服务账号并授予其权限(角色)。

使用 GCP 服务账号授予长期有效的凭据

我们需要集成以从 Kubernetes 节点中拉取镜像,这需要一个长期有效的凭据,可以存储到 Kubernetes 秘钥中。为此,请执行以下步骤:

  1. 创建一个 GCP 服务账号(container-sa):
$ gcloud iam service-accounts create container-sa 
Created service account [container-sa]. //full name is as below $ gcloud iam service-accounts list | grep container container-sa@kubernetes-cookbook.iam.gserviceaccount.com  
  1. container-sa(使用全名)分配给roles/storage.admin角色:
$ gcloud projects add-iam-policy-binding kubernetes-cookbook \

> --member serviceAccount:container-sa@kubernetes-cookbook.iam.gserviceaccount.com \

> --role=roles/storage.admin 
  1. container-sa生成一个密钥文件(container-sa.json):
$ gcloud iam service-accounts keys create container-sa.json \
> --iam-account container-sa@kubernetes-cookbook.iam.gserviceaccount.com 
 created key [f60a81235a1ed9fbce881639f621470cb087149c] of type [json] as [container-sa.json] for [container-sa@kubernetes-cookbook.iam.gserviceaccount.com] 
  1. 使用docker login来检查密钥文件是否有效:
//note that username must be _json_key 
$ cat container-sa.json | docker login --username **_json_key** --password-stdin gcr.io Login Succeeded
  1. 使用docker pull来检查是否可以从容器注册表中拉取:
$ docker pull gcr.io/kubernetes-cookbook/my-nginx 
Using default tag: latest latest: Pulling from kubernetes-cookbook/my-nginx e7bb522d92ff: Pulling fs layer 6edc05228666: Pulling fs layer  
...  

看起来一切都很好!现在你可以像使用私有注册表或 AWS ECR 一样使用 Kubernetes 秘钥。

  1. 创建一个 Kubernetes 秘钥(my-gcr-secret)来指定_json_keycontainer-sa.json
$ kubectl create secret docker-registry my-gcr-secret \ > --docker-server=gcr.io \ > --docker-username=_json_key \ > --docker-password=`cat container-sa.json` \ > --docker-email=hideto.saito@example.com secret "my-gcr-secret" created
  1. my-gcr-secret指定为imagePullSecrets以启动一个 pod:
$ cat private-nginx-gcr.yaml apiVersion: v1 kind: Pod metadata:
 name: private-nginx-gcr spec:
 containers: - name: private-nginx-gcr image: **gcr.io/kubernetes-cookbook/my-nginx** imagePullSecrets: - name: **my-gcr-secret**  $ kubectl create -f private-nginx-gcr.yaml pod "private-nginx-gcr" created $ kubectl get pods NAME                READY     STATUS    RESTARTS   AGE private-nginx-gcr   1/1       Running   0          47s

恭喜!现在你可以使用由 GCP 完全管理的 Google 容器注册表作为你的私有注册表。Kubernetes 可以从那里拉取你的私有镜像。

与 Jenkins 集成

在软件工程中,持续集成CI)(en.wikipedia.org/wiki/Continuous_integration)和持续交付CD)(en.wikipedia.org/wiki/Continuous_delivery)简称为 CI/CD,具有简化传统开发过程的能力,通过持续开发、测试和交付机制来减少严重冲突的恐慌,即一次交付小的变更并立即缩小问题范围。此外,通过自动化工具,CI/CD 系统交付的产品可以实现更高的效率并缩短上市时间。

Jenkins 是众所周知的 CI 系统之一,可以配置为持续交付系统。Jenkins 可以从源代码控制系统中拉取项目代码,运行测试,然后根据您的配置进行部署。在这个教程中,我们将向您展示如何将 Jenkins 集成到 Kubernetes 以实现持续交付。

准备工作

在开始本教程之前,准备一个 Docker hub 账户(hub.docker.com)或者您可以使用前一节中描述的私有注册表。但重要的是您必须有凭据来拉取和推送到注册表。如果您使用 Docker hub,请确保docker login与您的凭据正常工作。

接下来,确保您的 Kubernetes 已准备就绪。但我们将使用 RBAC 身份验证从 Jenkins pod 访问 Kubernetes 主 API。如果您使用minikube,则需要在启动 minikube 时添加--extra-config=apiserver.Authorization.Mode=RBAC选项:

//enable RBAC and allocate 8G memory
$ minikube start --memory=8192 **--extra-config=apiserver.Authorization.Mode=RBAC**

然后,您也可以通过 Kubernetes 设置自己的 Jenkins 服务器;具体细节在本节中。

一些 minikube 版本存在kube-dns问题,无法解析外部域名,例如github.com/jenkins.io/,无法处理此教程。在启动minikube后,用coredns插件替换kube-dns插件可能会解决此问题:

$ minikube addons disable kube-dns

$ minikube addons enable coredns

如何操作...

在 Jenkins 设置中有两个重要的部分要经历:

  1. Jenkins 需要运行docker命令来构建您的应用程序以组成容器镜像

  2. Jenkins 需要与 Kubernetes 主节点通信以控制部署

为了实现第 1 步,有一个需要类似Docker-in-Docker(dind)的棘手部分。这是因为 Jenkins 是由 Kubernetes 作为一个 pod(Docker 容器)运行的,而且 Jenkins 还需要调用docker命令来构建您的应用程序。可以通过将 Kubernetes 节点上的/var/run/docker.sock挂载到可以与 Jenkins、Kubernetes 节点和 Docker 守护程序通信的 Jenkins pod 来实现。

Docker-in-Docker 和挂载/var/run/docker.sock已在blog.docker.com/2013/09/docker-can-now-run-within-docker/jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/.中有描述。

为了实现第 2 步,我们将设置一个 Kubernetes 服务账户并分配一个ClusterRole,以便 Jenkins 服务账户可以拥有必要的特权。

让我们一步一步来做。

设置自定义的 Jenkins 镜像

通过 Kubernetes 运行 Jenkins,我们使用官方镜像(hub.docker.com/u/jenkins/),但自定义安装以下应用程序:

  • Docker CE

  • kubectl 二进制文件

  • Jenkins Docker 插件

为此,准备Dockerfile来维护您自己的 Jenkins 镜像:

$ cat Dockerfile FROM jenkins/jenkins:lts   EXPOSE 8080 50000 # install Docker CE for Debian : https://docs.docker.com/engine/installation/linux/docker-ce/debian/ USER root RUN apt-get update RUN apt-get install -y sudo apt-transport-https ca-certificates curl gnupg2 software-properties-common RUN curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg | apt-key add - RUN add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") $(lsb_release -cs) stable" RUN apt-get update && apt-get install -y docker-ce # install kubectl binary RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.9.2/bin/linux/amd64/kubectl RUN chmod +x ./kubectl RUN mv ./kubectl /usr/local/bin/kubectl # setup Jenkins plubins : https://github.com/jenkinsci/docker#script-usage RUN /usr/local/bin/install-plugins.sh docker 

使用docker build来构建您的 Jenkins 镜像,然后使用docker push命令将其上传到 Docker Hub 中的您自己的注册表,如下所示:

//build your own Jenkins image $ docker build -t <your-docker-hub-account>/my-jenkins .  //push to Docker Hub $ docker push <your-docker-hub-account>/my-jenkins

或者,您可以将其上传到您的私有注册表或任何其他云提供的注册表。

哎呀!我们现在已经准备好了我们的构建系统镜像。

设置 Kubernetes 服务账户和 ClusterRole

想象一下,成功使用 Jenkins 构建您的应用程序容器后,然后使用kubectl更新部署以推出新的二进制文件。为此,从 Jenkins pod 内部调用kubectl命令。在这种情况下,我们需要凭据与 Kubernetes 主节点通信。

幸运的是,Kubernetes 支持这种情景,使用服务账户进行描述。这在第八章中有详细描述,高级集群管理。因此,这个步骤将使用最简单的方式,即使用default命名空间和cluster-admin ClusterRole

要检查 RBAC 是否启用,以及cluster-admin ClusterRole是否存在,输入kubectl get clusterrole命令:

$ kubectl get clusterrole cluster-admin NAME            AGE cluster-admin   42m

接下来,创建一个服务账户jenkins-sa,它将被 Jenkins pod 使用。准备以下 YAML 配置,并输入kubectl create命令来创建它:

$ cat jenkins-serviceaccount.yaml apiVersion: v1 kind: ServiceAccount metadata:
 name: jenkins-sa namespace: default $ kubectl create -f jenkins-serviceaccount.yaml serviceaccount "jenkins-sa" created  

现在我们可以将jenkins-sa服务账户与cluster-admin ClusterRole关联起来。准备一个ClusterRoleBinding配置,并运行kubectl create命令:

$ cat jenkins-cluteradmin.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata:
 name: jenkins-cluster-admin roleRef:
 apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - kind: ServiceAccount
 name: jenkins-sa namespace: default $ kubectl create -f jenkins-cluster-admin.yaml clusterrolebinding.rbac.authorization.k8s.io "jenkins-cluster-admin" created  

结果,如果一个 pod 使用jenkins-sa服务账户启动,这个 pod 有权限控制 Kubernetes 集群,因为有cluster-admin ClusterRole

它应该创建一个具有 Jenkins 使用最小权限的自定义ClusterRole。但这个配方是专注于 Jenkins 设置本身。如果你想创建一个自定义的ClusterRole,请转到第八章,高级集群管理

通过 Kubernetes 部署启动 Jenkins 服务器

基于之前的配方,现在你有了:

  • 一个自定义的 Jenkins 容器镜像

  • 一个服务账户

最后,你可以在你的 Kubernetes 集群上启动你的自定义 Jenkins 服务器。记住,我们需要在 Docker 环境中运行docker命令,需要从本地 Kubernetes 节点挂载/var/run/docker.sock

此外,我们需要使用jenkins-sa服务账户来启动一个 Jenkins pod。需要在部署配置中指定spec.template.spec.serviceAccountName: jenkins-sa

还建议使用PersistentVolume来保存 Jenkins 主目录(/var/jenkins_home),以防 pod 重新启动。我们只需简单地使用hostPath /data/jenkins-data目录(假设您使用 minikube)。您可以更改为另一个路径或其他类型的PersistentVolume以适应您的环境。

总的来说,Jenkins 的部署 YAML 配置如下:

$ cat jenkins.yaml apiVersion: apps/v1 kind: Deployment ...
 spec: **serviceAccountName: jenkins-sa** containers: - name: my-jenkins image: **hidetosaito/my-jenkins** readinessProbe: initialDelaySeconds: 40 tcpSocket: port: 8080  volumeMounts:        - mountPath: **/var/run/docker.sock**
 name: docker-sock 
        - mountPath: **/var/jenkins_home**
          name: jenkins-data volumes:      - name: docker-sock
 hostPath:          path: **/var/run/docker.sock**  - name: jenkins-data
        hostPath:
          path: /data/jenkins-data ... $ kubectl create -f jenkins.yaml  deployment.apps "my-jenkins" created service "my-jenkins-service" created

几分钟后,Kubernetes 拉取您的自定义 Jenkins 镜像并运行一个 Jenkins pod,该 pod 能够运行docker命令和kubectl命令,无需任何配置,因为挂载了/var/run/docker.sockjenkins-sa服务账户:

//check Jenkins Pod status
$ kubectl get pods NAME                          READY     STATUS    RESTARTS   AGE my-jenkins-758b89849c-t2sm9   1/1       Running   0          17m

//access to Jenkins Pod $ kubectl exec -it my-jenkins-758b89849c-t2sm9 -- /bin/bash   //within Jenkins Pod, you can run docker command root@my-jenkins-758b89849c-t2sm9:/# docker pull nginx Using default tag: latest latest: Pulling from library/nginx e7bb522d92ff: Pull complete 6edc05228666: Pull complete cd866a17e81f: Pull complete Digest: sha256:926b086e1234b6ae9a11589c4cece66b267890d24d1da388c96dd8795b2ffcfb Status: Downloaded newer image for nginx:latest   //within Jenkins Pod, you can run kubectl command
root@my-jenkins-758b89849c-t2sm9:/# kubectl get nodes NAME                                      STATUS    ROLES     AGE       VERSION gke-chapter5-default-pool-97f6cad9-19vm   Ready     <none>    1h        v1.8.6-gke.0 gke-chapter5-default-pool-97f6cad9-1qxc   Ready     <none>    1h        v1.8.6-gke.0 gke-chapter5-default-pool-97f6cad9-cglm   Ready     <none>    1h        v1.8.6-gke.0 //go back to your terminal
root@my-jenkins-758b89849c-t2sm9:/# exit exit  

你已经准备好了!现在你可以配置一个 Jenkins 任务来构建你的应用程序,构建一个容器,并部署到 Kubernetes。

它是如何工作的...

现在我们开始配置 Jenkins 来构建您的应用程序。但是,要访问自定义 Jenkins 的 WebUI,您需要访问绑定到 Jenkins pod 的 Kubernetes 服务。使用 kubectl port-forward 更容易远程访问配置 Jenkins:

//check pod name
$ kubectl get pods NAME                         READY     STATUS    RESTARTS   AGE my-jenkins-cbdd6446d-ttxj5   1/1       Running   0          1m

//port forward from your machine :58080 to Jenkins :8080 $ kubectl port-forward my-jenkins-cbdd6446d-ttxj5 58080:8080 Forwarding from 127.0.0.1:58080 -> 8080

通过以下步骤完成 Jenkins 的初始配置:

  1. 访问 http://127.0.0.1:58080 的 Jenkins WebUI;它会要求您输入 initialAdminPassword

  2. 使用 kubectl exec 获取 initialAdminPassword。然后将其复制粘贴到 Jenkins WebUI 中,以进行初始配置以安装建议的插件并创建管理员用户:

$ kubectl get pods NAME                         READY     STATUS    RESTARTS   AGE my-jenkins-cbdd6446d-ttxj5   1/1       Running   0          1m //now you see initialAdminPassword

$ kubectl exec my-jenkins-cbdd6446d-ttxj5 -- /bin/bash -c 'cat /var/jenkins_home/secrets/initialAdminPassword' 47e236f0bf334f838c33f80aac206c22
  1. 您将看到 Jenkins 的顶部页面。然后点击管理 Jenkins,然后配置系统:

导航到 Jenkins 配置

  1. 滚动到底部,找到云部分。点击“添加新的云”以选择 Docker:

添加 Docker 设置

  1. 将名称设置为您想要的名称(例如:my-docker),并指定 Docker 主机 URI 和 Docker 域套接字为 unix:///var/run/docker.sock

在 Jenkins 上配置 Docker

使用 Jenkins 构建 Docker 镜像

让我们配置一个 Jenkins 作业来构建一个示例微服务应用程序,该应用程序在上一个配方中介绍过(my-calc)。执行以下步骤来配置和构建 Docker 镜像:

  1. 在左侧导航中,点击新项目:

导航到创建新项目

  1. 输入所需的项目名称(例如:my-calc),选择自由风格项目,然后点击确定:

创建一个新的 Jenkins 作业

  1. 在“源代码管理”选项卡中,选择 Git,并将“存储库 URL”设置为 github.com/kubernetes-cookbook/my-calc.git,或者您可以使用自己的具有 Dockerfile 的存储库:

源代码管理设置

  1. 在“构建环境”选项卡中,点击“添加构建步骤”以添加“构建/发布 Docker 镜像”:

构建环境设置

  1. 在“构建/发布 Docker 镜像”面板中:

  2. Dockerfile 的目录为当前目录(.

  3. 在我们设置的云中选择 my-docker

  4. 将镜像设置为您的 Docker 存储库,但附加 :${BUILD_NUMBER}(例如:hidetosaito/my-calc:${BUILD_NUMBER}

  5. 启用推送镜像

  6. 点击“添加”以添加您的 Docker hub ID 凭据

  7. 然后,点击保存:

Docker 构建/发布设置

  1. 最后,您可以点击“立即构建”来触发构建;为了测试目的,您可以点击五次来看看它是如何工作的:

触发构建

  1. 请注意,您可以看到一个控制台,它知道执行 Docker 构建和推送:

显示构建日志

  1. 访问您的 Docker hub 存储库;它已被推送了五次(因为点击了五次构建):

Docker hub 存储库

就是这样!您可以实现持续集成来构建 Docker 镜像,这样当您在 GitHub 中更新源代码时,您可以持续构建并将最新的镜像推送到 Jenkins 的 Docker hub 存储库中。

部署最新的容器镜像到 Kubernetes

每次构建后,Jenkins 都会在 CI 过程结束时将您的容器镜像推送到 Docker hub 存储库。接下来,更新 Jenkins 作业配置以使用最新的镜像部署到 Kubernetes,具体步骤如下:

  1. 第一次,我们通过kubectl deploy --record手动预部署微服务应用程序。请注意,您可以将spec.template.spec.containers.image: hidetosaito/my-calc更改为您的存储库:
$ cat my-calc.yaml apiVersion: apps/v1 kind: Deployment metadata:
 name: **my-calc-deploy** spec:
 replicas: 2 selector: matchLabels: run: my-calc template: metadata: labels: run: my-calc spec: containers: - name: **my-calc** image: **hidetosaito/my-calc**

//use --record to trace the history $ kubectl create -f my-calc-deploy.yaml --record deployment.apps "my-calc-deploy" created
  1. 打开 Jenkins 作业配置;在“构建”选项卡上,在 Docker 构建设置之后,点击“添加构建步骤”,然后选择“执行 shell”:

添加构建步骤

  1. 将此 shell 脚本添加并点击保存:
#!/bin/sh

set +x

# These 2 are defined in Deployment YAML
DEPLOYMENT_NAME=my-calc-deploy
CONTAINER_NAME=my-calc

# change to your Docker Hub repository
REPOSITORY=hidetosaito/my-calc

echo "*********************"
echo "*** before deploy ***"
echo "*********************"
kubectl rollout history deployment $DEPLOYMENT_NAME
kubectl set image deployment $DEPLOYMENT_NAME $CONTAINER_NAME=$REPOSITORY:$BUILD_NUMBER

echo "******************************************"
echo "*** waiting to complete rolling update ***"
echo "******************************************"
kubectl rollout status --watch=true deployment $DEPLOYMENT_NAME

echo "********************"
echo "*** after deploy ***"
echo "********************"
kubectl rollout history deployment $DEPLOYMENT_NAME

  1. 触发新的构建;您可以看到在 Docker 推送之后,它运行了前面的脚本:

Kubernetes 滚动结果

现在您可以将持续集成扩展到持续交付!您可以扩展以添加单元测试或集成测试,并将回滚机制添加到上述脚本中,以使您的 CI/CD 工作更加强大。

第六章:在 AWS 上构建 Kubernetes

本章涵盖以下内容:

  • 使用亚马逊网络服务

  • 通过 kops 设置 Kubernetes

  • 使用 AWS 作为 Kubernetes 云提供商

  • 通过 kops 在 AWS 上管理 Kubernetes 集群

介绍

根据最近的云原生计算基金会(CNCF)的调查,亚马逊网络服务AWS)是生产级 Kubernetes 系统的主要解决方案(www.cncf.io/blog/2017/12/06/cloud-native-technologies-scaling-production-applications/)。在本章中,您将了解 AWS 的云服务,以及这些服务如何共同工作以提供强大的 Kubernetes 系统。我们还将介绍 kops 的工作原理,这是一个用于 Kubernetes 操作的工具,可以帮助我们管理 Kubernetes 集群。让我们一起探索 AWS 中的 Kubernetes 世界!

使用亚马逊网络服务

亚马逊网络服务(aws.amazon.com)是最受欢迎的公共云服务。它提供虚拟服务器(EC2)、软件定义网络(VPC)、对象存储(S3)等在线服务。这是一个适合建立 Kubernetes 集群的基础设施。我们将探索 AWS 以了解 AWS 的基本功能。

准备工作

首先,您需要注册 AWS。AWS 提供免费套餐,允许您免费使用一定数量的 AWS 资源,有效期为 12 个月。访问aws.amazon.com/free/注册您的信息和信用卡。可能需要 24 小时来验证和激活您的账户。

一旦您的 AWS 账户激活,我们需要创建一个身份和访问管理IAM)用户,通过 API 来控制您的 AWS 基础设施。然后,在您的计算机上安装 AWS CLI。

创建 IAM 用户

执行以下步骤创建 IAM 用户:

  1. 转到 AWS Web 控制台console.aws.amazon.com

  2. 点击 IAM(使用搜索框,这样更容易找到):

访问 IAM 控制台

  1. 点击左侧导航中的用户,然后点击添加用户:

创建 IAM 用户

  1. 输入用户名chap6,然后选择编程访问:

创建 chap6 用户

  1. 选择直接附加现有策略,如下截图所示,然后选择以下策略:
  • AmazonEC2FullAccess

  • AmazonRoute53FullAcccess

  • AmazonS3FullAccess

  • AmazonVPCFullAccess

  • IAMFullAccess

附加必要的策略

  1. 最终,它会生成访问密钥 ID 和秘密访问密钥。将其复制并粘贴到文本编辑器中,或单击下载 .csv 以保存到您的计算机:

下载访问密钥 ID 和秘密访问密钥

在 macOS 上安装 AWS CLI

使用 HomeBrew(brew.sh)将awscli安装到 macOS;这是最简单的方法。在第一章中已经介绍了 HomeBrew,构建您自己的 Kubernetes 集群,同时安装 minikube。

要在 Mac 上通过 HomeBrew 安装 awscli,请执行以下步骤:

  1. 键入以下命令以更新最新的公式:
$ brew update
  1. 指定awscli进行安装:
$ brew install awscli
  1. 使用--version选项验证aws命令:
$ aws --version aws-cli/1.15.0 Python/3.6.5 Darwin/17.5.0 botocore/1.10.0

在 Windows 上安装 AWS CLI

在 Windows 上安装 awscli;有一个 Windows 安装程序包,这是在 Windows 上安装 awscli 的最简单方式:

  1. 转到 AWS 命令行界面页面(aws.amazon.com/cli/)。

  2. 根据您的 Windows 操作系统,下载 64 位(s3.amazonaws.com/aws-cli/AWSCLI64.msi)或 32 位(s3.amazonaws.com/aws-cli/AWSCLI32.msi)的 Windows 安装程序。

  3. 启动 AWS CLI 安装程序,然后选择默认选项继续安装:

为 Windows 安装 AWS CLI

  1. 安装完成后,启动命令提示符。然后,键入带有--version选项的aws命令进行验证:

在 Windows 上显示 aws 命令

如何做...

首先,您需要为 awscli 设置 AWS 访问密钥 ID 和 AWS 秘密访问密钥。我们已经为 IAM 用户获得了chap6。我们将使用此用户的访问密钥 ID 和秘密访问密钥。

  1. 启动终端(Windows 的命令提示符),然后使用aws命令设置访问密钥 ID秘密访问密钥。还要将默认区域设置为us-east-1
$ aws configure
AWS Access Key ID [None]: <Your Access KeyID>
AWS Secret Access Key [None]: <Your Secret Access Key>
Default region name [None]: us-east-1
Default output format [None]:
  1. 使用以下命令检查chap6 IAM 用户:
$ aws iam get-user
{
   "User": {
       "Path": "/",
       "UserName": "chap6",
       "UserId": "*********************",
       "Arn": "arn:aws:iam::***************:user/chap6",
       "CreateDate": "2018-04-14T04:22:21Z"
    }
}

就是这样!现在您可以开始使用 AWS 来启动您自己的网络和实例。

它是如何工作的...

让我们探索 AWS,启动一个典型的基础架构。使用 awscli 构建您自己的 VPC、子网、网关和安全组。然后,启动 EC2 实例以了解 AWS 的基本用法。

创建 VPC 和子网

虚拟私有云VPC)是一个软件定义的网络。您可以在 AWS 上配置一个虚拟网络。子网位于 VPC 内,定义了网络块(无类域间路由CIDR)),例如192.168.1.0/24

让我们使用以下步骤创建一个 VPC 和两个子网:

  1. 创建一个具有192.168.0.0/16 CIDR 块(IP 范围:192.168.0.0192.168.255.255)的新 VPC。然后,捕获VpcId
$ aws ec2 create-vpc --cidr-block 192.168.0.0/16
{
    "Vpc": {
        "CidrBlock": "192.168.0.0/16",
        "DhcpOptionsId": "dopt-3d901958",
        "State": "pending",
        "VpcId": "vpc-69cfbd12",
        "InstanceTenancy": "default",
       "Ipv6CidrBlockAssociationSet": [],
        "CidrBlockAssociationSet": [
            {
                "AssociationId": "vpc-cidr-assoc-c35411ae",
                "CidrBlock": "192.168.0.0/16",
                "CidrBlockState": {
                    "State": "associated"
                }
            }
        ],
        "IsDefault": false,
        "Tags": []
    }
}
  1. 在 VPC(vpc-69cfbd12)下创建第一个子网,其 CIDR 块为192.168.0.0/24(IP 范围:192.168.0.0192.168.0.255),并指定可用区为us-east-1a。然后,捕获SubnetId
$ aws ec2 create-subnet --vpc-id vpc-69cfbd12 --cidr-block 192.168.0.0/24 --availability-zone us-east-1a
{
    "Subnet": {
        "AvailabilityZone": "us-east-1a",
        "AvailableIpAddressCount": 251,
        "CidrBlock": "192.168.0.0/24",
        "DefaultForAz": false,
        "MapPublicIpOnLaunch": false,
        "State": "pending",
        "SubnetId": "subnet-6296863f",
        "VpcId": "vpc-69cfbd12",
       "AssignIpv6AddressOnCreation": false,
       "Ipv6CidrBlockAssociationSet": []
    }
}
  1. us-east-1b上创建第二个子网,其 CIDR 块为192.168.1.0/24(IP 范围:192.168.1.0192.168.1.255)。然后,捕获SubnetId
$ aws ec2 create-subnet --vpc-id vpc-69cfbd12 --cidr-block 192.168.1.0/24 --availability-zone us-east-1b
{
    "Subnet": {
        "AvailabilityZone": "us-east-1b",
        "AvailableIpAddressCount": 251,
        "CidrBlock": "192.168.1.0/24",
        "DefaultForAz": false,
        "MapPublicIpOnLaunch": false,
        "State": "pending",
        "SubnetId": "subnet-ce947da9",
        "VpcId": "vpc-69cfbd12",
       "AssignIpv6AddressOnCreation": false,
       "Ipv6CidrBlockAssociationSet": []
    }
}
  1. 使用以下命令检查 VPC(vpc-69cfbd12)下的子网列表:
$ aws ec2 describe-subnets --filters "Name=vpc-id,Values=vpc-69cfbd12" --query "Subnets[*].{Vpc:VpcId,CIDR:CidrBlock,AZ:AvailabilityZone,Id:SubnetId}" --output=table
---------------------------------------------------------------------
|                          DescribeSubnets                          |
+------------+------------------+-------------------+---------------+
|     AZ     |      CIDR        |       Id          |      Vpc      |
+------------+------------------+-------------------+---------------+
|  us-east-1a|  192.168.0.0/24  |  subnet-6296863f  |  vpc-69cfbd12 |
|  us-east-1b|  192.168.1.0/24  |  subnet-ce947da9  |  vpc-69cfbd12 |
+------------+------------------+-------------------+---------------+

看起来不错!

互联网网关

访问您的 VPC 网络,您需要有一个从互联网访问它的网关。互联网网关IGW)是将互联网连接到您的 VPC 的网关。

然后,在 VPC 的子网中,您可以设置默认路由是否去往 IGW。如果路由到 IGW,该子网被归类为公共子网。然后,您可以在公共子网上分配全局 IP 地址。

让我们使用以下步骤将第一个子网(192.168.0.0/24)配置为路由到 IGW 的公共子网:

  1. 创建 IGW 并捕获InternetGatewayId
$ aws ec2 create-internet-gateway
{
   "InternetGateway": {
       "Attachments": [],
       "InternetGatewayId": "igw-e50b849d",
       "Tags": []
    }
}
  1. 将 IGW(igw-e50b849d)附加到您的 VPC(vpc-69cfbd12):
$ aws ec2 attach-internet-gateway --vpc-id vpc-69cfbd12 --internet-gateway-id igw-e50b849d
  1. 在 VPC(vpc-69cfbd12)上创建一个路由表,然后捕获RouteTableId
$ aws ec2 create-route-table --vpc-id vpc-69cfbd12
{
    "RouteTable": {
       "Associations": [],
       "PropagatingVgws": [],
       "RouteTableId": "rtb-a9e791d5",
       "Routes": [
            {
               "DestinationCidrBlock": "192.168.0.0/16",
               "GatewayId": "local",
               "Origin": "CreateRouteTable",
               "State": "active"
            }
        ],
       "Tags": [],
       "VpcId": "vpc-69cfbd12"
    }
}
  1. 为路由表(rtb-a9e791d5)设置默认路由(0.0.0.0/0)为 IGW(igw-e50b849d):
$ aws ec2 create-route --route-table-id rtb-a9e791d5 --gateway-id igw-e50b849d --destination-cidr-block 0.0.0.0/0
  1. 将路由表(rtb-a9e791d5)关联到公共子网(subnet-6296863f):
$ aws ec2 associate-route-table --route-table-id rtb-a9e791d5 --subnet-id subnet-6296863f
  1. 在公共子网(subnet-6296863f)上启用自动分配公共 IP:
$ aws ec2 modify-subnet-attribute --subnet-id subnet-6296863f --map-public-ip-on-launch

NAT-GW

如果子网的默认路由没有指向 IGW 会发生什么?该子网被归类为私有子网,无法连接到互联网。然而,在某些情况下,您的私有子网中的虚拟机需要访问互联网。例如,下载一些安全补丁。

在这种情况下,您可以设置 NAT-GW。它允许您从私有子网访问互联网。但是,它只允许出站流量,因此您不能为私有子网分配公共 IP 地址。因此,它适用于后端实例,如数据库。

让我们创建 NAT-GW,并配置第二个子网(192.168.1.0/24)作为私有子网,通过以下步骤路由到 NAT-GW:

  1. NAT-GW 需要全局 IP 地址,因此创建弹性 IPEIP):
$ aws ec2 allocate-address
{
   "PublicIp": "18.232.18.38",
   "AllocationId": "eipalloc-bad28bb3",
   "Domain": "vpc"
}
  1. 在公共子网(subnet-6296863f)上创建 NAT-GW 并分配 EIP(eipalloc-bad28bb3)。然后,捕获NatGatewayId

由于 NAT-GW 需要访问互联网,它必须位于公共子网而不是私有子网。

输入以下命令:

$ aws ec2 create-nat-gateway --subnet-id subnet-6296863f --allocation-id eipalloc-bad28bb3
{
   "NatGateway": {
       "CreateTime": "2018-04-14T18:49:36.000Z",
       "NatGatewayAddresses": [
            {
               "AllocationId": "eipalloc-bad28bb3"
            }
       ],
       "NatGatewayId": "nat-0b12be42c575bba43",
       "State": "pending",
       "SubnetId": "subnet-6296863f",
       "VpcId": "vpc-69cfbd12"
    }
}
  1. 创建路由表并捕获RouteTableId
$ aws ec2 create-route-table --vpc-id vpc-69cfbd12
{
   "RouteTable": {
       "Associations": [],
       "PropagatingVgws": [],
       "RouteTableId": "rtb-70f1870c",
       "Routes": [
            {
               "DestinationCidrBlock": "192.168.0.0/16",
               "GatewayId": "local",
               "Origin": "CreateRouteTable",
               "State": "active"
            }
        ],
       "Tags": [],
       "VpcId": "vpc-69cfbd12"
    }
}
  1. 将路由表(rtb-70f1870c)的默认路由(0.0.0.0/0)设置为 NAT-GW(nat-0b12be42c575bba43):
$ aws ec2 create-route --route-table-id rtb-70f1870c --nat-gateway-id nat-0b12be42c575bba43 --destination-cidr-block 0.0.0.0/0
  1. 将路由表(rtb-70f1870c)关联到私有子网(subnet-ce947da9):
$ aws ec2 associate-route-table --route-table-id rtb-70f1870c --subnet-id subnet-ce947da9

安全组

在启动虚拟服务器(EC2)之前,您需要创建一个具有适当安全规则的安全组。现在,我们有两个子网,公共和私有。让我们设置公共子网,使其允许来自互联网的ssh22/tcp)和http80/tcp)。然后,设置私有子网,使其允许来自公共子网的 ssh:

  1. 在 VPC 上为公共子网创建一个安全组(vpc-69cfbd12):
$ aws ec2 create-security-group --vpc-id vpc-69cfbd12 --group-name public --description "public facing host"
{
   "GroupId": "sg-dd8a3f94"
}
  1. 向公共安全组(sg-dd8a3f94)添加 ssh 允许规则:
$ aws ec2 authorize-security-group-ingress --group-id sg-dd8a3f94 --protocol tcp --port 22 --cidr 0.0.0.0/0
  1. 向公共安全组(sg-dd8a3f94)添加http允许规则:
$ aws ec2 authorize-security-group-ingress --group-id sg-dd8a3f94 --protocol tcp --port 80 --cidr 0.0.0.0/0
  1. 在 VPC 上为私有子网创建第二个安全组(vpc-69cfbd12):
$ aws ec2 create-security-group --vpc-id vpc-69cfbd12 --group-name private --description "private subnet host"
{
   "GroupId": "sg-a18c39e8"
}
  1. 向私有安全组(sg-a18c39e8)添加一个ssh允许规则:
$ aws ec2 authorize-security-group-ingress --group-id sg-a18c39e8 --protocol tcp --port 22 --source-group sg-dd8a3f94
  1. 使用以下命令检查安全组列表:
$ aws ec2 describe-security-groups --filters "Name=vpc-id, Values=vpc-69cfbd12" --query "SecurityGroups[*].{id:GroupId,name:GroupName}" --output table ---------------------------- |  DescribeSecurityGroups  | +--------------+-----------+ |  id  |  name  | +--------------+-----------+ | sg-2ed56067 | default | | sg-a18c39e8 | private | | sg-dd8a3f94 | public | +--------------+-----------+

EC2

现在您需要上传您的 ssh 公钥,然后在公共子网和私有子网上启动 EC2 实例:

  1. 上传您的 ssh 公钥(假设您的公钥位于~/.ssh/id_rsa.pub):
$ aws ec2 import-key-pair --key-name=chap6-key --public-key-material "`cat ~/.ssh/id_rsa.pub`"
  1. 使用以下参数启动第一个 EC2 实例:
  • 使用 Amazon Linux 映像:ami-1853ac65(Amazon Linux)

  • T2.nano 实例类型:t2.nano

  • Ssh 密钥:chap6-key

  • 公共子网:subnet-6296863f

  • 公共安全组:sg-dd8a3f94

$ aws ec2 run-instances --image-id ami-1853ac65 --instance-type t2.nano --key-name chap6-key --security-group-ids sg-dd8a3f94 --subnet-id subnet-6296863f
  1. 使用以下参数启动第二个 EC2 实例:
  • 使用 Amazon Linux 映像:ami-1853ac65

  • T2.nano 实例类型:t2.nano

  • Ssh 密钥:chap6-key

  • 私有子网:subnet-ce947da9

  • 私有安全组:sg-a18c39e8

$ aws ec2 run-instances --image-id ami-1853ac65 --instance-type t2.nano --key-name chap6-key --security-group-ids sg-a18c39e8 --subnet-id subnet-ce947da9
  1. 检查 EC2 实例的状态:
$ aws ec2 describe-instances --filters "Name=vpc-id,Values=vpc-69cfbd12" --query "Reservations[*].Instances[*].{id:InstanceId,PublicIP:PublicIpAddress,PrivateIP:PrivateIpAddress,Subnet:SubnetId}" --output=table
-------------------------------------------------------------------------------
|                             DescribeInstances                              |
+---------------+-----------------+------------------+------------------------+
|   PrivateIP   |   PublicIP     |     Subnet       |          id            |
+---------------+-----------------+------------------+------------------------+
|  192.168.0.206|  34.228.228.140|  subnet-6296863f|  i-03a0e49d26a2dafa4   |
|  192.168.1.218|  None           | subnet-ce947da9|  i-063080766d2f2f520   |
+---------------+-----------------+------------------+------------------------+
  1. 从您的计算机通过 SSH(使用-A选项转发您的身份验证信息)到公共 EC2 主机:
$ ssh -A ec2-user@34.228.228.140
The authenticity of host '34.228.228.140 (34.228.228.140)' can't be established.
ECDSA key fingerprint is SHA256:lE7hoBhHntVDvRItnasqyHRynajn2iuHJ7U3nsWySRU.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '34.228.228.140' (ECDSA) to the list of known hosts.
       __|  __|_  )
       _|  (    /   Amazon Linux AMI
      ___|\___|___|
https://aws.amazon.com/amazon-linux-ami/2017.09-release-notes/
8 package(s) needed for security, out of 13 available
Run "sudo yum update" to apply all updates.
[ec2-user@ip-192-168-0-206 ~]$
  1. 在公共 EC2 主机上安装并启动 nginx:
[ec2-user@ip-192-168-0-206 ~]$ sudo yum -y install nginx
[ec2-user@ip-192-168-0-206 ~]$ sudo service nginx start
Starting nginx:                                            [ OK  ]
  1. 确保您可以从您的机器访问 nginx 服务器(请参见以下截图):

访问公共主机上的 nginx web 服务器

  1. 从公共主机到私有主机的 SSH(您必须使用私有 IP 地址):
$ ssh 192.168.1.218
  1. 确保私有主机可以通过 NAT-GW 执行 yum 更新:
[ec2-user@ip-192-168-1-218 ~]$ sudo yum -y update

恭喜!您可以在 AWS 上设置自己的基础架构,如下图所示,其中包括以下内容:

  • 一个 VPC,CIDR 为192.168.0.0/16

  • IGW

  • NAT-GW

  • 两个子网

  • 公共子网:192.168.0.0/24路由到 IGW

  • 私有子网:192.168.1.0/24 路由到 NAT-GW

  • 两个 EC2 实例(公共和私有)

  • 两个安全组(允许公共 http/ssh 和私有 ssh)

现在,看一下图表:

AWS 组件图

在本节中,您已经学会了如何从头开始使用 AWS。我们已经涵盖了它的基本用途,但在 AWS 上设置 Kubernetes 时,这一点很重要。接下来,我们将探讨如何在 AWS 上设置 Kubernetes。

使用 kops 设置 Kubernetes

什么是 kops?这是 Kubernetes Operation 的缩写(github.com/kubernetes/kops)。类似于 kubeadm、minikube 和 kubespray,kops 减少了我们自己构建 Kubernetes 集群的繁重工作。它帮助创建,并为用户提供管理集群的接口。此外,kops 实现了更自动化的安装过程,并提供了一个生产级系统。它旨在支持主要的云平台,如 AWS、GCE 和 VMware vSphere。在本教程中,我们将讨论如何使用 kops 运行 Kubernetes 集群。

准备工作

在我们的主要教程之前,我们需要在您的本地主机上安装 kops。这是一个简单的步骤,下载二进制文件并将其移动到执行文件的系统目录:

// download the latest stable kops binary

$ curl -LO https://github.com/kubernetes/kops/releases/download/$(curl -s https://api.github.com/repos/kubernetes/kops/releases/latest | grep tag_name | cut -d '"' -f 4)/kops-linux-amd64

$ chmod +x kops-linux-amd64

$ sudo mv kops-linux-amd64 /usr/local/bin/kops
// verify the command is workable

$ kops version
Version 1.9.0 (git-cccd71e67)

接下来,我们需要在您的主机上准备一些 AWS 配置和集群所需的服务。参考以下项目,并确保它们已准备就绪:

  • IAM 用户:由于 kops 将为您创建和构建多个 AWS 服务组件,您必须拥有具有 kops 所需权限的 IAM 用户。我们在上一节中创建了一个名为 chap6 的 IAM 用户,该用户具有以下策略和 kops 所需的必要权限:

  • AmazonEC2FullAccess

  • AmazonRoute53FullAccess

  • AmazonS3FullAccess

  • IAMFullAccess

  • AmazonVPCFullAccess

然后,暴露 AWS 访问密钥 ID 和秘钥作为环境变量,可以在执行kops命令时将此角色应用于主机:

$ export AWS_ACCESS_KEY_ID=${string of 20 capital character combination}
$ export AWS_SECRET_ACCESS_KEY=${string of 40 character and number combination}
  • 为存储集群配置准备 S3 存储桶:在我们之后的演示中,S3 存储桶的名称将是kubernetes-cookbook

  • 为访问集群的访问点准备 Route53 DNS 域:在我们之后的演示中,我们将使用的域名是k8s-cookbook.net

如何做到这一点...

我们可以使用包含完整配置的参数,通过单个命令轻松运行 Kubernetes 集群。这些参数在下表中进行了描述:

参数描述示例中的值
--name这是集群的名称。它也将是集群入口点的域名。因此,您可以利用您的 Route53 DNS 域名来使用自定义名称,例如,{您的集群名称}.{您的 Route53 域名}my-cluster.k8s-cookbook.net
--state这指示存储集群状态的 S3 存储桶的格式为s3://{存储桶名称}s3://kubernetes-cookbook
--zones这是您需要构建集群的可用区。us-east-1a
--cloud这是云提供商。aws
--network-cidr在这里,kops 帮助创建新 VPC 的独立 CIDR 范围。10.0.0.0/16
--master-size这是 Kubernetes 主节点的实例大小。t2.large
--node-size这是 Kubernetes 节点的实例大小。t2.medium
--node-count这是集群中节点的数量。2
--network这是集群中使用的覆盖网络。calico
--topology这有助于您决定集群是否面向公众。private
--ssh-public-key这有助于为堡垒服务器分配一个 SSH 公钥,然后我们可以通过私钥登录。~/.ssh/id_rsa.pub
--bastion这会指示您创建堡垒服务器。N/A
--yes这会给您立即执行的确认。N/A

现在我们准备将配置组合成一个命令并执行它:

$ kops create cluster --name my-cluster.k8s-cookbook.net --state=s3://kubernetes-cookbook --zones us-east-1a --cloud aws --network-cidr 10.0.0.0/16 --master-size t2.large --node-size t2.medium --node-count 2 --networking calico --topology private --ssh-public-key ~/.ssh/id_rsa.pub --bastion --yes
...
I0408 15:19:21.794035   13144 executor.go:91] Tasks: 105 done / 105 total; 0 can run
I0408 15:19:21.794111   13144 dns.go:153] Pre-creating DNS records
I0408 15:19:22.420077   13144 update_cluster.go:248] Exporting kubecfg for cluster
kops has set your kubectl context to my-cluster.k8s-cookbook.net Cluster is starting.  It should be ready in a few minutes.
...

几分钟后,该命令会输出之前的日志,显示为您构建的 kops Kubernetes 集群创建了哪些 AWS 服务并为您提供了哪些服务。您甚至可以检查您的 AWS 控制台来验证它们的关系,它们看起来类似于以下图表:

由 kops 在 AWS 中创建的 Kubernetes 集群的组件

它是如何工作的...

从本地主机,用户可以使用 kops 命令与 AWS 上的集群进行交互:

//check the cluster
$ kops get cluster --state s3://kubernetes-cookbook
NAME                         CLOUD  ZONES
my-cluster.k8s-cookbook.net  aws    us-east-1a

使用 kops 构建的 AWS 集群

此外,正如您在前一节中所看到的,kops 集群创建的最后几条日志显示客户端的环境也已准备就绪。这意味着 kops 还帮助将 API 服务器安全地绑定到我们的主机。我们可以像在 Kubernetes 主节点中一样使用 kubectl 命令。我们需要做的就是手动安装 kubectl。这与安装 kops 一样简单;只需下载二进制文件:

// install kubectl on local

$ curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl

$ chmod +x kubectl

$ sudo mv kubectl /usr/local/bin/
// check the nodes in cluster on AWS

$ kubectl get nodes
NAME                          STATUS    ROLES     AGE       VERSION
ip-10-0-39-216.ec2.internal   Ready     master    2m        v1.8.7
ip-10-0-40-26.ec2.internal    Ready     node      31s       v1.8.7
ip-10-0-50-147.ec2.internal   Ready     node      33s       v1.8.7

但是,您仍然可以访问集群中的节点。由于集群设置在私有网络中,我们需要先登录到堡垒服务器,然后跳转到下一个节点:

//add private key to ssh authentication agent
$ ssh-add ~/.ssh/id_rsa

//use your private key with flag “-i”
//we avoid it since the private key is in default location, ~/.ssh/id_rsa
//also use -A option to forward an authentication agent
$ ssh -A admin@bastion.my-cluster.k8s-cookbook.net

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sun Apr  8 19:37:31 2018 from 10.0.2.167
// access the master node with its private IP
admin@ip-10-0-0-70:~$ ssh 10.0.39.216

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sun Apr  8 19:36:22 2018 from 10.0.0.70
admin@ip-10-0-39-216:~$

删除 kops 构建的 AWS 集群

我们可以使用 kops 命令简单地删除我们的集群,如下所示:

$ kops delete cluster --name my-cluster.k8s-cookbook.net --state s3://kubernetes-cookbook --yes
Deleted cluster: "my-cluster.k8s-cookbook.net"

它将为您清理 AWS 服务。但是一些其他服务是由您自己创建的:S3 存储桶,具有强大授权的 IAM 角色和 Route53 域名;kops 不会在用户行为上删除它们。请记住删除您一侧未使用的 AWS 服务。

另请参阅

  • 使用亚马逊网络服务玩耍

  • 使用 AWS 作为 Kubernetes 云提供商

  • 通过 kops 在 AWS 上管理 Kubernetes 集群

  • 通过 kubeadm 在 Linux 上设置 Kubernetes 集群[第一章],构建您自己的 Kubernetes 集群

  • 在[第一章]中通过 kubespray 在 Linux 上设置 Kubernetes 集群,构建您自己的 Kubernetes 集群

使用 AWS 作为 Kubernetes 云提供商

从 Kubernetes 1.6 开始,引入了Cloud Controller ManagerCCM),它定义了一组接口,以便不同的云提供商可以在 Kubernetes 发布周期之外演进自己的实现。与云提供商交谈时,您不能忽视最大的参与者:亚马逊网络服务。根据 Cloud Native Computing Foundation 的数据,2017 年,63%的 Kubernetes 工作负载在 AWS 上运行。AWS CloudProvider 支持Elastic Load BalancerELB)作为服务和 Amazon Elastic Block StoreEBS)作为 StorageClass。

在撰写本书时,Amazon Elastic Container Service for Kubernetes (Amazon EKS)处于预览阶段,这是 AWS 中的托管 Kubernetes 服务。理想情况下,它将与 Kubernetes 有更好的集成,例如 Ingress 的Application Load BalancerALB),授权和网络。目前在 AWS 中,VPC 中每个路由表的路由限制为 50;根据 AWS 的官方文档,可以根据请求将其增加到 100。然而,如果路由超过 50,网络性能可能会受到影响。虽然 kops 默认使用 kubenet 网络,为每个节点分配一个/24 CIDR 并在 AWS VPC 的路由表中配置路由。如果集群中的节点超过 50 个,这可能会导致性能下降。使用 CNI 网络可以解决这个问题。

准备工作

为了跟着本教程中的示例,您需要在 AWS 中创建一个 Kubernetes 集群。以下示例使用 kops 在 AWS 中创建一个名为k8s-cookbook.net的 Kubernetes 集群;正如前面的示例所示,将$KOPS_STATE_STORE设置为一个 s3 存储桶,用于存储您的 kops 配置和元数据:

# kops create cluster --master-count 1 --node-count 2 --zones us-east-1a,us-east-1b,us-east-1c --node-size t2.micro --master-size t2.small --topology private --networking calico --authorization=rbac --cloud-labels "Environment=dev" --state $KOPS_STATE_STORE --name k8s-cookbook.net 
I0408 16:10:12.212571 34744 create_cluster.go:1318] Using SSH public key: /Users/k8s/.ssh/id_rsa.pub I0408 16:10:13.959274 34744 create_cluster.go:472] Inferred --cloud=aws from zone "us-east-1a" 
I0408 16:10:14.418739 34744 subnets.go:184] Assigned CIDR 172.20.32.0/19 to subnet us-east-1a 
I0408 16:10:14.418769 34744 subnets.go:184] Assigned CIDR 172.20.64.0/19 to subnet us-east-1b I0408 16:10:14.418777 34744 subnets.go:184] Assigned CIDR 172.20.96.0/19 to subnet us-east-1c 
I0408 16:10:14.418785 34744 subnets.go:198] Assigned CIDR 172.20.0.0/22 to subnet utility-us-east-1a I0408 16:10:14.418793 34744 subnets.go:198] Assigned CIDR 172.20.4.0/22 to subnet utility-us-east-1b 
I0408 16:10:14.418801 34744 subnets.go:198] Assigned CIDR 172.20.8.0/22 to subnet utility-us-east-1c ... 
Finally configure your cluster with: kops update cluster k8s-cookbook.net --yes

一旦我们运行了推荐的 kops update cluster <cluster_name> --yes命令,几分钟后,集群就可以运行起来了。我们可以使用 kops validate cluster 来检查集群组件是否都已经启动:

# kops validate cluster
Using cluster from kubectl context: k8s-cookbook.net
Validating cluster k8s-cookbook.net
INSTANCE GROUPS
NAME                  ROLE   MACHINETYPE   MIN    MAX    SUBNETS
master-us-east-1a     Master t2.small      1      1      us-east-1a
nodes                 Node   t2.micro      2      2      us-east-1a,us-east-1b,us-east-1c
NODE STATUS           
NAME                              ROLE   READY                 
ip-172-20-44-140.ec2.internal     node   True
ip-172-20-62-204.ec2.internal     master True
ip-172-20-87-38.ec2.internal      node   True
Your cluster k8s-cookbook.net is ready

我们准备好了!

如何做到这一点...

在 AWS 中运行 Kubernetes 时,我们可以使用两种可能的集成方式:将 ELB 作为LoadBalancer类型的服务,以及将 Amazon Elastic Block Store 作为StorageClass

弹性负载均衡器作为 LoadBalancer 服务

让我们创建一个LoadBalancer服务,并在其下创建 Pods,这是我们在第三章中学到的内容,Playing with Containers

# cat aws-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      run: nginx
  template:
    metadata:
      labels:
        run: nginx
    spec:
      containers:
        - image: nginx
          name: nginx
          ports:
            - containerPort: 80
---

apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  ports:
    - port: 80
      targetPort: 80
  type: LoadBalancer
  selector:
    run: nginx

在上述模板中,我们声明了一个 nginx Pod,并将其与LoadBalancer服务关联起来。该服务将把数据包发送到容器端口80

# kubectl create -f aws-service.yaml 
deployment.apps "nginx" created 
service "nginx" created 

让我们描述一下我们的nginx服务:

# kubectl describe svc nginx
Name:                     nginx
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 run=nginx
Type:                     LoadBalancer
IP:                       100.68.35.30
LoadBalancer Ingress:     a9da4ef1d402211e8b1240ef0c7f25d3-1251329976.us-east-1.elb.amazonaws.com
Port:                     <unset>  80/TCP
TargetPort:               80/TCP
NodePort:                 <unset>  31384/TCP
Endpoints:                100.124.40.196:80,100.99.102.130:80,100.99.102.131:80
Session Affinity:         None
External Traffic Policy:  Cluster
Events:
  Type    Reason                Age   From                Message
  ----    ------                ----  ----                -------
  Normal  EnsuringLoadBalancer  2m    service-controller  Ensuring load balancer
  Normal  EnsuredLoadBalancer   2m    service-controller  Ensured load balancer 

服务创建后,我们将发现 AWS CloudProvider 将使用端点adb576a05401911e8b1240ef0c7f25d3-1637943008.us-east-1.elb.amazonaws.com来提供经典负载均衡器。我们可以通过 aws 命令行界面(aws.amazon.com/cli/)检查其详细设置。

要安装 aws CLI,您可以使用 pip 在 Mac 或 Linux 上安装(pip install awscli);对于 Windows 用户,您需要从官方网站下载安装程序。

AWS CLI 命令的组合是aws [options] <command> <subcommand> [<subcommand> ...] [parameters]。对于列出负载均衡器,我们将使用aws elb describe-load-balancers作为主要命令。使用--load-balancer-names参数将按名称过滤负载均衡器,对于--output参数,您可以选择文本、JSON 或表格:

# aws elb describe-load-balancers --load-balancer-names a9da4ef1d402211e8b1240ef0c7f25d3 --output text
LOADBALANCERDESCRIPTIONS     a9da4ef1d402211e8b1240ef0c7f25d3-1251329976.us-east-1.elb.amazonaws.com Z35SXDOTRQ7X7K 2018-04-14T20:30:45.990Z       a9da4ef1d402211e8b1240ef0c7f25d3-1251329976.us-east-1.elb.amazonaws.com a9da4ef1d402211e8b1240ef0c7f25d3    internet-facing       vpc-07374a7c
AVAILABILITYZONES     us-east-1a
AVAILABILITYZONES     us-east-1b
AVAILABILITYZONES     us-east-1c
HEALTHCHECK   2      10     TCP:31384     5      6
INSTANCES     i-03cafedc27dca591b
INSTANCES     i-060f9d17d9b473074
LISTENER      31384  TCP    80     TCP
SECURITYGROUPS sg-3b4efb72
SOURCESECURITYGROUP   k8s-elb-a9da4ef1d402211e8b1240ef0c7f25d3   516726565417
SUBNETS subnet-088f9d27
SUBNETS subnet-e7ec0580
SUBNETS subnet-f38191ae

如果我们访问这个 ELB 端点端口80,我们会看到 nginx 欢迎页面:

访问 ELB 端点以访问负载均衡器服务

在幕后,AWS CloudProvider 创建了一个 AWS 弹性负载均衡器,并通过我们刚刚定义的服务配置其入口规则和监听器。以下是流量进入 Pod 的图示:

Kubernetes 资源和 AWS 资源的示例,用于 LoadBalancer 类型的服务

外部负载均衡器接收请求并使用轮询算法将其转发到 EC2 实例。对于 Kubernetes,流量通过 NodePort 进入服务,并开始服务与 Pod 之间的通信。有关外部到服务和服务到 Pod 通信的更多信息,您可以参考第三章,使用容器

弹性块存储作为 StorageClass

我们在第二章中学习了关于卷的知识,深入了解 Kubernetes 概念。我们知道PersistentVolumeClaims用于从用户中抽象存储资源。它可以通过StorageClass动态地提供PersistentVolume。在AWS CloudProvider 中,StorageClass 的默认提供者是弹性块存储服务‎(aws-ebs)。每当您请求 PVC 时,aws-ebs 提供者将在 AWS EBS 中创建一个卷。

让我们检查一下我们集群中的存储类:

// list all storageclass
# kubectl get storageclass
NAME            PROVISIONER             AGE
default         kubernetes.io/aws-ebs   2h
gp2 (default)   kubernetes.io/aws-ebs   2h
In this recipe, we'll reuse the PVC example we mentioned in Chapter 2-6:
# cat chapter2/2-6_volumes/2-6-7_pvc.yaml
apiVersion: "v1"
kind: "PersistentVolumeClaim"
metadata:
  name: "pvclaim01"
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
// create pvc
# kubectl create -f chapter2/2-6_volumes/2-6-7_pvc.yaml
persistentvolumeclaim "pvclaim01" created
// check pvc is created successfully.
# kubectl get pvc
NAME        STATUS    VOLUME                                     CAPACITY   
pvclaim01   Bound     pvc-e3d881d4-402e-11e8-b124-0ef0c7f25d36   1Gi        
ACCESS    MODES   STORAGECLASS   AGE
RWO            gp2            16m

创建 PVC 后,将创建关联的 PV:

# kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   
pvc-e3d881d4-402e-11e8-b124-0ef0c7f25d36   1Gi        RWO
RECLAIM POLICY   STATUS    CLAIM               STORAGECLASS   REASON    AGE
Delete           Bound     default/pvclaim01   gp2                      16m

您可以在这里更仔细地查看 PV:

# kubectl describe pv pvc-e3d881d4-402e-11e8-b124-0ef0c7f25d36
Name:            pvc-e3d881d4-402e-11e8-b124-0ef0c7f25d36
Labels:          failure-domain.beta.kubernetes.io/region=us-east-1
                 failure-domain.beta.kubernetes.io/zone=us-east-1a
Annotations:     kubernetes.io/createdby=aws-ebs-dynamic-provisioner
                 pv.kubernetes.io/bound-by-controller=yes
                 pv.kubernetes.io/provisioned-by=kubernetes.io/aws-ebs
Claim:           default/pvclaim01
...
Source:
    Type:       AWSElasticBlockStore (a Persistent Disk resource in AWS)
    VolumeID:   aws://us-east-1a/vol-035ca31b9cc1820d7
    FSType:     ext4
    Partition:  0
    ReadOnly:   false

我们可以发现它与我们刚刚创建的声明pvclaim01相关联,源类型是AWSElasticBlockStore,正如预期的那样。

我们可以使用 AWS CLI 来检查我们在 EBS 中创建的卷。使用--filter Name=tag-value 我们可以过滤 EBS 中的卷:

// aws ec2 describe-volumes --filter Name=tag-value,Values=$PV_NAME
# aws ec2 describe-volumes --filter Name=tag-value,Values="pvc-e3d881d4-402e-11e8-b124-0ef0c7f25d36"{
    "Volumes": [
        {
            "AvailabilityZone": "us-east-1a",
             "Tags": [
                {   "Value": "k8s-cookbook.net",
                    "Key": "KubernetesCluster" },
                {   "Value": "default",
                    "Key": "kubernetes.io/created-for/pvc/namespace" },
                {   "Value": "k8s-cookbook.net-dynamic-pvc-e3d881d4-402e-11e8-b124-0ef0c7f25d36",
                    "Key": "Name" },
                {   "Value": "pvclaim01",
                    "Key": "kubernetes.io/created-for/pvc/name" },
                {   "Value": "owned",
                    "Key": "kubernetes.io/cluster/k8s-cookbook.net" },
                {   "Value": "pvc-e3d881d4-402e-11e8-b124-0ef0c7f25d36",
                    "Key": "kubernetes.io/created-for/pv/name" }],
            "VolumeType": "gp2",
            "VolumeId": "vol-035ca31b9cc1820d7",
         ...
        }    
   ]
}

我们可以看到 EBS 资源已经被标记了许多不同的值:通过观察这些标记,我们可以知道这个 EBS 卷与哪个 Kubernetes 集群、命名空间、PVC 和 PV 相关联。

由于 StorageClass 和 CloudProvider 支持的动态配置,卷管理不再是一个巨大的痛点。我们可以随时创建和销毁 PV。

还有更多...

在撰写本书时,Kubernetes 1.10 还没有原生的方式来支持 AWS CloudProvider 中的 Ingress 集成(理想情况下是应用负载均衡器)。作为替代,kops 提供了允许你这样做的插件。第一个是 ingress-nginx(github.com/kubernetes/kops/tree/master/addons/ingress-nginx),它由 nginx(nginx.org)和 AWS Elastic Load Balancer 提供支持。请求将通过 ELB 传递到 nginx,nginx 将根据 Ingress 中的路径定义分发请求。另一个选择是将 skipper 作为 kubernetes-ingress-controller 运行(zalando.github.io/skipper/dataclients/kubernetes)。Kops 还提供了插件来帮助你部署和利用 skipper 和 AWS 应用负载均衡器(github.com/kubernetes/kops/tree/master/addons/kube-ingress-aws-controller)。

我们期待 CCM 和 Amazon EKS(aws.amazon.com/eks/)能够通过 AWS 应用负载均衡器提供更多原生的 Ingress 集成,还有更多的功能即将推出!

通过 kops 在 AWS 上管理 Kubernetes 集群

在 kops 中,Kubernetes 的 master 和节点都作为 AWS 中的自动扩展组运行。在 kops 中,这个概念被称为实例组ig),表示集群中相同类型的实例。类似于跨区域的节点,或者每个可用性区域中的 master,我们可以通过 kops 命令行来检查它:

// kops get instancegroups or kops get ig 
# kops get instancegroups --name k8s-cookbook.net 
NAME ROLE MACHINETYPE MIN MAX ZONES 
master-us-east-1a Master t2.small 1 1 us-east-1a 
nodes Node t2.micro 2 2 us-east-1a,us-east-1b,us-east-1c 

使用 kops,你可以更改实例类型,调整实例组(主节点和节点),滚动更新和升级集群。Kops 还支持特定 AWS 功能的配置,例如为集群中的实例启用 AWS 详细监控。

准备工作

要执行这个操作,你需要在 AWS 中使用 kops 部署的 Kubernetes 集群。你需要按照本章中的先前操作来启动一个集群。在这里,我们将使用在上一个操作中创建的相同的集群:

# kops validate cluster
Using cluster from kubectl context: k8s-cookbook.net
Validating cluster k8s-cookbook.net
INSTANCE GROUPS
NAME                  ROLE   MACHINETYPE   MIN    MAX    SUBNETS
master-us-east-1a     Master t2.small      1      1      us-east-1a
nodes                 Node   t2.micro      2      2      us-east-1a,us-east-1b,us-east-1c
NODE STATUS
NAME                         ROLE   READY
ip-172-20-44-140.ec2.internal       node   True
ip-172-20-62-204.ec2.internal       master True
ip-172-20-87-38.ec2.internal node   True
Your cluster k8s-cookbook.net is ready

在上一个操作中,我们已经将KOPS_STATE_STORE环境变量设置为我们的 S3 存储桶名称之一,格式为s3://<bucket_name>,用于存储 kops 的配置和元数据。

如何做...

接下来的小节涵盖了一些集群管理员可能遇到的常见操作示例。

修改和调整实例组

如果你手动部署所有实例,修改实例组可能会很麻烦。你需要逐个更新实例或重新启动它们。通过 kops,我们可以轻松进行更新而不痛苦。

更新节点

使用 kops edit 命令,我们可以修改实例类型和节点数量:

// kops edit ig nodes
# kops edit instancegroups nodes --name k8s-cookbook.net
apiVersion: kops/v1alpha2
kind: InstanceGroup
metadata:
  creationTimestamp: 2018-04-14T19:06:47Z
  labels:
    kops.k8s.io/cluster: k8s-cookbook.net
  name: nodes
spec:
  image: kope.io/k8s-1.8-debian-jessie-amd64-hvm-ebs-2018-02-08
  machineType: t2.micro
  maxSize: 2
  minSize: 2
  nodeLabels:
    kops.k8s.io/instancegroup: nodes
  role: Node
  subnets:
  - us-east-1a
  - us-east-1b
  - us-east-1c

在这个例子中,我们将minSizemaxSize都从2修改为3。修改后,我们需要运行 kops update 来看到其生效:

# kops update cluster k8s-cookbook.net --yes
...
I0414 21:23:52.505171   16291 update_cluster.go:291] Exporting kubecfg for cluster
kops has set your kubectl context to k8s-cookbook.net
Cluster changes have been applied to the cloud.
Changes may require instances to restart: kops rolling-update cluster

一些更新将需要一个滚动更新集群。在这个例子中,kops 已经更新了 AWS 自动扩展组的配置。然后 AWS 将启动一个新的实例来适应这个变化。以下是来自 AWS 自动扩展组控制台的截图:

nodes_in_AWS_Auto_Scaling_Groups

我们可以看到配置已经更新,AWS 正在扩展一个新的实例。几分钟后,我们可以通过kops validatekubectl get nodes来检查集群状态:

# kops validate cluster
Using cluster from kubectl context: k8s-cookbook.net
Validating cluster k8s-cookbook.net
INSTANCE GROUPS
NAME                  ROLE   MACHINETYPE   MIN    MAX    SUBNETS
master-us-east-1a     Master t2.small      1      1      us-east-1a
nodes                 Node   t2.micro      3      3      us-east-1a,us-east-1b,us-east-1c
NODE STATUS
NAME                         ROLE   READY
ip-172-20-119-170.ec2.internal      node   True
ip-172-20-44-140.ec2.internal       node   True
ip-172-20-62-204.ec2.internal       master True
ip-172-20-87-38.ec2.internal node   True

一切看起来很好!

更新主节点

更新主节点与更新节点相同。请注意,同一可用区中的主节点属于一个实例组。这意味着你不能将额外的子网添加到主节点实例组中。在下面的例子中,我们将主节点数量从 1 调整为 2。

在这个操作中,我们只将主节点数量设置为 1。在现实世界中,推荐的方式是将主节点部署到至少两个可用区,并且每个区有三个主节点(一个 kops 实例组)。你可以通过--master-count--master-zones参数在启动集群时实现这一点。

现在看一下以下命令:

# kops edit ig master-us-east-1a
apiVersion: kops/v1alpha2
kind: InstanceGroup
metadata:
  creationTimestamp: 2018-04-14T19:06:47Z
  labels:
    kops.k8s.io/cluster: k8s-cookbook.net
  name: master-us-east-1a
spec:
  image: kope.io/k8s-1.8-debian-jessie-amd64-hvm-ebs-2018-02-08
  machineType: t2.small
  maxSize: 1
  minSize: 1
  nodeLabels:
    kops.k8s.io/instancegroup: master-us-east-1a
  role: Master
  subnets:
  - us-east-1a

在应用更改之前,我们可以在干跑模式下运行更新集群命令,不带--yes

# kops update cluster k8s-cookbook.net
...
Will modify resources:
  AutoscalingGroup/master-us-east-1a.masters.k8s-cookbook.net
       MinSize                1 -> 2
       MaxSize                1 -> 2
Must specify --yes to apply changes

验证干跑消息后,我们可以按以下方式执行更新。在这种情况下,我们将执行滚动更新。

如何知道是否需要进行滚动更新

如果我们在前面的示例中没有运行 kops 滚动更新,那么在运行 kops 验证集群时,kops 将显示验证错误:

验证错误

种类 名称 消息

InstanceGroup master-us-east-1a InstanceGroup master-us-east-1a did not have enough nodes 1 vs 2

记得用你的集群名称替换 k8s-cookbook.net。

# kops update cluster k8s-cookbook.net –-yes && kops rolling-update cluster
...
Using cluster from kubectl context: k8s-cookbook.net
NAME                  STATUS NEEDUPDATE    READY  MIN    MAX    NODES
master-us-east-1a     Ready  0             2      2      2      1
nodes                 Ready  0             3      3      3      3
No rolling-update required.

就像修改节点一样,我们可以使用kubectl get nodeskops validate cluster来检查新的主节点是否已加入集群。

升级集群

为了演示我们如何升级 Kubernetes 版本,我们将首先使用 1.8.7 版本启动集群。有关参数的详细说明,请参阅本章中的先前示例。输入以下命令:

// launch a cluster with additional parameter --kubernetes-version 1.8.7 # kops create cluster --master-count 1 --node-count 2 --zones us-east-1a,us-east-1b,us-east-1c --node-size t2.micro --master-size t2.small --topology private --networking calico --authorization=rbac --cloud-labels "Environment=dev" --state $KOPS_STATE_STORE --kubernetes-version 1.8.7 --name k8s-cookbook.net --yes 

几分钟后,我们可以看到主节点和节点已升级到 1.8.7 版本:

# kubectl get nodes 
NAME STATUS ROLES AGE VERSION 
ip-172-20-44-128.ec2.internal Ready master 3m v1.8.7 
ip-172-20-55-191.ec2.internal Ready node 1m v1.8.7 
ip-172-20-64-30.ec2.internal Ready node 1m v1.8.7

在以下示例中,我们将演示如何使用 kops 将 Kubernetes 集群从 1.8.7 升级到 1.9.3。首先运行 kops 升级集群命令。Kops 将向我们显示可以升级到的最新版本:

# kops upgrade cluster k8s-cookbook.net --yes 
ITEM PROPERTY OLD NEW 
Cluster KubernetesVersion 1.8.7 1.9.3 
Updates applied to configuration. You can now apply these changes, 
using `kops update cluster k8s-cookbook.net` 

这表明配置已经更新,我们现在需要更新集群。我们首先以干跑模式运行命令,以检查将被修改的内容:

// update cluster
# kops update cluster k8s-cookbook.net
...
Will modify resources:
  LaunchConfiguration/master-us-east-1a.masters.k8s-cookbook.net
       UserData
                             ...
                             +   image: gcr.io/google_containers/kube-apiserver:v1.9.3
                             -   image: gcr.io/google_containers/kube-apiserver:v1.8.7
                             ...
                             +   image: gcr.io/google_containers/kube-controller
manager:v1.9.3
                             -   image: gcr.io/google_containers/kube-controller-manager:v1.8.7
                             ...
                                 hostnameOverride: '@aws'
                             +   image: gcr.io/google_containers/kube-proxy:v1.9.3
                             -   image: gcr.io/google_containers/kube-proxy:v1.8.7
                                 logLevel: 2
                               kubeScheduler:
                             +   image: gcr.io/google_containers/kube-scheduler:v1.9.3
                             -   image: gcr.io/google_containers/kube
scheduler:v1.8.7
                             ...
Must specify --yes to apply changes

我们可以看到所有组件都从 v1.8.7 移动到了 v1.9.3 的自动扩展启动配置中。在验证一切正常后,我们可以使用--yes参数运行相同的命令:

// run the same command with --yes 
# kops update cluster k8s-cookbook.net --yes 
... 
kops has set your kubectl context to k8s-cookbook.net 
Cluster changes have been applied to the cloud. 
Changes may require instances to restart: kops rolling-update cluster 

在这种情况下,我们需要为集群运行滚动更新:

# kops rolling-update cluster --yes
Using cluster from kubectl context: k8s-cookbook.net
NAME                  STATUS        NEEDUPDATE    READY  MIN    MAX    NODES
master-us-east-1a     NeedsUpdate   1             0      1      1      1
nodes                 NeedsUpdate   2             0      2      2      2
I0414 22:45:05.887024   51333 rollingupdate.go:193] Rolling update completed for cluster "k8s-cookbook.net"!

所有节点都已升级到 1.9.3!在执行滚动更新时,kops 首先排空一个实例,然后封锁节点。自动扩展组将启动另一个包含更新的用户数据的节点,其中包含了 Kubernetes 组件镜像。为了避免停机时间,您应该将多个主节点和节点作为基本部署。

完成滚动更新后,我们可以通过kubectl get nodes检查集群版本:

# kubectl get nodes
NAME                            STATUS    ROLES     AGE       VERSION
ip-172-20-116-81.ec2.internal   Ready     node      14m       v1.9.3
ip-172-20-41-113.ec2.internal   Ready     master    17m       v1.9.3
ip-172-20-56-230.ec2.internal   Ready     node      8m        v1.9.3

所有节点都已升级到 1.9.3!

还有更多...

在 kops 中,有许多有用的插件,比如自动扩展节点(github.com/kubernetes/kops/tree/master/addons/cluster-autoscaler)和将服务映射到 Route53 中的记录(github.com/kubernetes/kops/tree/master/addons/route53-mapper)。请参考插件页面以了解更多信息!

另请参阅

  • 在《深入理解 Kubernetes 概念》的第二章中部署 API

  • 在《构建高可用性集群》的第四章中构建多个主节点

  • 在《在 GCP 上构建 Kubernetes》的第七章中管理 GKE 上的 Kubernetes 集群