## 基於Docker和Kubernetes的企業級DevOps實踐訓練營
### 課程准備
1. 離線鏡像包
百度:https://pan.baidu.com/s/1N1AYGCYftYGn6L0QPMWIMw 提取碼:ev2h
天翼雲:https://cloud.189.cn/t/ENjUbmRR7FNz
3. CentOS7.4版本以上 虛擬機3台(4C+8G+50G),內網互通,可連外網
3. 課件文檔
- 《訓練營課件》
- 《安裝手冊》
4. git倉庫
https://gitee.com/agagin/python-demo.git python demo項目
https://gitee.com/agagin/demo-resources.git demo項目演示需要的資源文件
### 關於本人
李永信
2012-2017,雲平台開發工程師,先后對接過Vmware、OpenStack、Docker平台
2017-2019, 運維開發工程師,Docker+Kubernetes的Paas平台運維開發
2019至今,DevOps工程師
8年多的時間,積攢了一定的開發和運維經驗,跟大家分享。
### 課程安排
2020.4.11 Docker + kubernetes
2020.4.18 DevOps平台實踐
2天的時間,節奏會相對快一些
小調研:
- A : 只聽過docker,幾乎沒有docker的使用經驗
- B:有一定的docker實踐經驗,不熟悉或者幾乎沒用過k8s
- C:對於docker和k8s都有一定的實踐經驗,想更多了解如何基於docker+k8s構建devops平台
- D:其他
### 課程介紹
最近的三年多時間,關注容器圈的話應該會知道這么幾個事情:
- 容器技術持續火爆

- Kubernetes(k8s)成為容器編排管理的標准
- 國內外廠商均已開始了全面擁抱Kubernetes的轉型, 無數中小型企業已經落地 Kubernetes,或正走落地的道路上 。基於目前的發展趨勢可以預見,未來幾年以kubernetes平台為核心的容器運維管理、DevOps等將迎來全面的發展。
本着實踐為核心的思想,本課程使用企業常見的基於Django + uwsgi + Nginx架構的Python Demo項目,分別講述三個事情:
- 項目的容器化
教大家如何把公司的項目做成容器,並且運行在docker環境中
- 使用Kubernetes集群來管理容器化的項目
帶大家一步一步部署k8s集群,並把容器化后的demo項目使用k8s來管理起來
- 使用Jenkins和Kubernetes集成,實現demo項目的持續集成/持續交付(CI/CD)
會使用k8s管理應用生命周期后,還差最后的環節,就是如何把開發、測試、部署的流程使用自動化工具整合起來,最后一部分呢,課程會教會大家如何優雅的使用gitlab+Jenkins+k8s構建企業級的DevOps平台
### 流程示意

### 你將學到哪些
- Docker相關
- 如何使用Dockerfile快速構建鏡像
- Docker鏡像、容器、倉庫的常用操作
- Docker容器的網絡(Bridge下的SNAT、DNAT)
- Kubernetes相關
- 集群的快速搭建
- kubernetes的架構及工作流程
- 使用Pod控制器管理業務應用的生命周期
- 使用CoreDNS、Service和Ingress實現服務發現、負載均衡及四層、七層網絡的訪問
- Kubernetes的認證授權體系
- 使用EFK構建集群業務應用的日志收集系統
- 基於Gitlab+Jenkins+k8s構建DevOps平台
- Jenkins介紹及流水線的使用
- Jenkinsfile及多分支流水線的實際應用
- Jenkins集成sonarQube、Docker、Kubernetes
- 使用groovy編寫sharedLibrary,實現CI/CD流程的優化
### 第一章 走進Docker的世界
介紹docker的前世今生,了解docker的實現原理,以Django項目為例,帶大家如何編寫最佳的Dockerfile構建鏡像。通過本章的學習,大家會知道docker的概念及基本操作,並學會構建自己的業務鏡像,並通過抓包的方式掌握Docker最常用的bridge網絡模式的通信。
#### 認識docker
###### 怎么出現的
- 輕量、高效的虛擬化
Docker 公司位於舊金山,原名dotCloud,底層利用了Linux容器技術(在操作系統中實現資源隔離與限制)。為了方便創建和管理這些容器,dotCloud 開發了一套內部工具,之后被命名為“Docker”。Docker就是這樣誕生的。
(思考為啥要用Linux容器技術?)

Hypervisor: 一種運行在基礎物理服務器和操作系統之間的中間軟件層,可允許多個操作系統和應用共享硬件 。常見的VMware的 Workstation 、ESXi、微軟的Hyper-V或者思傑的XenServer。
Container Runtime:通過Linux內核虛擬化能力管理多個容器,多個容器共享一套操作系統內核。因此摘掉了內核占用的空間及運行所需要的耗時,使得容器極其輕量與快速。
- 軟件交付過程中的環境依賴

###### 幾個知識點
- 可以把應用程序代碼及運行依賴環境打包成鏡像,作為交付介質,在各環境部署
- 可以將鏡像(image)啟動成為容器(container),並且提供多容器的生命周期進行管理(啟、停、刪)
- container容器之間相互隔離,且每個容器可以設置資源限額
- 提供輕量級虛擬化功能,容器就是在宿主機中的一個個的虛擬的空間,彼此相互隔離,完全獨立
- CS架構的軟件產品

###### 版本管理
- Docker 引擎主要有兩個版本:企業版(EE)和社區版(CE)
- 每個季度(1-3,4-6,7-9,10-12),企業版和社區版都會發布一個穩定版本(Stable)。社區版本會提供 4 個月的支持,而企業版本會提供 12 個月的支持
- 每個月社區版還會通過 Edge 方式發布月度版
- 從 2017 年第一季度開始,Docker 版本號遵循 YY.MM-xx 格式,類似於 Ubuntu 等項目。例如,2018 年 6 月第一次發布的社區版本為 18.06.0-ce

###### 發展史
13年成立,15年開始,迎來了飛速發展。

Docker 1.8之前,使用[LXC](https://linuxcontainers.org/fr/lxc/introduction/),Docker在上層做了封裝, 把LXC復雜的容器創建與使用方式簡化為自己的一套命令體系。
之后,為了實現跨平台等復雜的場景,Docker抽出了libcontainer項目,把對namespace、cgroup的操作封裝在libcontainer項目里,支持不同的平台類型。
2015年6月,Docker牽頭成立了 OCI(Open Container Initiative開放容器計划)組織,這個組織的目的是建立起一個圍繞容器的通用標准 。 容器格式標准是一種不受上層結構綁定的協議,即不限於某種特定操作系統、硬件、CPU架構、公有雲等 , 允許任何人在遵循該標准的情況下開發應用容器技術,這使得容器技術有了一個更廣闊的發展空間。
OCI成立后,libcontainer 交給OCI組織來維護,但是libcontainer中只包含了與kernel交互的庫,因此基於libcontainer項目,后面又加入了一個CLI工具,並且項目改名為runC (https://github.com/opencontainers/runc ), 目前runC已經成為一個功能強大的runtime工具。
Docker也做了架構調整。將容器運行時相關的程序從docker daemon剝離出來,形成了**containerd**。containerd向上為Docker Daemon提供了`gRPC接口`,使得Docker Daemon屏蔽下面的結構變化,確保原有接口向下兼容。向下通過`containerd-shim`結合`runC`,使得引擎可以獨立升級,避免之前Docker Daemon升級會導致所有容器不可用的問題。

也就是說
- runC(libcontainer)是符合OCI標准的一個實現,與底層系統交互
- containerd是實現了OCI之上的容器的高級功能,比如鏡像管理、容器執行的調用等
- Dockerd目前是最上層與CLI交互的進程,接收cli的請求並與containerd協作
###### 小結
1. 為了解決軟件交付過程中的環境依賴,同時提供一種更加輕量的虛擬化技術,Docker出現了
2. Docker是一種CS架構的軟件產品,可以把代碼及依賴打包成鏡像,作為交付介質,並且把鏡像啟動成為容器,提供容器生命周期的管理
3. docker-ce,每季度發布stable版本。18.06,18.09,19.03
4. 發展至今,docker已經通過制定OCI標准對最初的項目做了拆分,其中runC和containerd是docker的核心項目,理解docker整個請求的流程,對我們深入理解docker有很大的幫助
#### 安裝
###### 配置宿主機網卡轉發
```powershell
## 配置網卡轉發,看值是否為1
$ sysctl -a |grep -w net.ipv4.ip_forward
net.ipv4.ip_forward = 1
## 若未配置,需要執行如下
$ cat <<EOF > /etc/sysctl.d/docker.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward=1
EOF
$ sysctl -p /etc/sysctl.d/docker.conf
```
###### Yum安裝配置docker
```powershell
## 下載阿里源repo文件
$ curl -o /etc/yum.repos.d/Centos-7.repo http://mirrors.aliyun.com/repo/Centos-7.repo
$ curl -o /etc/yum.repos.d/docker-ce.repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
$ yum clean all && yum makecache
## yum安裝
$ yum install -y docker-ce
## 查看源中可用版本
$ yum list docker-ce --showduplicates | sort -r
## 安裝指定版本
##yum install -y docker-ce-18.09.9
## 配置源加速
## https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors
mkdir -p /etc/docker
vi /etc/docker/daemon.json
{
"registry-mirrors" : [
"https://8xpk5wnt.mirror.aliyuncs.com",
"https://dockerhub.azk8s.cn",
"https://registry.docker-cn.com",
"https://ot2k4d59.mirror.aliyuncs.com/"
]
}
## 設置開機自啟
systemctl enable docker
systemctl daemon-reload
## 啟動docker
systemctl start docker
## 查看docker信息
docker info
## docker-client
which docker
## docker daemon
ps aux |grep docker
```
#### 核心要素及常用操作詳解

三大核心要素:鏡像(Image)、容器(Container)、倉庫(Registry)
(先整體看下流程,再逐個演示)
###### 鏡像(Image)
打包了業務代碼及運行環境的包,是靜態的文件,不能直接對外提供服務。
###### 容器(Container)
鏡像的運行時,可以對外提供服務。本質上講是利用namespace和cgroup等技術在宿主機中創建的獨立的虛擬空間。
###### 倉庫(Registry)
- 公有倉庫,Docker Hub,阿里,網易...
- 私有倉庫,企業內部搭建
- Docker Registry,Docker官方提供的鏡像倉庫存儲服務
- Harbor, 是Docker Registry的更高級封裝,它除了提供友好的Web UI界面,角色和用戶權限管理,用戶操作審計等功能
- 鏡像訪問地址形式 registry.devops.com/demo/hello:latest,若沒有前面的url地址,則默認尋找Docker Hub中的鏡像,若沒有tag標簽,則使用latest作為標簽
- 公有的倉庫中,一般存在這么幾類鏡像
- 操作系統基礎鏡像(centos,ubuntu,suse,alpine)
- 中間件(nginx,redis,mysql,tomcat)
- 語言編譯環境(python,java,golang)
- 業務鏡像(django-demo...)
###### 操作演示

1. 解壓離線包
為了保證鏡像下載的速度,因此提前在一台節點下載了離線鏡像包,做解壓:
```powershell
$ tar zxf registry.tar.gz -C /opt
$ ll /opt/registry-data
total 25732
drwxr-xr-x 3 root root 4096 Apr 9 20:11 registry
-rw------- 1 root root 26344448 Apr 9 22:15 registry-v2.tar
```
2. 查看所有鏡像:
```powershell
$ docker images
```
2. 拉取鏡像:
```powershell
$ docker pull nginx:alpine
```
3. 如何唯一確定鏡像:
- image_id
- repository:tag
```powershell
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx alpine 377c0837328f 2 weeks ago 19.7MB
```
4. 導出鏡像到文件中
```powershell
$ docker save -o nginx-alpine.tar nginx:alpine
```
5. 從文件中加載鏡像
```powershell
$ docker load -i nginx-alpine.tar
```
6. 部署鏡像倉庫
https://docs.docker.com/registry/
```powershell
## 使用docker鏡像啟動鏡像倉庫服務
$ docker run -d -p 5000:5000 --restart always -v /opt/registry-data/registry:/var/lib/registry --name registry registry:2
## 默認倉庫不帶認證,若需要認證,參考https://docs.docker.com/registry/deploying/#restricting-access
```
假設啟動鏡像倉庫服務的主機地址為172.21.32.6,該目錄中已存在的鏡像列表:
| 現鏡像倉庫地址 | 原鏡像倉庫地址 |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| 172.21.32.6:5000/coreos/flannel:v0.11.0-amd64 | quay.io/coreos/flannel:v0.11.0-amd64 |
| 172.21.32.6:5000/mysql:5.7 | mysql:5.7 |
| 172.21.32.6:5000/nginx:alpine | nginx:alpine |
| 172.21.32.6:5000/centos:centos7.5.1804 | centos:centos7.5.1804 |
| 172.21.32.6:5000/elasticsearch/elasticsearch:7.4.2 | docker.elastic.co/elasticsearch/elasticsearch:7.4.2 |
| 172.21.32.6:5000/fluentd-es-root:v1.6.2-1.0 | gcr.io/google_containers/fluentd-elasticsearch:v2.4.0 |
| 172.21.32.6:5000/kibana/kibana:7.4.2 | docker.elastic.co/kibana/kibana:7.4.2 |
| 172.21.32.6:5000/kubernetesui/dashboard:v2.0.0-beta5 | kubernetesui/dashboard:v2.0.0-beta5 |
| 172.21.32.6:5000/kubernetesui/metrics-scraper:v1.0.1 | kubernetesui/metrics-scraper:v1.0.1 |
| 172.21.32.6:5000/kubernetes-ingress-controller/nginx-ingress-controller:0.30.0 | quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.30.0 |
7. 推送本地鏡像到鏡像倉庫中
```powershell
$ docker tag nginx:alpine localhost:5000/nginx:alpine
$ docker push localhost:5000/nginx:alpine
## 我的鏡像倉庫給外部訪問,不能通過localhost,嘗試使用內網地址172.21.16.3:5000/nginx:alpine
$ docker tag nginx:alpine 172.21.16.3:5000/nginx:alpine
$ docker push 172.21.16.3:5000/nginx:alpine
The push refers to repository [172.21.16.3:5000/nginx]
Get https://172.21.16.3:5000/v2/: http: server gave HTTP response to HTTPS client
## docker默認不允許向http的倉庫地址推送,如何做成https的,參考:https://docs.docker.com/registry/deploying/#run-an-externally-accessible-registry
## 我們沒有可信證書機構頒發的證書和域名,自簽名證書需要在每個節點中拷貝證書文件,比較麻煩,因此我們通過配置daemon的方式,來跳過證書的驗證:
$ cat /etc/docker/daemon.json
{
"registry-mirrors": [
"https://8xpk5wnt.mirror.aliyuncs.com"
],
"insecure-registries": [
"172.21.16.3:5000"
]
}
$ systemctl restart docker
$ docker push 172.21.16.3:5000/nginx:alpine
$ docker images # IMAGE ID相同,等於起別名或者加快捷方式
REPOSITORY TAG IMAGE ID CREATED SIZE
172.21.16.3:5000/nginx alpine 377c0837328f 4 weeks ago
nginx alpine 377c0837328f 4 weeks ago
localhost:5000/nginx alpine 377c0837328f 4 weeks ago
registry 2 708bc6af7e5e 2 months ago
```
8. 刪除鏡像
```powershell
docker rmi nginx:alpine
```
9. 查看容器列表
```powershell
## 查看運行狀態的容器列表
$ docker ps
## 查看全部狀態的容器列表
$ docker ps -a
```
10. 啟動容器
```powershell
## 后台啟動
$ docker run --name nginx -d nginx:alpine
##查看run流程#
##查看容器進程
## 等同於在虛擬機中開辟了一塊隔離的獨立的虛擬空間
## 啟動容器的同時進入容器,-ti與/bin/sh或者/bin/bash配套使用,意思未分配一個tty終端
$ docker run --name nginx -ti nginx:alpine /bin/sh
(注意:退出容器后,該容器會變成退出狀態,因為容器內部的1號進程退出)
## 實際上,在運行容器的時候,鏡像地址后面跟的命令等於是覆蓋了原有的容器的CMD命令,因此,執行的這些命令在容器內部就是1號進程,若該進程不存在了,那么容器就會處於退出的狀態,比如,宿主機中執行
1. echo 1,執行完后,該命令立馬就結束了
2. ping www.baidu.com,執行完后,命令的進程會持續運行
$ docker run -d --name test_echo nginx:alpine echo 1,容器會立馬退出
$ docker run -d --name test_ping nginx:alpine ping www.baidu.com,容器不會退出,但是因為沒有加-d參數,因此一直在前台運行,若ctrl+C終止,則容器退出,因為1號進程被終止了
## 映射端口,把容器的端口映射到宿主機中,-p <host_port>:<container_port>
$ docker run --name nginx -d -p 8080:80 nginx:alpine
## 資源限制,-cpuset-cpus用於設置容器可以使用的 vCPU 核。-c,--cpu-shares用於設置多個容器競爭 CPU 時,各個容器相對能分配到的 CPU 時間比例。假設有三個正在運行的容器,這三個容器中的任務都是 CPU 密集型的。第一個容器的 cpu 共享權值是 1024,其它兩個容器的 cpu 共享權值是 512。第一個容器將得到 50% 的 CPU 時間,而其它兩個容器就只能各得到 25% 的 CPU 時間了。如果再添加第四個 cpu 共享值為 1024 的容器,每個容器得到的 CPU 時間將重新計算。第一個容器的CPU 時間變為 33%,其它容器分得的 CPU 時間分別為 16.5%、16.5%、33%。必須注意的是,這個比例只有在 CPU 密集型的任務執行時才有用。在四核的系統上,假設有四個單進程的容器,它們都能各自使用一個核的 100% CPU 時間,不管它們的 cpu 共享權值是多少。
$ docker run --cpuset-cpus="0-3" --cpu-shares=512 --memory=500m nginx:alpine
```

11. 容器數據持久化
```powershell
## 掛載主機目錄
$ docker run --name nginx -d -v /opt:/opt -v /var/log:/var/log nginx:alpine
$ docker run --name mysql -e MYSQL_ROOT_PASSWORD=123456 -d -v /opt/mysql/:/var/lib/mysql mysql:5.7
## 使用volumes卷
$ docker volume ls
$ docker volume create my-vol
$ docker run --name nginx -d -v my-vol:/opt/my-vol nginx:alpine
$ docker exec -ti nginx touch /opt/my-vol/a.txt
## 驗證數據共享
$ docker run --name nginx2 -d -v my-vol:/opt/hh nginx:alpine
$ docker exec -ti nginx2 ls /opt/hh/
a.txt
```
12. 進入容器或者執行容器內的命令
```powershell
$ docker exec -ti <container_id_or_name> /bin/sh
$ docker exec -ti <container_id_or_name> hostname
```
13. 主機與容器之間拷貝數據
```powershell
## 主機拷貝到容器
$ echo '123'>/tmp/test.txt
$ docker cp /tmp/test.txt nginx:/tmp
$ docker exec -ti nginx cat /tmp/test.txt
123
## 容器拷貝到主機
$ docker cp nginx:/tmp/test.txt ./
```
14. 查看容器日志
```powershell
## 查看全部日志
$ docker logs nginx
## 實時查看最新日志
$ docker logs -f nginx
## 從最新的100條開始查看
$ docker logs --tail=100 -f nginx
```
15. 停止或者刪除容器
```powershell
## 停止運行中的容器
$ docker stop nginx
## 啟動退出容器
$ docker start nginx
## 刪除退出容器
$ docker rm nginx
## 刪除運行中的容器
$ docker rm -f nginx
```
16. 查看容器或者鏡像的明細
```powershell
## 查看容器詳細信息,包括容器IP地址等
$ docker inspect nginx
## 查看鏡像的明細信息
$ docker inspect nginx:alpine
```
#### Django應用容器化實踐
###### django項目介紹
- 項目地址:https://gitee.com/agagin/python-demo.git
- python3 + uwsgi + nginx + mysql
- 內部服務端口8002
###### 構建命令
```powershell
$ docker build . -t ImageName:ImageTag -f Dockerfile
```
如何理解構建鏡像的過程?
Dockerfile是一堆指令,在docker build的時候,按照該指令進行操作,最終生成我們期望的鏡像
- FROM 指定基礎鏡像,必須為第一個命令
```
格式:
FROM <image>
FROM <image>:<tag>
示例:
FROM mysql:5.7
注意:
tag是可選的,如果不使用tag時,會使用latest版本的基礎鏡像
```
- MAINTAINER 鏡像維護者的信息
```
格式:
MAINTAINER <name>
示例:
MAINTAINER Yongxin Li
MAINTAINER inspur_lyx@hotmail.com
MAINTAINER Yongxin Li <inspur_lyx@hotmail.com>
```
- COPY|ADD 添加本地文件到鏡像中
```
格式:
COPY <src>... <dest>
示例:
ADD hom* /mydir/ # 添加所有以"hom"開頭的文件
ADD test relativeDir/ # 添加 "test" 到 `WORKDIR`/relativeDir/
ADD test /absoluteDir/ # 添加 "test" 到 /absoluteDir/
```
- WORKDIR 工作目錄
```
格式:
WORKDIR /path/to/workdir
示例:
WORKDIR /a (這時工作目錄為/a)
注意:
通過WORKDIR設置工作目錄后,Dockerfile中其后的命令RUN、CMD、ENTRYPOINT、ADD、COPY等命令都會在該目錄下執行
```
- RUN 構建鏡像過程中執行命令
```
格式:
RUN <command>
示例:
RUN yum install nginx
RUN pip install django
RUN mkdir test && rm -rf /var/lib/unusedfiles
注意:
RUN指令創建的中間鏡像會被緩存,並會在下次構建中使用。如果不想使用這些緩存鏡像,可以在構建時指定--no-cache參數,如:docker build --no-cache
```
- CMD 構建容器后調用,也就是在容器啟動時才進行調用
```
格式:
CMD ["executable","param1","param2"] (執行可執行文件,優先)
CMD ["param1","param2"] (設置了ENTRYPOINT,則直接調用ENTRYPOINT添加參數)
CMD command param1 param2 (執行shell內部命令)
示例:
CMD ["/usr/bin/wc","--help"]
CMD ping www.baidu.com
注意:
CMD不同於RUN,CMD用於指定在容器啟動時所要執行的命令,而RUN用於指定鏡像構建時所要執行的命令。
```
- ENTRYPOINT 設置容器初始化命令,使其可執行化
```
格式:
ENTRYPOINT ["executable", "param1", "param2"] (可執行文件, 優先)
ENTRYPOINT command param1 param2 (shell內部命令)
示例:
ENTRYPOINT ["/usr/bin/wc","--help"]
注意:
ENTRYPOINT與CMD非常類似,不同的是通過docker run執行的命令不會覆蓋ENTRYPOINT,而docker run命令中指定的任何參數,都會被當做參數再次傳遞給ENTRYPOINT。Dockerfile中只允許有一個ENTRYPOINT命令,多指定時會覆蓋前面的設置,而只執行最后的ENTRYPOINT指令
```
- ENV
```
格式:
ENV <key> <value>
ENV <key>=<value>
示例:
ENV myName John
ENV myCat=fluffy
```
- EXPOSE
```
格式:
EXPOSE <port> [<port>...]
示例:
EXPOSE 80 443
EXPOSE 8080
EXPOSE 11211/tcp 11211/udp
注意:
EXPOSE並不會讓容器的端口訪問到主機。要使其可訪問,需要在docker run運行容器時通過-p來發布這些端口,或通過-P參數來發布EXPOSE導出的所有端口
```

###### Dockerfile
*dockerfiles/myblog/Dockerfile*
```dockerfile
# This my first django Dockerfile
# Version 1.0
# Base images 基礎鏡像
FROM centos:centos7.5.1804
#MAINTAINER 維護者信息
LABEL maintainer="inspur_lyx@hotmail.com"
#ENV 設置環境變量
ENV LANG en_US.UTF-8
ENV LC_ALL en_US.UTF-8
#RUN 執行以下命令
RUN curl -so /etc/yum.repos.d/Centos-7.repo http://mirrors.aliyun.com/repo/Centos-7.repo
RUN yum install -y python36 python3-devel gcc pcre-devel zlib-devel make net-tools
#工作目錄
WORKDIR /opt/myblog
#拷貝文件至工作目錄
COPY . .
#安裝nginx
RUN tar -zxf nginx-1.13.7.tar.gz -C /opt && cd /opt/nginx-1.13.7 && ./configure --prefix=/usr/local/nginx \
&& make && make install && ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx
RUN cp myblog.conf /usr/local/nginx/conf/myblog.conf
#安裝依賴的插件
RUN pip3 install -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com -r requirements.txt
RUN chmod +x run.sh && rm -rf ~/.cache/pip
#EXPOSE 映射端口
EXPOSE 8002
#容器啟動時執行命令
CMD ["./run.sh"]
```
執行構建:
```powershell
$ docker build . -t myblog:v1 -f Dockerfile
```
###### 定制化基礎鏡像
`dockerfiles/myblog/Dockerfile-base`
```dockerfile
# Base images 基礎鏡像
FROM centos:centos7.5.1804
#MAINTAINER 維護者信息
LABEL maintainer="inspur_lyx@hotmail.com"
#ENV 設置環境變量
ENV LANG en_US.UTF-8
ENV LC_ALL en_US.UTF-8
#RUN 執行以下命令
RUN curl -so /etc/yum.repos.d/Centos-7.repo http://mirrors.aliyun.com/repo/Centos-7.repo
RUN yum install -y python36 python3-devel gcc pcre-devel zlib-devel make net-tools
COPY nginx-1.13.7.tar.gz /opt
#安裝nginx
RUN tar -zxf /opt/nginx-1.13.7.tar.gz -C /opt && cd /opt/nginx-1.13.7 && ./configure --prefix=/usr/local/nginx && make && make install && ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx
```
```powershell
## 構建基礎鏡像
$ docker build . -t centos-python3-nginx:v1 -f Dockerfile-base
$ docker tag centos-python3-nginx:v1 172.21.32.6:5000/base/centos-python3-nginx:v1
$ docker push 172.21.32.6:5000/base/centos-python3-nginx:v1
```
###### 簡化Dockerfile
`dockerfiles/myblog/Dockerfile-optimized`
```dockerfile
# This my first django Dockerfile
# Version 1.0
# Base images 基礎鏡像
FROM centos-python3-nginx:v1
#MAINTAINER 維護者信息
LABEL maintainer="inspur_lyx@hotmail.com"
#工作目錄
WORKDIR /opt/myblog
#拷貝文件至工作目錄
COPY . .
RUN cp myblog.conf /usr/local/nginx/conf/myblog.conf
#安裝依賴的插件
RUN pip3 install -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com -r requirements.txt
RUN chmod +x run.sh && rm -rf ~/.cache/pip
#EXPOSE 映射端口
EXPOSE 8002
#容器啟動時執行命令
CMD ["./run.sh"]
```
```powershell
$ docker build . -t myblog -f Dockerfile-optimized
```
###### 運行mysql
```powershell
$ docker run -d -p 3306:3306 --name mysql -v /opt/mysql/mysql-data/:/var/lib/mysql -e MYSQL_DATABASE=myblog -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7
## 查看數據庫
$ docker exec -ti mysql bash
#/ mysql -uroot -p123456
#/ show databases;
## navicator連接
```
###### 啟動Django應用
```powershell
## 啟動容器
$ docker run -d -p 8002:8002 --name myblog -e MYSQL_HOST=172.21.32.6 -e MYSQL_USER=root -e MYSQL_PASSWD=123456 myblog
## migrate
$ docker exec -ti myblog bash
#/ python3 manage.py makemigrations
#/ python3 manage.py migrate
#/ python3 manage.py createsuperuser
## 創建超級用戶
$ docker exec -ti myblog python3 manage.py createsuperuser
## 收集靜態文件
## $ docker exec -ti myblog python3 manage.py collectstatic
```
訪問62.234.214.206:8002/admin
構建鏡像,替換默認編碼:
`dockerfiles/mysql/my.cnf`
```powershell
$ cat my.cnf
[mysqld]
user=root
character-set-server=utf8
lower_case_table_names=1
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mysql.conf.d/
```
`dockerfiles/mysql/Dockerfile`
```dockerfile
FROM mysql:5.7
COPY my.cnf /etc/mysql/my.cnf
## CMD或者ENTRYPOINT默認繼承
```
```powershell
$ docker build . -t mysql:5.7-utf8
$ docker tag mysql:5.7-utf8 172.21.16.3:5000/mysql:5.7-utf8
$ docker push 172.21.16.3:5000/mysql:5.7-utf8
## 刪除舊的mysql容器,使用新鏡像啟動,不用再次初始化
$ docker rm -f mysql
$ rm -rf /opt/mysql/mysql-data/*
$ docker run -d -p 3306:3306 --name mysql -v /opt/mysql/mysql-data/:/var/lib/mysql -e MYSQL_DATABASE=myblog -e MYSQL_ROOT_PASSWORD=123456 172.21.32.6:5000/mysql:5.7-utf8
## 重新migrate
$ docker exec -ti myblog bash
#/ python3 manage.py makemigrations
#/ python3 manage.py migrate
#/ python3 manage.py createsuperuser
```
#### 實現原理 錄屏!!!
虛擬化核心需要解決的問題:資源隔離與資源限制
- 虛擬機硬件虛擬化技術, 通過一個 hypervisor 層實現對資源的徹底隔離。
- 容器則是操作系統級別的虛擬化,利用的是內核的 Cgroup 和 Namespace 特性,此功能完全通過軟件實現。
###### Namespace 資源隔離
命名空間是全局資源的一種抽象,將資源放到不同的命名空間中,各個命名空間中的資源是相互隔離的。 通俗來講,就是docker在啟動一個容器的時候,會調用Linux Kernel Namespace的接口,來創建一塊虛擬空間,創建的時候,可以支持設置下面這幾種(可以隨意選擇),docker默認都設置。
- pid:用於進程隔離(PID:進程ID)
- net:管理網絡接口(NET:網絡)
- ipc:管理對 IPC 資源的訪問(IPC:進程間通信(信號量、消息隊列和共享內存))
- mnt:管理文件系統掛載點(MNT:掛載)
- uts:隔離主機名和域名
- user:隔離用戶和用戶組(3.8以后的內核才支持)
```go
func setNamespaces(daemon *Daemon, s *specs.Spec, c *container.Container) error {
// user
// network
// ipc
// uts
// pid
if c.HostConfig.PidMode.IsContainer() {
ns := specs.LinuxNamespace{Type: "pid"}
pc, err := daemon.getPidContainer(c)
if err != nil {
return err
}
ns.Path = fmt.Sprintf("/proc/%d/ns/pid", pc.State.GetPID())
setNamespace(s, ns)
} else if c.HostConfig.PidMode.IsHost() {
oci.RemoveNamespace(s, specs.LinuxNamespaceType("pid"))
} else {
ns := specs.LinuxNamespace{Type: "pid"}
setNamespace(s, ns)
}
return nil
}
```
###### CGroup 資源限制
通過namespace可以保證容器之間的隔離,但是無法控制每個容器可以占用多少資源, 如果其中的某一個容器正在執行 CPU 密集型的任務,那么就會影響其他容器中任務的性能與執行效率,導致多個容器相互影響並且搶占資源。如何對多個容器的資源使用進行限制就成了解決進程虛擬資源隔離之后的主要問題。

Control Groups(簡稱 CGroups)就是能夠隔離宿主機器上的物理資源,例如 CPU、內存、磁盤 I/O 和網絡帶寬。每一個 CGroup 都是一組被相同的標准和參數限制的進程。而我們需要做的,其實就是把容器這個進程加入到指定的Cgroup中。深入理解CGroup,請[點此]()。
###### UnionFS 聯合文件系統
Linux namespace和cgroup分別解決了容器的資源隔離與資源限制,那么容器是很輕量的,通常每台機器中可以運行幾十上百個容器, 這些個容器是共用一個image,還是各自將這個image復制了一份,然后各自獨立運行呢? 如果每個容器之間都是全量的文件系統拷貝,那么會導致至少如下問題:
- 運行容器的速度會變慢
- 容器和鏡像對宿主機的磁盤空間的壓力
怎么解決這個問題------Docker的存儲驅動
- 鏡像分層存儲
- UnionFS
Docker 鏡像是由一系列的層組成的,每層代表 Dockerfile 中的一條指令,比如下面的 Dockerfile 文件:
```dockerfile
FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py
```
這里的 Dockerfile 包含4條命令,其中每一行就創建了一層,下面顯示了上述Dockerfile構建出來的鏡像運行的容器層的結構:

鏡像就是由這些層一層一層堆疊起來的,鏡像中的這些層都是只讀的,當我們運行容器的時候,就可以在這些基礎層至上添加新的可寫層,也就是我們通常說的`容器層`,對於運行中的容器所做的所有更改(比如寫入新文件、修改現有文件、刪除文件)都將寫入這個容器層。
對容器層的操作,主要利用了寫時復制(CoW)技術。CoW就是copy-on-write,表示只在需要寫時才去復制,這個是針對已有文件的修改場景。 CoW技術可以讓所有的容器共享image的文件系統,所有數據都從image中讀取,只有當要對文件進行寫操作時,才從image里把要寫的文件復制到自己的文件系統進行修改。所以無論有多少個容器共享同一個image,所做的寫操作都是對從image中復制到自己的文件系統中的復本上進行,並不會修改image的源文件,且多個容器操作同一個文件,會在每個容器的文件系統里生成一個復本,每個容器修改的都是自己的復本,相互隔離,相互不影響。使用CoW可以有效的提高磁盤的利用率。

鏡像中每一層的文件都是分散在不同的目錄中的,如何把這些不同目錄的文件整合到一起呢?
UnionFS 其實是一種為 Linux 操作系統設計的用於把多個文件系統聯合到同一個掛載點的文件系統服務。 它能夠將不同文件夾中的層聯合(Union)到了同一個文件夾中,整個聯合的過程被稱為聯合掛載(Union Mount)。

上圖是AUFS的實現,AUFS是作為Docker存儲驅動的一種實現,Docker 還支持了不同的存儲驅動,包括 aufs、devicemapper、overlay2、zfs 和 Btrfs 等等,在最新的 Docker 中,overlay2 取代了 aufs 成為了推薦的存儲驅動,但是在沒有 overlay2 驅動的機器上仍然會使用 aufs 作為 Docker 的默認驅動。
#### Docker網絡 錄屏!!!
docker容器是一塊具有隔離性的虛擬系統,容器內可以有自己獨立的網絡空間,
- 多個容器之間是如何實現通信的呢?
- 容器和宿主機之間又是如何實現的通信呢?
- 使用-p參數是怎么實現的端口映射?
帶着我們就這些問題,我們來學習一下docker的網絡模型,最后我會通過抓包的方式,給大家演示一下數據包在容器和宿主機之間的轉換過程。
##### 網絡模式
我們在使用docker run創建Docker容器時,可以用--net選項指定容器的網絡模式,Docker有以下4種網絡模式:
- bridge模式,使用--net=bridge指定,默認設置
- host模式,使用--net=host指定,容器內部網絡空間共享宿主機的空間,效果類似直接在宿主機上啟動一個進程,端口信息和宿主機共用。
- container模式,使用--net=container:NAME_or_ID指定
指定容器與特定容器共享網絡命名空間
- none模式,使用--net=none指定
網絡模式為空,即僅保留網絡命名空間,但是不做任何網絡相關的配置(網卡、IP、路由等)
##### bridge模式
那我們之前在演示創建docker容器的時候其實是沒有指定的網絡模式的,如果不指定的話默認就會使用bridge模式,bridge本意是橋的意思,其實就是網橋模式,那我們怎么理解網橋,如果需要做類比的話,我們可以把網橋看成一個二層的交換機設備,我們來看下這張圖:
交換機通信簡圖

網橋模式示意圖

網橋在哪,查看網橋
```powershell
$ yum install -y bridge-utils
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.0242b5fbe57b no veth3a496ed
```
有了網橋之后,那我們看下docker在啟動一個容器的時候做了哪些事情才能實現容器間的互聯互通
Docker 創建一個容器的時候,會執行如下操作:
- 創建一對虛擬接口/網卡,也就是veth pair;
- 本地主機一端橋接 到默認的 docker0 或指定網橋上,並具有一個唯一的名字,如 veth9953b75;
- 容器一端放到新啟動的容器內部,並修改名字作為 eth0,這個網卡/接口只在容器的命名空間可見;
- 從網橋可用地址段中(也就是與該bridge對應的network)獲取一個空閑地址分配給容器的 eth0
- 配置默認路由到網橋
那整個過程其實是docker自動幫我們完成的,清理掉所有容器,來驗證。
```powershell
## 清掉所有容器
$ docker rm -f `docker ps -aq`
$ docker ps
$ brctl show # 查看網橋中的接口,目前沒有
## 創建測試容器test1
$ docker run -d --name test1 nginx:alpine
$ brctl show # 查看網橋中的接口,已經把test1的veth端接入到網橋中
$ ip a |grep veth # 已在宿主機中可以查看到
$ docker exec -ti test1 sh
/ # ifconfig # 查看容器的eth0網卡及分配的容器ip
/ # route -n # 觀察默認網關都指向了網橋的地址,即所有流量都轉向網橋,等於是在veth pair接通了網線
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
# 再來啟動一個測試容器,測試容器間的通信
$ docker run -d --name test2 nginx:alpine
$ docker exec -ti test sh
/ # sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
/ # apk add curl
/ # curl 172.17.0.8:80
## 為啥可以通信,因為兩個容器是接在同一個網橋中的,通信其實是通過mac地址和端口的的記錄來做轉發的。test1訪問test2,通過test1的eth0發送ARP廣播,網橋會維護一份mac映射表,我們可以大概通過命令來看一下,
$ brctl showmacs docker0
## 這些mac地址是主機端的veth網卡對應的mac,可以查看一下
$ ip a
```
我們如何知道網橋上的這些虛擬網卡與容器端是如何對應?
通過ifindex,網卡索引號
```powershell
## 查看test1容器的網卡索引
$ docker exec -ti test1 cat /sys/class/net/eth0/ifindex
## 主機中找到虛擬網卡后面這個@ifxx的值,如果是同一個值,說明這個虛擬網卡和這個容器的eth0網卡是配對的。
$ ip a |grep @if
```
整理腳本,快速查看對應:
```powershell
for container in $(docker ps -q); do
iflink=`docker exec -it $container sh -c 'cat /sys/class/net/eth0/iflink'`
iflink=`echo $iflink|tr -d '\r'`
veth=`grep -l $iflink /sys/class/net/veth*/ifindex`
veth=`echo $veth|sed -e 's;^.*net/\(.*\)/ifindex$;\1;'`
echo $container:$veth
done
```
上面我們講解了容器之間的通信,那么容器與宿主機的通信是如何做的?

添加端口映射:
```powershell
## 啟動容器的時候通過-p參數添加宿主機端口與容器內部服務端口的映射
$ docker run --name test -d -p 8088:80 nginx:alpine
$ curl localhost:8088
```
端口映射如何實現的?先來回顧iptables鏈表圖

訪問本機的8088端口,數據包會從流入方向進入本機,因此涉及到PREROUTING和INPUT鏈,我們是通過做宿主機與容器之間加的端口映射,所以肯定會涉及到端口轉換,那哪個表是負責存儲端口轉換信息的呢,就是nat表,負責維護網絡地址轉換信息的。因此我們來查看一下PREROUTING鏈的nat表:
```powershell
$ iptables -t nat -nvL PREROUTING
Chain PREROUTING (policy ACCEPT 159 packets, 20790 bytes)
pkts bytes target prot opt in out source destination
3 156 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
```
規則利用了iptables的addrtype拓展,匹配網絡類型為本地的包,如何確定哪些是匹配本地,
```powershell
$ ip route show table local type local
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
local 172.17.0.1 dev docker0 proto kernel scope host src 172.17.0.1
local 192.168.136.133 dev ens33 proto kernel scope host src 192.168.136.133
```
也就是說目標地址類型匹配到這些的,會轉發到我們的TARGET中,TARGET是動作,意味着對符合要求的數據包執行什么樣的操作,最常見的為ACCEPT或者DROP,此處的TARGET為DOCKER,很明顯DOCKER不是標准的動作,那DOCKER是什么呢?我們通常會定義自定義的鏈,這樣把某類對應的規則放在自定義鏈中,然后把自定義的鏈綁定到標准的鏈路中,因此此處DOCKER 是自定義的鏈。那我們現在就來看一下DOCKER這個自定義鏈上的規則。
```powershell
$ iptables -t nat -nvL DOCKER
Chain DOCKER (2 references)
pkts bytes target prot opt in out source destination
0 0 RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0
0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8088 to:172.17.0.2:80
```
此條規則就是對主機收到的目的端口為8088的tcp流量進行DNAT轉換,將流量發往172.17.0.2:80,172.17.0.2地址是不是就是我們上面創建的Docker容器的ip地址,流量走到網橋上了,后面就走網橋的轉發就ok了。
所以,外界只需訪問192.168.136.133:8088就可以訪問到容器中的服務了。
數據包在出口方向走POSTROUTING鏈,我們查看一下規則:
```powershell
$ iptables -t nat -nvL POSTROUTING
Chain POSTROUTING (policy ACCEPT 1099 packets, 67268 bytes)
pkts bytes target prot opt in out source destination
86 5438 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0
0 0 MASQUERADE tcp -- * * 172.17.0.4 172.17.0.4 tcp dpt:80
```
大家注意MASQUERADE這個動作是什么意思,其實是一種更靈活的SNAT,把源地址轉換成主機的出口ip地址,那解釋一下這條規則的意思:
這條規則會將源地址為172.17.0.0/16的包(也就是從Docker容器產生的包),並且不是從docker0網卡發出的,進行源地址轉換,轉換成主機網卡的地址。大概的過程就是ACK的包在容器里面發出來,會路由到網橋docker0,網橋根據宿主機的路由規則會轉給宿主機網卡eth0,這時候包就從docker0網卡轉到eth0網卡了,並從eth0網卡發出去,這時候這條規則就會生效了,把源地址換成了eth0的ip地址。
> 注意一下,剛才這個過程涉及到了網卡間包的傳遞,那一定要打開主機的ip_forward轉發服務,要不然包轉不了,服務肯定訪問不到。
>
###### 抓包演示
我們先想一下,我們要抓哪個網卡的包
- 首先訪問宿主機的8088端口,我們抓一下宿主機的eth0
```powershell
$ tcpdump -i eth0 port 8088 -w host.cap
```
- 然后最終包會流入容器內,那我們抓一下容器內的eth0網卡
```powershell
# 容器內安裝一下tcpdump
$ sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
$ apk add tcpdump
$ tcpdump -i eth0 port 80 -w container.cap
```
到另一台機器訪問一下,
```powershell
$ curl 172.21.32.6:8088/
```
停止抓包,拷貝容器內的包到宿主機
```powershell
$ docker cp test:/root/container.cap /root/
```
把抓到的內容拷貝到本地,使用wireshark進行分析。
```powershell
$ scp root@172.21.32.6:/root/*.cap /d/packages
```
(wireshark合並包進行分析)


進到容器內的包做DNAT,出去的包做SNAT,這樣對外面來講,根本就不知道機器內部是誰提供服務,其實這就和一個內網多個機器公用一個外網IP地址上網的效果是一樣的,對吧,那這也屬於NAT功能的一個常見的應用場景。
##### Host模式
容器內部不會創建網絡空間,共享宿主機的網絡空間
```powershell
$ docker run --net host -d --name mysql mysql:5.7
```
##### Conatiner模式
這個模式指定新創建的容器和已經存在的一個容器共享一個 Network Namespace,而不是和宿主機共享。新創建的容器不會創建自己的網卡,配置自己的 IP,而是和一個指定的容器共享 IP、端口范圍等。同樣,兩個容器除了網絡方面,其他的如文件系統、進程列表等還是隔離的。兩個容器的進程可以通過 lo 網卡設備通信。

```powershell
## 啟動測試容器,共享mysql的網絡空間
$ docker run -ti --rm --net=container:mysql busybox sh
/ # ip a
/ # netstat -tlp|grep 3306
/ # telnet localhost 3306
```
#### 實用技巧
1. 清理主機上所有退出的容器
```powershell
$ docker rm $(docker ps -aq)
```
2. 調試或者排查容器啟動錯誤
```powershell
## 若有時遇到容器啟動失敗的情況,可以先使用相同的鏡像啟動一個臨時容器,先進入容器
$ docker exec -ti --rm <image_id> bash
## 進入容器后,手動執行該容器對應的ENTRYPOINT或者CMD命令,這樣即使出錯,容器也不會退出,因為bash作為1號進程,我們只要不退出容器,該容器就不會自動退出
```
#### 本章小結
1. 為了解決軟件交付過程中的環境依賴,同時提供一種更加輕量的虛擬化技術,Docker出現了
2. 2013年誕生,15年開始迅速發展,從17.03月開始,使用時間日期管理版本,穩定版以每季度為准
3. Docker是一種CS架構的軟件產品,可以把代碼及依賴打包成鏡像,作為交付介質,並且把鏡像啟動成為容器,提供容器生命周期的管理
4. 使用yum部署docker,啟動后通過操作docker這個命令行,自動調用docker daemon完成容器相關操作
5. 常用操作
- systemctl start|stop|restart docker
- docker build | pull -> docker tag -> docker push
- docker run --name my-demo -d -p 8080:80 -v /opt/data:/data demo:v20200327
- docker cp /path/a.txt mycontainer:/opt
- docker exec -ti mycontainer /bin/sh
- docker logs -f mycontainer
6. 通過dockerfile構建業務鏡像,先使用基礎鏡像,然后通過一系列的指令把我們的業務應用所需要的運行環境和依賴都打包到鏡像中,然后通過CMD或者ENTRYPOINT指令把鏡像啟動時的入口制定好,完成封裝即可。有點類似於,先找來一個空的集裝箱(基礎鏡像),然后把項目依賴的服務都扔到集裝箱中,然后設置好服務的啟動入口,關閉箱門,即完成了業務鏡像的制作。
7. 容器的實現依賴於內核模塊提供的namespace和control-group的功能,通過namespace創建一塊虛擬空間,空間內實現了各類資源(進程、網絡、文件系統)的隔離,提供control-group實現了對隔離的空間的資源使用的限制。
8. docker鏡像使用分層的方式進行存儲,根據主機的存儲驅動的不同,實現方式會不同,kernel在3.10.0-514以上自動支持overlay2 存儲驅動,也是目前Docker推薦的方式。
9. 得益於分層存儲的模式,多個容器可以通過copy-on-write的策略,在鏡像的最上層加一個可寫層,實現一個鏡像快速啟動多個容器的場景
10. docker的網絡模式分為4種,最常用的為bridge和host模式。bridge模式通過docker0網橋,啟動容器的時候通過創建一對虛擬網卡,將容器連接在橋上,同時維護了虛擬網卡與網橋端口的關系,實現容器間的通信。容器與宿主機之間的通信通過iptables端口映射的方式,docker利用iptables的PREROUTING和POSTROUTING的nat功能,實現了SNAT與DNAT,使得容器內部的服務被完美的保護起來。
### 第二章 Kubernetes實踐之旅 錄屏!!!
本章學習kubernetes的架構及工作流程,重點介紹如何使用Deployment管理Pod生命周期,實現服務不中斷的滾動更新,通過服務發現來實現集群內部的服務間訪問,並通過ingress-nginx實現外部使用域名訪問集群內部的服務。同時介紹基於EFK如何搭建Kubernetes集群的日志收集系統。
學完本章,我們的Django demo項目已經可以運行在k8s集群中,同時我們可以使用域名進行服務的訪問。
- 架構及核心組件介紹
- 使用kubeadm快速搭建集群
- 運行第一個Pod應用
- Pod進階
- Pod控制器的使用
- 實現服務與Node綁定的幾種方式
- 負載均衡與服務發現
- 使用Ingress實現集群服務的7層代理
- Django項目k8s落地實踐
- 基於EFK實現kubernetes集群的日志平台(擴展)
- 集群認證與授權
#### 純容器模式的問題
1. 業務容器數量龐大,哪些容器部署在哪些節點,使用了哪些端口,如何記錄、管理,需要登錄到每台機器去管理?
2. 跨主機通信,多個機器中的容器之間相互調用如何做,iptables規則手動維護?
3. 跨主機容器間互相調用,配置如何寫?寫死固定IP+端口?
4. 如何實現業務高可用?多個容器對外提供服務如何實現負載均衡?
5. 容器的業務中斷了,如何可以感知到,感知到以后,如何自動啟動新的容器?
6. 如何實現滾動升級保證業務的連續性?
7. ......
#### 容器調度管理平台
Docker Swarm Mesos Google Kubernetes
2017年開始Kubernetes憑借強大的容器集群管理功能, 逐步占據市場,目前在容器編排領域一枝獨秀
https://kubernetes.io/
#### 架構圖
區分組件與資源

#### 核心組件
- ETCD:分布式高性能鍵值數據庫,存儲整個集群的所有元數據
- ApiServer: API服務器,集群資源訪問控制入口,提供restAPI及安全訪問控制
- Scheduler:調度器,負責把業務容器調度到最合適的Node節點
- Controller Manager:控制器管理,確保集群資源按照期望的方式運行
- Replication Controller
- Node controller
- ResourceQuota Controller
- Namespace Controller
- ServiceAccount Controller
- Tocken Controller
- Service Controller
- Endpoints Controller
- kubelet:運行在每運行在每個節點上的主要的“節點代理”個節點上的主要的“節點代理”
- pod 管理:kubelet 定期從所監聽的數據源獲取節點上 pod/container 的期望狀態(運行什么容器、運行的副本數量、網絡或者存儲如何配置等等),並調用對應的容器平台接口達到這個狀態。
- 容器健康檢查:kubelet 創建了容器之后還要查看容器是否正常運行,如果容器運行出錯,就要根據 pod 設置的重啟策略進行處理.
- 容器監控:kubelet 會監控所在節點的資源使用情況,並定時向 master 報告,資源使用數據都是通過 cAdvisor 獲取的。知道整個集群所有節點的資源情況,對於 pod 的調度和正常運行至關重要
- kubectl: 命令行接口,用於對 Kubernetes 集群運行命令 https://kubernetes.io/zh/docs/reference/kubectl/
- CNI實現: 通用網絡接口, 我們使用flannel來作為k8s集群的網絡插件, 實現跨節點通信
#### 工作流程

1. 用戶准備一個資源文件(記錄了業務應用的名稱、鏡像地址等信息),通過調用APIServer執行創建Pod
2. APIServer收到用戶的Pod創建請求,將Pod信息寫入到etcd中
3. 調度器通過list-watch的方式,發現有新的pod數據,但是這個pod還沒有綁定到某一個節點中
4. 調度器通過調度算法,計算出最適合該pod運行的節點,並調用APIServer,把信息更新到etcd中
5. kubelet同樣通過list-watch方式,發現有新的pod調度到本機的節點了,因此調用容器運行時,去根據pod的描述信息,拉取鏡像,啟動容器,同時生成事件信息
6. 同時,把容器的信息、事件及狀態也通過APIServer寫入到etcd中
#### 實踐--集群安裝 錄屏!!!
kubeadm https://kubernetes.io/zh/docs/reference/setup-tools/kubeadm/kubeadm/
《Kubernetes安裝手冊(非高可用版)》
###### 核心組件
靜態Pod的方式:
```powershell
## etcd、apiserver、controller-manager、kube-scheduler
$ kubectl -n kube-system get po
```
systemd服務方式:
```powershell
$ systemctl status kubelet
```
kubectl:二進制命令行工具
###### 理解集群資源
組件是為了支撐k8s平台的運行,安裝好的軟件。
資源是如何去使用k8s的能力的定義。比如,k8s可以使用Pod來管理業務應用,那么Pod就是k8s集群中的一類資源,集群中的所有資源可以提供如下方式查看:
```powershell
$ kubectl api-resources
```
如何理解namespace:
命名空間,集群內一個虛擬的概念,類似於資源池的概念,一個池子里可以有各種資源類型,絕大多數的資源都必須屬於某一個namespace。集群初始化安裝好之后,會默認有如下幾個namespace:
```powershell
$ kubectl get namespaces
NAME STATUS AGE
default Active 84m
kube-node-lease Active 84m
kube-public Active 84m
kube-system Active 84m
kubernetes-dashboard Active 71m
```
- 所有NAMESPACED的資源,在創建的時候都需要指定namespace,若不指定,默認會在default命名空間下
- 相同namespace下的同類資源不可以重名,不同類型的資源可以重名
- 不同namespace下的同類資源可以重名
- 通常在項目使用的時候,我們會創建帶有業務含義的namespace來做邏輯上的整合
###### kubectl的使用
類似於docker,kubectl是命令行工具,用於與APIServer交互,內置了豐富的子命令,功能極其強大。 https://kubernetes.io/docs/reference/kubectl/overview/
```powershell
$ kubectl -h
$ kubectl get -h
$ kubectl create -h
$ kubectl create namespace -h
```
kubectl如何管理集群資源
```powershell
$ kubectl get po -v=7
```
#### 實踐--使用k8s管理業務應用
##### 最小調度單元 Pod 錄屏!!!
docker調度的是容器,在k8s集群中,最小的調度單元是Pod(豆莢)

###### 為什么引入Pod
- 與容器引擎解耦
Docker、Rkt。平台設計與引擎的具體的實現解耦
- 多容器共享網絡|存儲|進程 空間, 支持的業務場景更加靈活
###### 使用yaml格式定義Pod
*myblog/one-pod/pod.yaml*
```yaml
apiVersion: v1
kind: Pod
metadata:
name: myblog
namespace: demo
labels:
component: myblog
spec:
containers:
- name: myblog
image: 172.21.32.6:5000/myblog
env:
- name: MYSQL_HOST # 指定root用戶的用戶名
value: "127.0.0.1"
- name: MYSQL_PASSWD
value: "123456"
ports:
- containerPort: 8002
- name: mysql
image: 172.21.32.6:5000/mysql:5.7-utf8
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: "123456"
- name: MYSQL_DATABASE
value: "myblog"
```
```json
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "myblog",
"namespace": "demo",
"labels": {
"component": "myblog"
}
},
"spec": {
"containers": [
{
"name": "myblog",
"image": "172.21.32.6:5000/myblog",
"env": [
{
"name": "MYSQL_HOST",
"value": "127.0.0.1"
},
{
"name": "MYSQL_PASSWD",
"value": "123456"
}
],
"ports": [
{
"containerPort": 8002
}
]
},
{
"name": "mysql",
...
}
]
}
}
```
| apiVersion | 含義 |
| :--------- | :----------------------------------------------------------- |
| alpha | 進入K8s功能的早期候選版本,可能包含Bug,最終不一定進入K8s |
| beta | 已經過測試的版本,最終會進入K8s,但功能、對象定義可能會發生變更。 |
| stable | 可安全使用的穩定版本 |
| v1 | stable 版本之后的首個版本,包含了更多的核心對象 |
| apps/v1 | 使用最廣泛的版本,像Deployment、ReplicaSets都已進入該版本 |
資源類型與apiVersion對照表
| Kind | apiVersion |
| :-------------------- | :-------------------------------------- |
| ClusterRoleBinding | rbac.authorization.k8s.io/v1 |
| ClusterRole | rbac.authorization.k8s.io/v1 |
| ConfigMap | v1 |
| CronJob | batch/v1beta1 |
| DaemonSet | extensions/v1beta1 |
| Node | v1 |
| Namespace | v1 |
| Secret | v1 |
| PersistentVolume | v1 |
| PersistentVolumeClaim | v1 |
| Pod | v1 |
| Deployment | v1、apps/v1、apps/v1beta1、apps/v1beta2 |
| Service | v1 |
| Ingress | extensions/v1beta1 |
| ReplicaSet | apps/v1、apps/v1beta2 |
| Job | batch/v1 |
| StatefulSet | apps/v1、apps/v1beta1、apps/v1beta2 |
快速獲得資源和版本
```powershell
$ kubectl explain pod
$ kubectl explain Pod.apiVersion
```
###### 創建和訪問Pod
```powershell
## 創建namespace, namespace是邏輯上的資源池
$ kubectl create namespace demo
## 使用指定文件創建Pod
$ kubectl create -f demo-pod.yaml
## 查看pod,可以簡寫po
## 所有的操作都需要指定namespace,如果是在default命名空間下,則可以省略
$ kubectl -n demo get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
myblog 2/2 Running 0 3m 10.244.1.146 k8s-slave1
## 使用Pod Ip訪問服務,3306和8002
$ curl 10.244.1.146:8002/blog/index/
## 進入容器,執行初始化, 不必到對應的主機執行docker exec
$ kubectl -n demo exec -ti myblog -c myblog bash
/ # env
/ # python3 manage.py migrate
$ kubectl -n demo exec -ti myblog -c mysql bash
/ # mysql -p123456
## 再次訪問服務,3306和8002
$ curl 10.244.1.146:8002/blog/index/
```
###### Infra容器
登錄`k8s-slave1`節點
```powershell
$ docker ps -a |grep myblog ## 發現有三個容器
## 其中包含mysql和myblog程序以及Infra容器
## 為了實現Pod內部的容器可以通過localhost通信,每個Pod都會啟動Infra容器,然后Pod內部的其他容器的網絡空間會共享該Infra容器的網絡空間(Docker網絡的container模式),Infra容器只需要hang住網絡空間,不需要額外的功能,因此資源消耗極低。
## 登錄master節點,查看pod內部的容器ip均相同,為pod ip
$ kubectl -n demo exec -ti myblog -c myblog bash
/ # ifconfig
$ kubectl -n demo exec -ti myblog -c mysql bash
/ # ifconfig
```
pod容器命名: ```k8s_<container_name>_<pod_name>_<namespace>_<random_string>```
###### 查看pod詳細信息
```powershell
## 查看pod調度節點及pod_ip
$ kubectl -n demo get pods -o wide
## 查看完整的yaml
$ kubectl -n demo get po myblog -o yaml
## 查看pod的明細信息及事件
$ kubectl -n demo describe pod myblog
```
###### Troubleshooting and Debugging
```powershell
#進入Pod內的容器
$ kubectl -n <namespace> exec <pod_name> -c <container_name> -ti /bin/sh
#查看Pod內容器日志,顯示標准或者錯誤輸出日志
$ kubectl -n <namespace> logs -f <pod_name> -c <container_name>
```
###### 更新服務版本
```powershell
$ kubectl apply -f demo-pod.yaml
```
###### 刪除Pod服務
```powershell
#根據文件刪除
$ kubectl delete -f demo-pod.yaml
#根據pod_name刪除
$ kubectl -n <namespace> delete pod <pod_name>
```
###### Pod數據持久化
若刪除了Pod,由於mysql的數據都在容器內部,會造成數據丟失,因此需要數據進行持久化。
- 定點使用hostpath掛載,nodeSelector定點
`myblog/one-pod/pod-with-volume.yaml`
```yaml
apiVersion: v1
kind: Pod
metadata:
name: myblog
namespace: demo
labels:
component: myblog
spec:
volumes:
- name: mysql-data
hostPath:
path: /opt/mysql/data
nodeSelector: # 使用節點選擇器將Pod調度到指定label的節點
component: mysql
containers:
- name: myblog
image: 172.21.32.6:5000/myblog
env:
- name: MYSQL_HOST # 指定root用戶的用戶名
value: "127.0.0.1"
- name: MYSQL_PASSWD
value: "123456"
ports:
- containerPort: 8002
- name: mysql
image: 172.21.32.6:5000/mysql:5.7-utf8
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: "123456"
- name: MYSQL_DATABASE
value: "myblog"
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
```
保存文件為`pod-with-volume.yaml`,執行創建
```powershell
## 若存在舊的同名服務,先刪除掉,后創建
$ kubectl -n demo delete pod myblog
## 創建
$ kubectl create -f pod-with-volume.yaml
## 此時pod狀態Pending
$ kubectl -n demo get po
NAME READY STATUS RESTARTS AGE
myblog 0/2 Pending 0 32s
## 查看原因,提示調度失敗,因為節點不滿足node selector
$ kubectl -n demo describe po myblog
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 12s (x2 over 12s) default-scheduler 0/3 nodes are available: 3 node(s) didn't match node selector.
## 為節點打標簽
$ kubectl label node k8s-slave1 component=mysql
## 再次查看,已經運行成功
$ kubectl -n demo get po
NAME READY STATUS RESTARTS AGE IP NODE
myblog 2/2 Running 0 3m54s 10.244.1.150 k8s-slave1
## 到k8s-slave1節點,查看/opt/mysql/data
$ ll /opt/mysql/data/
total 188484
-rw-r----- 1 polkitd input 56 Mar 29 09:20 auto.cnf
-rw------- 1 polkitd input 1676 Mar 29 09:20 ca-key.pem
-rw-r--r-- 1 polkitd input 1112 Mar 29 09:20 ca.pem
drwxr-x--- 2 polkitd input 8192 Mar 29 09:20 sys
...
## 執行migrate,創建數據庫表,然后刪掉pod,再次創建后驗證數據是否存在
$ kubectl -n demo exec -ti myblog python3 manage.py migrate
## 訪問服務,正常
$ curl 10.244.1.150:8002/blog/index/
## 刪除pod
$ kubectl delete -f pod-with-volume.yaml
## 再次創建Pod
$ kubectl create -f pod-with-volume.yaml
## 查看pod ip並訪問服務
$ kubectl -n demo get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
myblog 2/2 Running 0 7s 10.244.1.151 k8s-slave1
## 未做migrate,服務正常
$ curl 10.244.1.151:8002/blog/index/
```
- 使用PV+PVC連接分布式存儲解決方案
- ceph
- glusterfs
- nfs
###### 服務健康檢查
檢測容器服務是否健康的手段,若不健康,會根據設置的重啟策略(restartPolicy)進行操作,兩種檢測機制可以分別單獨設置,若不設置,默認認為Pod是健康的。
兩種機制:
- LivenessProbe探針
用於判斷容器是否存活,即Pod是否為running狀態,如果LivenessProbe探針探測到容器不健康,則kubelet將kill掉容器,並根據容器的重啟策略是否重啟,如果一個容器不包含LivenessProbe探針,則Kubelet認為容器的LivenessProbe探針的返回值永遠成功。
- ReadinessProbe探針
用於判斷容器是否正常提供服務,即容器的Ready是否為True,是否可以接收請求,如果ReadinessProbe探測失敗,則容器的Ready將為False,控制器將此Pod的Endpoint從對應的service的Endpoint列表中移除,從此不再將任何請求調度此Pod上,直到下次探測成功。(剔除此pod不參與接收請求不會將流量轉發給此Pod)。
三種類型:
- exec:通過執行命令來檢查服務是否正常,回值為0則表示容器健康
- httpGet方式:通過發送http請求檢查服務是否正常,返回200-399狀態碼則表明容器健康
- tcpSocket:通過容器的IP和Port執行TCP檢查,如果能夠建立TCP連接,則表明容器健康
示例:
完整文件路徑 `myblog/one-pod/pod-with-healthcheck.yaml`
```yaml
containers:
- name: myblog
image: 172.21.32.6:5000/myblog
env:
- name: MYSQL_HOST # 指定root用戶的用戶名
value: "127.0.0.1"
- name: MYSQL_PASSWD
value: "123456"
ports:
- containerPort: 8002
livenessProbe:
httpGet:
path: /blog/index/
port: 8002
scheme: HTTP
initialDelaySeconds: 10 # 容器啟動后第一次執行探測是需要等待多少秒
periodSeconds: 15 # 執行探測的頻率
timeoutSeconds: 2 # 探測超時時間
readinessProbe:
httpGet:
path: /blog/index/
port: 8002
scheme: HTTP
initialDelaySeconds: 10
timeoutSeconds: 2
periodSeconds: 15
```
- initialDelaySeconds:容器啟動后第一次執行探測是需要等待多少秒。
- periodSeconds:執行探測的頻率。默認是10秒,最小1秒。
- timeoutSeconds:探測超時時間。默認1秒,最小1秒。
- successThreshold:探測失敗后,最少連續探測成功多少次才被認定為成功。默認是1。對於liveness必須是1,最小值是1。
- failureThreshold:探測成功后,最少連續探測失敗多少次
才被認定為失敗。默認是3,最小值是1。
重啟策略:
Pod的重啟策略(RestartPolicy)應用於Pod內的所有容器,並且僅在Pod所處的Node上由kubelet進行判斷和重啟操作。當某個容器異常退出或者健康檢查失敗時,kubelet將根據RestartPolicy的設置來進行相應的操作。
Pod的重啟策略包括Always、OnFailure和Never,默認值為Always。
- Always:當容器失敗時,由kubelet自動重啟該容器;
- OnFailure:當容器終止運行且退出碼不為0時,有kubelet自動重啟該容器;
- Never:不論容器運行狀態如何,kubelet都不會重啟該容器。
###### 鏡像拉取策略
```yaml
spec:
containers:
- name: myblog
image: 172.21.32.6:5000/demo/myblog
imagePullPolicy: IfNotPresent
```
設置鏡像的拉取策略,默認為IfNotPresent
- Always,總是拉取鏡像,即使本地有鏡像也從倉庫拉取
- IfNotPresent ,本地有則使用本地鏡像,本地沒有則去倉庫拉取
- Never,只使用本地鏡像,本地沒有則報錯
###### Pod資源限制
為了保證充分利用集群資源,且確保重要容器在運行周期內能夠分配到足夠的資源穩定運行,因此平台需要具備
Pod的資源限制的能力。 對於一個pod來說,資源最基礎的2個的指標就是:CPU和內存。
Kubernetes提供了個采用requests和limits 兩種類型參數對資源進行預分配和使用限制。
完整文件路徑:`myblog/one-pod/pod-with-resourcelimits.yaml`
```yaml
...
containers:
- name: myblog
image: 172.21.32.6:5000/myblog
env:
- name: MYSQL_HOST # 指定root用戶的用戶名
value: "127.0.0.1"
- name: MYSQL_PASSWD
value: "123456"
ports:
- containerPort: 8002
resources:
requests:
memory: 100Mi
cpu: 50m
limits:
memory: 500Mi
cpu: 100m
...
```
requests:
- 容器使用的最小資源需求,作用於schedule階段,作為容器調度時資源分配的判斷依賴
- 只有當前節點上可分配的資源量 >= request 時才允許將容器調度到該節點
- request參數不限制容器的最大可使用資源
- requests.cpu被轉成docker的--cpu-shares參數,與cgroup cpu.shares功能相同 (無論宿主機有多少個cpu或者內核,--cpu-shares選項都會按照比例分配cpu資源)
- requests.memory沒有對應的docker參數,僅作為k8s調度依據
limits:
- 容器能使用資源的最大值
- 設置為0表示對使用的資源不做限制, 可無限的使用
- 當pod 內存超過limit時,會被oom
- 當cpu超過limit時,不會被kill,但是會限制不超過limit值
- limits.cpu會被轉換成docker的–cpu-quota參數。與cgroup cpu.cfs_quota_us功能相同
- limits.memory會被轉換成docker的–memory參數。用來限制容器使用的最大內存
對於 CPU,我們知道計算機里 CPU 的資源是按`“時間片”`的方式來進行分配的,系統里的每一個操作都需要 CPU 的處理,所以,哪個任務要是申請的 CPU 時間片越多,那么它得到的 CPU 資源就越多。
然后還需要了解下 CGroup 里面對於 CPU 資源的單位換算:
```shell
1 CPU = 1000 millicpu(1 Core = 1000m)
```
這里的 `m` 就是毫、毫核的意思,Kubernetes 集群中的每一個節點可以通過操作系統的命令來確認本節點的 CPU 內核數量,然后將這個數量乘以1000,得到的就是節點總 CPU 總毫數。比如一個節點有四核,那么該節點的 CPU 總毫量為 4000m。
`docker run`命令和 CPU 限制相關的所有選項如下:
| 選項 | 描述 |
| --------------------- | ------------------------------------------------------- |
| `--cpuset-cpus=""` | 允許使用的 CPU 集,值可以為 0-3,0,1 |
| `-c`,`--cpu-shares=0` | CPU 共享權值(相對權重) |
| `cpu-period=0` | 限制 CPU CFS 的周期,范圍從 100ms~1s,即[1000, 1000000] |
| `--cpu-quota=0` | 限制 CPU CFS 配額,必須不小於1ms,即 >= 1000,絕對限制 |
```shell
docker run -it --cpu-period=50000 --cpu-quota=25000 ubuntu:16.04 /bin/bash
```
將 CFS 調度的周期設為 50000,將容器在每個周期內的 CPU 配額設置為 25000,表示該容器每 50ms 可以得到 50% 的 CPU 運行時間。
> 注意:若內存使用超出限制,會引發系統的OOM機制,因CPU是可壓縮資源,不會引發Pod退出或重建
###### yaml優化
目前完善后的yaml,`myblog/one-pod/pod-completed.yaml`
```yaml
apiVersion: v1
kind: Pod
metadata:
name: myblog
namespace: demo
labels:
component: myblog
spec:
volumes:
- name: mysql-data
hostPath:
path: /opt/mysql/data
nodeSelector: # 使用節點選擇器將Pod調度到指定label的節點
component: mysql
containers:
- name: myblog
image: 172.21.32.6:5000/myblog
env:
- name: MYSQL_HOST # 指定root用戶的用戶名
value: "127.0.0.1"
- name: MYSQL_PASSWD
value: "123456"
ports:
- containerPort: 8002
resources:
requests:
memory: 100Mi
cpu: 50m
limits:
memory: 500Mi
cpu: 100m
livenessProbe:
httpGet:
path: /blog/index/
port: 8002
scheme: HTTP
initialDelaySeconds: 10 # 容器啟動后第一次執行探測是需要等待多少秒
periodSeconds: 15 # 執行探測的頻率
timeoutSeconds: 2 # 探測超時時間
readinessProbe:
httpGet:
path: /blog/index/
port: 8002
scheme: HTTP
initialDelaySeconds: 10
timeoutSeconds: 2
periodSeconds: 15
- name: mysql
image: 172.21.32.6:5000/mysql:5.7-utf8
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: "123456"
- name: MYSQL_DATABASE
value: "myblog"
resources:
requests:
memory: 100Mi
cpu: 50m
limits:
memory: 500Mi
cpu: 100m
readinessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 15
periodSeconds: 20
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
```
為什么要優化
- 考慮真實的使用場景,像數據庫這類中間件,是作為公共資源,為多個項目提供服務,不適合和業務容器綁定在同一個Pod中,因為業務容器是經常變更的,而數據庫不需要頻繁迭代
- yaml的環境變量中存在敏感信息(賬號、密碼),存在安全隱患
解決問題一,需要拆分yaml
`myblog/two-pod/mysql.yaml`
```yaml
apiVersion: v1
kind: Pod
metadata:
name: mysql
namespace: demo
labels:
component: mysql
spec:
hostNetwork: true # 聲明pod的網絡模式為host模式,效果通docker run --net=host
volumes:
- name: mysql-data
hostPath:
path: /opt/mysql/data
nodeSelector: # 使用節點選擇器將Pod調度到指定label的節點
component: mysql
containers:
- name: mysql
image: 172.21.32.6:5000/mysql:5.7-utf8
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: "123456"
- name: MYSQL_DATABASE
value: "myblog"
resources:
requests:
memory: 100Mi
cpu: 50m
limits:
memory: 500Mi
cpu: 100m
readinessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 15
periodSeconds: 20
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
```
myblog.yaml
```yaml
apiVersion: v1
kind: Pod
metadata:
name: myblog
namespace: demo
labels:
component: myblog
spec:
containers:
- name: myblog
image: 172.21.32.6:5000/myblog
imagePullPolicy: IfNotPresent
env:
- name: MYSQL_HOST # 指定root用戶的用戶名
value: "172.21.32.6"
- name: MYSQL_PASSWD
value: "123456"
ports:
- containerPort: 8002
resources:
requests:
memory: 100Mi
cpu: 50m
limits:
memory: 500Mi
cpu: 100m
livenessProbe:
httpGet:
path: /blog/index/
port: 8002
scheme: HTTP
initialDelaySeconds: 10 # 容器啟動后第一次執行探測是需要等待多少秒
periodSeconds: 15 # 執行探測的頻率
timeoutSeconds: 2 # 探測超時時間
readinessProbe:
httpGet:
path: /blog/index/
port: 8002
scheme: HTTP
initialDelaySeconds: 10
timeoutSeconds: 2
periodSeconds: 15
```
創建測試
```powershell
## 先刪除舊pod
$ kubectl -n demo delete po myblog
## 分別創建mysql和myblog
$ kubectl create -f mysql.yaml
$ kubectl create -f myblog.yaml
## 查看pod,注意mysqlIP為宿主機IP,因為網絡模式為host
$ kubectl -n demo get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
myblog 1/1 Running 0 41s 10.244.1.152 k8s-slave1
mysql 1/1 Running 0 52s 192.168.136.131 k8s-slave1
## 訪問myblog服務正常
$ curl 10.244.1.152:8002/blog/index/
```
解決問題二,環境變量中敏感信息帶來的安全隱患
為什么要統一管理環境變量
- 環境變量中有很多敏感的信息,比如賬號密碼,直接暴漏在yaml文件中存在安全性問題
- 團隊內部一般存在多個項目,這些項目直接存在配置相同環境變量的情況,因此可以統一維護管理
- 對於開發、測試、生產環境,由於配置均不同,每套環境部署的時候都要修改yaml,帶來額外的開銷
k8s提供兩類資源,configMap和Secret,可以用來實現業務配置的統一管理, 允許將配置文件與鏡像文件分離,以使容器化的應用程序具有可移植性 。

- configMap,通常用來管理應用的配置文件或者環境變量,`myblog/two-pod/configmap.yaml`
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: myblog
namespace: demo
data:
MYSQL_HOST: "172.21.32.6"
MYSQL_PORT: "3306"
```
- Secret,管理敏感類的信息,默認會base64編碼存儲,有三種類型
- Service Account :用來訪問Kubernetes API,由Kubernetes自動創建,並且會自動掛載到Pod的/run/secrets/kubernetes.io/serviceaccount目錄中;創建ServiceAccount后,Pod中指定serviceAccount后,自動創建該ServiceAccount對應的secret;
- Opaque : base64編碼格式的Secret,用來存儲密碼、密鑰等;
- kubernetes.io/dockerconfigjson :用來存儲私有docker registry的認證信息。
`myblog/two-pod/secret.yaml`
```yaml
apiVersion: v1
kind: Secret
metadata:
name: myblog
namespace: demo
type: Opaque
data:
MYSQL_USER: cm9vdA== #注意加-n參數, echo -n root|base64
MYSQL_PASSWD: MTIzNDU2
```
創建並查看:
```powershell
$ kubectl create -f secret.yaml
$ kubectl -n demo get secret
```
如果不習慣這種方式,可以通過如下方式:
```powershell
$ cat secret.txt
MYSQL_USER=root
MYSQL_PASSWD=123456
$ kubectl -n demo create secret generic myblog --from-env-file=secret.txt
```
修改后的mysql的yaml,資源路徑:`myblog/two-pod/mysql-with-config.yaml`
```yaml
...
spec:
containers:
- name: mysql
image: 172.21.32.6:5000/mysql:5.7-utf8
env:
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: myblog
key: MYSQL_USER
- name: MYSQL_PASSWD
valueFrom:
secretKeyRef:
name: myblog
key: MYSQL_PASSWD
- name: MYSQL_DATABASE
value: "myblog"
...
```
修改后的myblog的yaml,資源路徑:`myblog/two-pod/myblog-with-config.yaml`
```yaml
spec:
containers:
- name: myblog
image: 172.21.32.6:5000/myblog
imagePullPolicy: IfNotPresent
env:
- name: MYSQL_HOST
valueFrom:
configMapKeyRef:
name: myblog
key: MYSQL_HOST
- name: MYSQL_PORT
valueFrom:
configMapKeyRef:
name: myblog
key: MYSQL_PORT
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: myblog
key: MYSQL_USER
- name: MYSQL_PASSWD
valueFrom:
secretKeyRef:
name: myblog
key: MYSQL_PASSWD
```
在部署不同的環境時,pod的yaml無須再變化,只需要在每套環境中維護一套ConfigMap和Secret即可。但是注意configmap和secret不能跨namespace使用,且更新后,pod內的env不會自動更新,重建后方可更新。
###### 如何編寫資源yaml
1. 拿來主義,從機器中已有的資源中拿
```powershell
$ kubectl -n kube-system get po,deployment,ds
```
2. 學會在官網查找, https://kubernetes.io/docs/home/
3. 從kubernetes-api文檔中查找, https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.16/#pod-v1-core
4. kubectl explain 查看具體字段含義
###### pod狀態與生命周期
Pod的狀態如下表所示:
| 狀態值 | 描述 |
| ----------------- | ------------------------------------------------------------ |
| Pending | API Server已經創建該Pod,等待調度器調度 |
| ContainerCreating | 鏡像正在創建 |
| Running | Pod內容器均已創建,且至少有一個容器處於運行狀態、正在啟動狀態或正在重啟狀態 |
| Succeeded | Pod內所有容器均已成功執行退出,且不再重啟 |
| Failed | Pod內所有容器均已退出,但至少有一個容器退出為失敗狀態 |
| CrashLoopBackOff | Pod內有容器啟動失敗,比如配置文件丟失導致主進程啟動失敗 |
| Unknown | 由於某種原因無法獲取該Pod的狀態,可能由於網絡通信不暢導致 |
生命周期示意圖:

啟動和關閉示意:

```yaml
apiVersion: v1
kind: Pod
metadata:
name: demo-start-stop
namespace: demo
labels:
component: demo-start-stop
spec:
initContainers:
- name: init
image: busybox
command: ['sh', '-c', 'echo $(date +%s): INIT >> /loap/timing']
volumeMounts:
- mountPath: /loap
name: timing
containers:
- name: main
image: busybox
command: ['sh', '-c', 'echo $(date +%s): START >> /loap/timing;
sleep 10; echo $(date +%s): END >> /loap/timing;']
volumeMounts:
- mountPath: /loap
name: timing
livenessProbe:
exec:
command: ['sh', '-c', 'echo $(date +%s): LIVENESS >> /loap/timing']
readinessProbe:
exec:
command: ['sh', '-c', 'echo $(date +%s): READINESS >> /loap/timing']
lifecycle:
postStart:
exec:
command: ['sh', '-c', 'echo $(date +%s): POST-START >> /loap/timing']
preStop:
exec:
command: ['sh', '-c', 'echo $(date +%s): PRE-STOP >> /loap/timing']
volumes:
- name: timing
hostPath:
path: /tmp/loap
```
創建pod測試:
```powershell
$ kubectl create -f demo-pod-start.yaml
## 查看demo狀態
$ kubectl -n demo get po -o wide -w
## 查看調度節點的/tmp/loap/timing
$ cat /tmp/loap/timing
1585424708: INIT
1585424746: START
1585424746: POST-START
1585424754: READINESS
1585424756: LIVENESS
1585424756: END
```
> 須主動殺掉 Pod 才會觸發 `pre-stop hook`,如果是 Pod 自己 Down 掉,則不會執行 `pre-stop hook`
###### 小結
1. 實現k8s平台與特定的容器運行時解耦,提供更加靈活的業務部署方式,引入了Pod概念
2. k8s使用yaml格式定義資源文件,yaml中Map與List的語法,與json做類比
3. 通過kubectl create | get | exec | logs | delete 等操作k8s資源,必須指定namespace
4. 每啟動一個Pod,為了實現網絡空間共享,會先創建Infra容器,並把其他容器網絡加入該容器
5. 通過livenessProbe和readinessProbe實現Pod的存活性和就緒健康檢查
6. 通過requests和limit分別限定容器初始資源申請與最高上限資源申請
7. 通過Pod IP訪問具體的Pod服務,實現是
##### Pod控制器 錄屏!!!
只使用Pod, 將會面臨如下需求:
1. 業務應用啟動多個副本
2. Pod重建后IP會變化,外部如何訪問Pod服務
3. 運行業務Pod的某個節點掛了,可以自動幫我把Pod轉移到集群中的可用節點啟動起來
4. 我的業務應用功能是收集節點監控數據,需要把Pod運行在k8集群的各個節點上
###### Workload (工作負載)
控制器又稱工作負載是用於實現管理pod的中間層,確保pod資源符合預期的狀態,pod的資源出現故障時,會嘗試 進行重啟,當根據重啟策略無效,則會重新新建pod的資源。

- ReplicaSet: 代用戶創建指定數量的pod副本數量,確保pod副本數量符合預期狀態,並且支持滾動式自動擴容和縮容功能
- Deployment:工作在ReplicaSet之上,用於管理無狀態應用,目前來說最好的控制器。支持滾動更新和回滾功能,還提供聲明式配置
- DaemonSet:用於確保集群中的每一個節點只運行特定的pod副本,通常用於實現系統級后台任務。比如ELK服務
- Job:只要完成就立即退出,不需要重啟或重建
- Cronjob:周期性任務控制,不需要持續后台運行
- StatefulSet:管理有狀態應用
###### Deployment
`myblog/deployment/deploy-mysql.yaml`
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
namespace: demo
spec:
replicas: 1 #指定Pod副本數
selector: #指定Pod的選擇器
matchLabels:
app: mysql
template:
metadata:
labels: #給Pod打label
app: mysql
spec:
hostNetwork: true # 聲明pod的網絡模式為host模式,效果通docker run --net=host
volumes:
- name: mysql-data
hostPath:
path: /opt/mysql/data
nodeSelector: # 使用節點選擇器將Pod調度到指定label的節點
component: mysql
containers:
- name: mysql
image: 172.21.32.15:5000/mysql:5.7-utf8
ports:
- containerPort: 3306
env:
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: myblog
key: MYSQL_USER
- name: MYSQL_PASSWD
valueFrom:
secretKeyRef:
name: myblog
key: MYSQL_PASSWD
- name: MYSQL_DATABASE
value: "myblog"
resources:
requests:
memory: 100Mi
cpu: 50m
limits:
memory: 500Mi
cpu: 100m
readinessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 15
periodSeconds: 20
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
```
deploy-myblog.yaml:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myblog
namespace: demo
spec:
replicas: 1 #指定Pod副本數
selector: #指定Pod的選擇器
matchLabels:
app: myblog
template:
metadata:
labels: #給Pod打label
app: myblog
spec:
containers:
- name: myblog
image: 172.21.32.15:5000/myblog
imagePullPolicy: IfNotPresent
env:
- name: MYSQL_HOST
valueFrom:
configMapKeyRef:
name: myblog
key: MYSQL_HOST
- name: MYSQL_PORT
valueFrom:
configMapKeyRef:
name: myblog
key: MYSQL_PORT
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: myblog
key: MYSQL_USER
- name: MYSQL_PASSWD
valueFrom:
secretKeyRef:
name: myblog
key: MYSQL_PASSWD
ports:
- containerPort: 8002
resources:
requests:
memory: 100Mi
cpu: 50m
limits:
memory: 500Mi
cpu: 100m
livenessProbe:
httpGet:
path: /blog/index/
port: 8002
scheme: HTTP
initialDelaySeconds: 10 # 容器啟動后第一次執行探測是需要等待多少秒
periodSeconds: 15 # 執行探測的頻率
timeoutSeconds: 2 # 探測超時時間
readinessProbe:
httpGet:
path: /blog/index/
port: 8002
scheme: HTTP
initialDelaySeconds: 10
timeoutSeconds: 2
periodSeconds: 15
```
###### 創建Deployment
```powershell
$ kubectl create -f deploy.yaml
```
###### 查看Deployment
```powershell
# kubectl api-resources
$ kubectl -n demo get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
myblog 1/1 1 1 2m22s
mysql 1/1 1 1 2d11h
* `NAME` 列出了集群中 Deployments 的名稱。
* `READY`顯示當前正在運行的副本數/期望的副本數。
* `UP-TO-DATE`顯示已更新以實現期望狀態的副本數。
* `AVAILABLE`顯示應用程序可供用戶使用的副本數。
* `AGE` 顯示應用程序運行的時間量。
# 查看pod
$ kubectl -n demo get po
NAME READY STATUS RESTARTS AGE
myblog-7c96c9f76b-qbbg7 1/1 Running 0 109s
mysql-85f4f65f99-w6jkj 1/1 Running 0 2m28s
```
###### 副本保障機制
controller實時檢測pod狀態,並保障副本數一直處於期望的值。
```powershell
## 刪除pod,觀察pod狀態變化
$ kubectl -n demo delete pod myblog-7c96c9f76b-qbbg7
# 觀察pod
$ kubectl get pods -o wide
## 設置兩個副本, 或者通過kubectl -n demo edit deploy myblog的方式,最好通過修改文件,然后apply的方式,這樣yaml文件可以保持同步
$ kubectl -n demo scale deploy myblog --replicas=2
deployment.extensions/myblog scaled
# 觀察pod
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE
myblog-7c96c9f76b-qbbg7 1/1 Running 0 11m
myblog-7c96c9f76b-s6brm 1/1 Running 0 55s
mysql-85f4f65f99-w6jkj 1/1 Running 0 11m
```
###### Pod驅逐策略
K8S 有個特色功能叫 pod eviction,它在某些場景下如節點 NotReady,或者資源不足時,把 pod 驅逐至其它節點,這也是出於業務保護的角度去考慮的。
1. Kube-controller-manager: 周期性檢查所有節點狀態,當節點處於 NotReady 狀態超過一段時間后,驅逐該節點上所有 pod。停掉kubelet
- `pod-eviction-timeout`:NotReady 狀態節點超過該時間后,執行驅逐,默認 5 min
2. Kubelet: 周期性檢查本節點資源,當資源不足時,按照優先級驅逐部分 pod
- `memory.available`:節點可用內存
- `nodefs.available`:節點根盤可用存儲空間
- `nodefs.inodesFree`:節點inodes可用數量
- `imagefs.available`:鏡像存儲盤的可用空間
- `imagefs.inodesFree`:鏡像存儲盤的inodes可用數量
###### 服務更新
修改dockerfile,重新打tag模擬服務更新。
更新方式:
- 修改yaml文件,使用`kubectl -n demo apply -f deploy-myblog.yaml`來應用更新
- `kubectl -n demo edit deploy myblog`在線更新
- `kubectl set image deploy myblog myblog=172.21.32.6:5000/myblog:v2 --record`
修改文件測試:
```powershell
$ vi mybolg/blog/template/index.html
$ docker build . -t 172.21.32.6:5000/myblog:v2 -f Dockerfile_optimized
$ docker push 172.21.32.6:5000/myblog:v2
```
###### 更新策略
```yaml
...
spec:
replicas: 2 #指定Pod副本數
selector: #指定Pod的選擇器
matchLabels:
app: myblog
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate #指定更新方式為滾動更新,默認策略,通過get deploy yaml查看
...
```

策略控制:
- maxSurge:最大激增數, 指更新過程中, 最多可以比replicas預先設定值多出的pod數量, 可以為固定值或百分比,默認為desired Pods數的25%。計算時向上取整(比如3.4,取4),更新過程中最多會有replicas + maxSurge個pod
- maxUnavailable: 指更新過程中, 最多有幾個pod處於無法服務狀態 , 可以為固定值或百分比,默認為desired Pods數的25%。計算時向下取整(比如3.6,取3)
*在Deployment rollout時,需要保證Available(Ready) Pods數不低於 desired pods number - maxUnavailable; 保證所有的非異常狀態Pods數不多於 desired pods number + maxSurge*。
以myblog為例,使用默認的策略,更新過程:
1. maxSurge 25%,2個實例,向上取整,則maxSurge為1,意味着最多可以有2+1=3個Pod,那么此時會新創建1個ReplicaSet,RS-new,把副本數置為1,此時呢,副本控制器就去創建這個新的Pod
2. 同時,maxUnavailable是25%,副本數2*25%,向下取整,則為0,意味着,滾動更新的過程中,不能有少於2個可用的Pod,因此,舊的Replica(RS-old)會先保持不動,等RS-new管理的Pod狀態Ready后,此時已經有3個Ready狀態的Pod了,那么由於只要保證有2個可用的Pod即可,因此,RS-old的副本數會有2個變成1個,此時,會刪掉一個舊的Pod
3. 刪掉舊的Pod的時候,由於總的Pod數量又變成2個了,因此,距離最大的3個還有1個Pod可以創建,所以,RS-new把管理的副本數由1改成2,此時又會創建1個新的Pod,等RS-new管理了2個Pod都ready后,那么就可以把RS-old的副本數由1置為0了,這樣就完成了滾動更新
```powershell
#查看滾動更新事件
$ kubectl -n demo describe deploy myblog
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 11s deployment-controller Scaled up replica set myblog-6cf56fc848 to 1
Normal ScalingReplicaSet 11s deployment-controller Scaled down replica set myblog-6fdcf98f9 to 1
Normal ScalingReplicaSet 11s deployment-controller Scaled up replica set myblog-6cf56fc848 to 2
Normal ScalingReplicaSet 6s deployment-controller Scaled down replica set myblog-6fdcf98f9 to 0
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
myblog-6cf56fc848 2 2 2 16h
myblog-6fdcf98f9 0 0 0 16h
```
###### 服務回滾
通過滾動升級的策略可以平滑的升級Deployment,若升級出現問題,需要最快且最好的方式回退到上一次能夠提供正常工作的版本。為此K8S提供了回滾機制。
**revision**:更新應用時,K8S都會記錄當前的版本號,即為revision,當升級出現問題時,可通過回滾到某個特定的revision,默認配置下,K8S只會保留最近的幾個revision,可以通過Deployment配置文件中的spec.revisionHistoryLimit屬性增加revision數量,默認是10。
查看當前:
```powershell
$ kubectl -n demo rollout history deploy myblog ##CHANGE-CAUSE為空
$ kubectl delete -f deploy-myblog.yaml ## 方便演示到具體效果,刪掉已有deployment
```
記錄回滾:
```powershell
$ kubectl create -f deploy-myblog.yaml --record
$ kubectl -n demo set image deploy myblog myblog=172.21.32.6:5000/myblog:v2 --record=true
```
查看deployment更新歷史:
```powershell
$ kubectl -n demo rollout history deploy myblog
deployment.extensions/myblog
REVISION CHANGE-CAUSE
1 kubectl create --filename=deploy-myblog.yaml --record=true
2 kubectl set image deploy myblog myblog=172.21.32.6:5000/demo/myblog:v1 --record=true
```
回滾到具體的REVISION:
```powershell
$ kubectl -n demo rollout undo deploy myblog --to-revision=1
deployment.extensions/myblog rolled back
# 訪問應用測試
```
##### Kubernetes調度 錄屏!!!
###### 為何要控制Pod應該如何調度
- 集群中有些機器的配置高(SSD,更好的內存等),我們希望核心的服務(比如說數據庫)運行在上面
- 某兩個服務的網絡傳輸很頻繁,我們希望它們最好在同一台機器上
- ......
###### NodeSelector
`label`是`kubernetes`中一個非常重要的概念,用戶可以非常靈活的利用 label 來管理集群中的資源,POD 的調度可以根據節點的 label 進行特定的部署。
查看節點的label:
```powershell
$ kubectl get nodes --show-labels
```
為節點打label:
```powershell
$ kubectl label node k8s-master disktype=ssd
```
當 node 被打上了相關標簽后,在調度的時候就可以使用這些標簽了,只需要在spec 字段中添加`nodeSelector`字段,里面是我們需要被調度的節點的 label。
```yaml
...
spec:
hostNetwork: true # 聲明pod的網絡模式為host模式,效果通docker run --net=host
volumes:
- name: mysql-data
hostPath:
path: /opt/mysql/data
nodeSelector: # 使用節點選擇器將Pod調度到指定label的節點
component: mysql
containers:
- name: mysql
image: 172.21.32.6:5000/demo/mysql:5.7
...
```
###### nodeAffinity
節點親和性 , 比上面的`nodeSelector`更加靈活,它可以進行一些簡單的邏輯組合,不只是簡單的相等匹配 。分為兩種,軟策略和硬策略。
preferredDuringSchedulingIgnoredDuringExecution:軟策略,如果你沒有滿足調度要求的節點的話,Pod就會忽略這條規則,繼續完成調度過程,說白了就是滿足條件最好了,沒有滿足就忽略掉的策略。
requiredDuringSchedulingIgnoredDuringExecution : 硬策略,如果沒有滿足條件的節點的話,就不斷重試直到滿足條件為止,簡單說就是你必須滿足我的要求,不然我就不會調度Pod。
```yaml
#要求 Pod 不能運行在128和132兩個節點上,如果有個節點滿足disktype=ssd的話就優先調度到這個節點上
...
spec:
containers:
- name: demo
image: 172.21.32.6:5000/demo/myblog
ports:
- containerPort: 8002
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values:
- 192.168.136.128
- 192.168.136.132
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: disktype
operator: In
values:
- ssd
- sas
...
```
這里的匹配邏輯是 label 的值在某個列表中,現在`Kubernetes`提供的操作符有下面的幾種:
- In:label 的值在某個列表中
- NotIn:label 的值不在某個列表中
- Gt:label 的值大於某個值
- Lt:label 的值小於某個值
- Exists:某個 label 存在
- DoesNotExist:某個 label 不存在
*如果nodeSelectorTerms下面有多個選項的話,滿足任何一個條件就可以了;如果matchExpressions有多個選項的話,則必須同時滿足這些條件才能正常調度 Pod*
###### 污點(Taints)與容忍(tolerations)
對於`nodeAffinity`無論是硬策略還是軟策略方式,都是調度 Pod 到預期節點上,而`Taints`恰好與之相反,如果一個節點標記為 Taints ,除非 Pod 也被標識為可以容忍污點節點,否則該 Taints 節點不會被調度Pod。
Taints(污點)是Node的一個屬性,設置了Taints(污點)后,因為有了污點,所以Kubernetes是不會將Pod調度到這個Node上的。於是Kubernetes就給Pod設置了個屬性Tolerations(容忍),只要Pod能夠容忍Node上的污點,那么Kubernetes就會忽略Node上的污點,就能夠(不是必須)把Pod調度過去。
比如用戶希望把 Master 節點保留給 Kubernetes 系統組件使用,或者把一組具有特殊資源預留給某些 Pod,則污點就很有用了,Pod 不會再被調度到 taint 標記過的節點。taint 標記節點舉例如下:
設置污點:
```powershell
$ kubectl taint node [node_name] key=value:[effect]
其中[effect] 可取值: [ NoSchedule | PreferNoSchedule | NoExecute ]
NoSchedule:一定不能被調度。
PreferNoSchedule:盡量不要調度。
NoExecute:不僅不會調度,還會驅逐Node上已有的Pod。
示例:kubectl taint node k8s-master smoke=true:NoSchedule
```
去除污點:
```powershell
去除指定key及其effect:
kubectl taint nodes [node_name] key:[effect]- #這里的key不用指定value
去除指定key所有的effect:
kubectl taint nodes node_name key-
示例:
kubectl taint node k8s-master smoke=true:NoSchedule
kubectl taint node k8s-master smoke:NoExecute-
kubectl taint node k8s-master smoke-
```
污點演示:
```powershell
## 給k8s-slave1打上污點,smoke=true:NoSchedule
$ kubectl taint node k8s-slave1 smoke=true:NoSchedule
$ kubectl taint node k8s-slave2 drunk=true:NoSchedule
## 擴容myblog的Pod,觀察新Pod的調度情況
$ kuebctl -n demo scale deploy myblog --replicas=3
$ kubectl -n demo get po -w ## pending
```
Pod容忍污點示例:`myblog/deployment/deploy-myblog-taint.yaml`
```powershell
...
spec:
containers:
- name: demo
image: 172.21.32.6:5000/demo/myblog
tolerations: #設置容忍性
- key: "smoke"
operator: "Equal" #如果操作符為Exists,那么value屬性可省略,不指定operator,默認為Equal
value: "true"
effect: "NoSchedule"
- key: "drunk"
operator: "Equal" #如果操作符為Exists,那么value屬性可省略,不指定operator,默認為Equal
value: "true"
effect: "NoSchedule"
#意思是這個Pod要容忍的有污點的Node的key是smoke Equal true,效果是NoSchedule,
#tolerations屬性下各值必須使用引號,容忍的值都是設置Node的taints時給的值。
```
```powershell
$ kubectl apply -f deploy-myblog-taint.yaml
```
```powershell
spec:
containers:
- name: demo
image: 172.21.32.6:5000/demo/myblog
tolerations:
- operator: "Exists"
```
##### Kubernetes服務訪問之Service 錄屏!!!
通過以前的學習,我們已經能夠通過Deployment來創建一組Pod來提供具有高可用性的服務。雖然每個Pod都會分配一個單獨的Pod IP,然而卻存在如下兩個問題:
- Pod IP僅僅是集群內可見的虛擬IP,外部無法訪問。
- Pod IP會隨着Pod的銷毀而消失,當ReplicaSet對Pod進行動態伸縮時,Pod IP可能隨時隨地都會變化,這樣對於我們訪問這個服務帶來了難度。
###### Service 負載均衡之Cluster IP
service是一組pod的服務抽象,相當於一組pod的LB,負責將請求分發給對應的pod。service會為這個LB提供一個IP,一般稱為cluster IP 。使用Service對象,通過selector進行標簽選擇,找到對應的Pod:
`myblog/deployment/svc-myblog.yaml`
```yaml
apiVersion: v1
kind: Service
metadata:
name: myblog
namespace: demo
spec:
ports:
- port: 80
protocol: TCP
targetPort: 8002
selector:
app: myblog
type: ClusterIP
```
操作演示:
```powershell
## 別名
$ alias kd='kubectl -n demo'
## 創建服務
$ kd create -f svc-myblog.yaml
$ kd get po --show-labels
NAME READY STATUS RESTARTS AGE LABELS
myblog-5c97d79cdb-jn7km 1/1 Running 0 6m5s app=myblog
mysql-85f4f65f99-w6jkj 1/1 Running 0 176m app=mysql
$ kd get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
myblog ClusterIP 10.99.174.93 <none> 80/TCP 7m50s
$ kd describe svc myblog
Name: myblog
Namespace: demo
Labels: <none>
Annotations: <none>
Selector: app=myblog
Type: ClusterIP
IP: 10.99.174.93
Port: <unset> 80/TCP
TargetPort: 8002/TCP
Endpoints: 10.244.0.68:8002
Session Affinity: None
Events: <none>
## 擴容myblog服務
$ kd scale deploy myblog --replicas=2
deployment.extensions/myblog scaled
## 再次查看
$ kd describe svc myblog
Name: myblog
Namespace: demo
Labels: <none>
Annotations: <none>
Selector: app=myblog
Type: ClusterIP
IP: 10.99.174.93
Port: <unset> 80/TCP
TargetPort: 8002/TCP
Endpoints: 10.244.0.68:8002,10.244.1.158:8002
Session Affinity: None
Events: <none>
```
Service與Pod如何關聯:
service對象創建的同時,會創建同名的endpoints對象,若服務設置了readinessProbe, 當readinessProbe檢測失敗時,endpoints列表中會剔除掉對應的pod_ip,這樣流量就不會分發到健康檢測失敗的Pod中
```powershell
$ kd get endpoints myblog
NAME ENDPOINTS AGE
myblog 10.244.0.68:8002,10.244.1.158:8002 7m
```
Service Cluster-IP如何訪問:
```powershell
$ kd get svc myblog
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
myblog ClusterIP 10.99.174.93 <none> 80/TCP 13m
$ curl 10.99.174.93/blog/index/
```
為mysql服務創建service:
```yaml
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: demo
spec:
ports:
- port: 3306
protocol: TCP
targetPort: 3306
selector:
app: mysql
type: ClusterIP
```
訪問mysql:
```powershell
$ kd get svc mysql
mysql ClusterIP 10.108.214.84 <none> 3306/TCP 3s
$ curl 10.108.214.84:3306
```
目前使用hostNetwork部署,通過宿主機ip+port訪問,弊端:
- 服務使用hostNetwork,使得宿主機的端口大量暴漏,存在安全隱患
- 容易引發端口沖突
服務均屬於k8s集群,盡可能使用k8s的網絡訪問,因此可以對目前myblog訪問mysql的方式做改造:
- 為mysql創建一個固定clusterIp的Service,把clusterIp配置在myblog的環境變量中
- 利用集群服務發現的能力,組件之間通過service name來訪問
###### 服務發現
在k8s集群中,組件之間可以通過定義的Service名稱實現通信。
演示服務發現:
```powershell
## 演示思路:在myblog的容器中直接通過service名稱訪問服務,觀察是否可以訪問通
# 先查看服務
$ kd get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
myblog ClusterIP 10.99.174.93 <none> 80/TCP 59m
mysql ClusterIP 10.108.214.84 <none> 3306/TCP 35m
# 進入myblog容器
$ kd exec -ti myblog-5c97d79cdb-j485f bash
[root@myblog-5c97d79cdb-j485f myblog]# curl mysql:3306
5.7.29 )→ (mysql_native_password ot packets out of order
[root@myblog-5c97d79cdb-j485f myblog]# curl myblog/blog/index/
我的博客列表
```
雖然podip和clusterip都不固定,但是service name是固定的,而且具有完全的跨集群可移植性,因此組件之間調用的同時,完全可以通過service name去通信,這樣避免了大量的ip維護成本,使得服務的yaml模板更加簡單。因此可以對mysql和myblog的部署進行優化改造:
1. mysql可以去掉hostNetwork部署,使得服務只暴漏在k8s集群內部網絡
2. configMap中數據庫地址可以換成Service名稱,這樣跨環境的時候,配置內容基本上可以保持不用變化
修改deploy-mysql.yaml
```yaml
spec:
hostNetwork: true # 去掉此行
volumes:
- name: mysql-data
hostPath:
path: /opt/mysql/data
```
修改configmap.yaml
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: myblog
namespace: demo
data:
MYSQL_HOST: "mysql" # 此處替換為mysql
MYSQL_PORT: "3306"
```
應用修改:
```powershell
$ kubectl apply -f configmap.yaml
$ kubectl apply -f deploy-mysql.yaml
## 重建pod
$ kubectl -n demo delete po mysql-7f747644b8-6npzn
#去掉taint
$ kubectl taint node k8s-slave1 smoke-
$ kubectl taint node k8s-slave2 drunk-
## myblog不用動,會自動因健康檢測不過而重啟
```
服務發現實現:
`CoreDNS`是一個`Go`語言實現的鏈式插件`DNS服務端`,是CNCF成員,是一個高性能、易擴展的`DNS服務端`。
```powershell
$ kubectl -n kube-system get po -o wide|grep dns
coredns-d4475785-2w4hk 1/1 Running 0 4d22h 10.244.0.64
coredns-d4475785-s49hq 1/1 Running 0 4d22h 10.244.0.65
# 查看myblog的pod解析配置
$ kubectl -n demo exec -ti myblog-5c97d79cdb-j485f bash
[root@myblog-5c97d79cdb-j485f myblog]# cat /etc/resolv.conf
nameserver 10.96.0.10
search demo.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
## 10.96.0.10 從哪來
$ kubectl -n kube-system get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP 51d
## 啟動pod的時候,會把kube-dns服務的cluster-ip地址注入到pod的resolve解析配置中,同時添加對應的namespace的search域。 因此跨namespace通過service name訪問的話,需要添加對應的namespace名稱,
service_name.namespace_name
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 26h
```
###### Service負載均衡之NodePort
cluster-ip為虛擬地址,只能在k8s集群內部進行訪問,集群外部如果訪問內部服務,實現方式之一為使用NodePort方式。NodePort會默認在 30000-32767 ,不指定的會隨機使用其中一個。
`myblog/deployment/svc-myblog-nodeport.yaml`
```powershell
apiVersion: v1
kind: Service
metadata:
name: myblog-np
namespace: demo
spec:
ports:
- port: 80
protocol: TCP
targetPort: 8002
selector:
app: myblog
type: NodePort
```
查看並訪問服務:
```powershell
$ kd create -f svc-myblog-nodeport.yaml
service/myblog-np created
$ kd get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
myblog ClusterIP 10.99.174.93 <none> 80/TCP 102m
myblog-np NodePort 10.105.228.101 <none> 80:30647/TCP 4s
mysql ClusterIP 10.108.214.84 <none> 3306/TCP 77m
#集群內每個節點的NodePort端口都會進行監聽
$ curl 192.168.136.128:30647/blog/index/
我的博客列表
$ curl 192.168.136.131:30647/blog/index/
我的博客列表
## 瀏覽器訪問
```
思考:
1. NodePort的端口監聽如何轉發到對應的Pod服務?
2. CLUSTER-IP為虛擬IP,集群內如何通過虛擬IP訪問到具體的Pod服務?
###### kube-proxy
運行在每個節點上,監聽 API Server 中服務對象的變化,再通過創建流量路由規則來實現網絡的轉發。[參照]( https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies )
有三種模式:
- User space, 讓 Kube-Proxy 在用戶空間監聽一個端口,所有的 Service 都轉發到這個端口,然后 Kube-Proxy 在內部應用層對其進行轉發 , 所有報文都走一遍用戶態,性能不高,k8s v1.2版本后廢棄。
- Iptables, 當前默認模式,完全由 IPtables 來實現, 通過各個node節點上的iptables規則來實現service的負載均衡,但是隨着service數量的增大,iptables模式由於線性查找匹配、全量更新等特點,其性能會顯著下降。
- IPVS, 與iptables同樣基於Netfilter,但是采用的hash表,因此當service數量達到一定規模時,hash查表的速度優勢就會顯現出來,從而提高service的服務性能。 k8s 1.8版本開始引入,1.11版本開始穩定,需要開啟宿主機的ipvs模塊。
IPtables模式示意圖:

```powershell
$ iptables-save |grep -v myblog-np|grep "demo/myblog"
-A KUBE-SERVICES ! -s 10.244.0.0/16 -d 10.99.174.93/32 -p tcp -m comment --comment "demo/myblog: cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.99.174.93/32 -p tcp -m comment --comment "demo/myblog: cluster IP" -m tcp --dport 80 -j KUBE-SVC-WQNGJ7YFZKCTKPZK
$ iptables-save |grep KUBE-SVC-WQNGJ7YFZKCTKPZK
-A KUBE-SVC-WQNGJ7YFZKCTKPZK -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-GB5GNOM5CZH7ICXZ
-A KUBE-SVC-WQNGJ7YFZKCTKPZK -j KUBE-SEP-7GWC3FN2JI5KLE47
$ iptables-save |grep KUBE-SEP-GB5GNOM5CZH7ICXZ
-A KUBE-SEP-GB5GNOM5CZH7ICXZ -p tcp -m tcp -j DNAT --to-destination 10.244.1.158:8002
$ iptables-save |grep KUBE-SEP-7GWC3FN2JI5KLE47
-A KUBE-SEP-7GWC3FN2JI5KLE47 -p tcp -m tcp -j DNAT --to-destination 10.244.1.159:8002
```
##### Kubernetes服務訪問之Ingress
對於Kubernetes的Service,無論是Cluster-Ip和NodePort均是四層的負載,集群內的服務如何實現七層的負載均衡,這就需要借助於Ingress,Ingress控制器的實現方式有很多,比如nginx, Contour, Haproxy, trafik, Istio,我們以nginx的實現為例做演示。
Ingress-nginx是7層的負載均衡器 ,負責統一管理外部對k8s cluster中service的請求。主要包含:
- ingress-nginx-controller:根據用戶編寫的ingress規則(創建的ingress的yaml文件),動態的去更改nginx服務的配置文件,並且reload重載使其生效(是自動化的,通過lua腳本來實現);
- ingress資源對象:將Nginx的配置抽象成一個Ingress對象,每添加一個新的Service資源對象只需寫一個新的Ingress規則的yaml文件即可(或修改已存在的ingress規則的yaml文件)
###### 示意圖:

###### 實現邏輯
1)ingress controller通過和kubernetes api交互,動態的去感知集群中ingress規則變化
2)然后讀取ingress規則(規則就是寫明了哪個域名對應哪個service),按照自定義的規則,生成一段nginx配置
3)再寫到nginx-ingress-controller的pod里,這個Ingress controller的pod里運行着一個Nginx服務,控制器把生成的nginx配置寫入/etc/nginx.conf文件中
4)然后reload一下使配置生效。以此達到域名分別配置和動態更新的問題。
###### 安裝
[官方文檔](https://github.com/kubernetes/ingress-nginx/blob/master/docs/deploy/index.md)
```powershell
$ wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.30.0/deploy/static/mandatory.yaml
## 或者使用myblog/deployment/ingress/mandatory.yaml
## 修改部署節點
$ grep -n5 nodeSelector mandatory.yaml
212- spec:
213- hostNetwork: true #添加為host模式
214- # wait up to five minutes for the drain of connections
215- terminationGracePeriodSeconds: 300
216- serviceAccountName: nginx-ingress-serviceaccount
217: nodeSelector:
218- ingress: "true" #替換此處,來決定將ingress部署在哪些機器
219- containers:
220- - name: nginx-ingress-controller
221- image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.30.0
222- args:
```
使用示例:`myblog/deployment/ingress.yaml`
```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: myblog
namespace: demo
spec:
rules:
- host: myblog.devops.cn
http:
paths:
- path: /
backend:
serviceName: myblog
servicePort: 80
```
ingress-nginx動態生成upstream配置:
```yaml
...
server_name myblog.devops.cn ;
listen 80 ;
listen [::]:80 ;
listen 443 ssl http2 ;
listen [::]:443 ssl http2 ;
set $proxy_upstream_name "-";
ssl_certificate_by_lua_block {
certificate.call()
}
location / {
set $namespace "demo";
set $ingress_name "myblog";
...
```
###### 訪問
域名解析服務,將 `myblog.devops.cn`解析到ingress的地址上。ingress是支持多副本的,高可用的情況下,生產的配置是使用lb服務(內網F5設備,公網elb、slb、clb,解析到各ingress的機器,如何域名指向lb地址)
本機,添加如下hosts記錄來演示效果。
```json
192.168.136.128 myblog.devops.cn
```
然后,訪問 http://myblog.devops.cn/blog/index/
HTTPS訪問:
```powershell
#自簽名證書
$ openssl req -x509 -nodes -days 2920 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=*.devops.cn/O=ingress-nginx"
# 證書信息保存到secret對象中,ingress-nginx會讀取secret對象解析出證書加載到nginx配置中
$ kubectl -n demo create secret tls https-secret --key tls.key --cert tls.crt
secret/https-secret created
```
修改yaml
```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: myblog-tls
namespace: demo
spec:
rules:
- host: myblog.devops.cn
http:
paths:
- path: /
backend:
serviceName: myblog
servicePort: 80
tls:
- hosts:
- myblog.devops.cn
secretName: https-secret
```
然后,訪問 https://myblog.devops.cn/blog/index/
##### Kubernetes認證與授權 錄屏!!!
###### APIService安全控制

- Authentication:身份認證
1. 這個環節它面對的輸入是整個`http request`,負責對來自client的請求進行身份校驗,支持的方法包括:
- client證書驗證(https雙向驗證)
- `basic auth`
- 普通token
- `jwt token`(用於serviceaccount)
2. APIServer啟動時,可以指定一種Authentication方法,也可以指定多種方法。如果指定了多種方法,那么APIServer將會逐個使用這些方法對客戶端請求進行驗證, 只要請求數據通過其中一種方法的驗證,APIServer就會認為Authentication成功;
3. 使用kubeadm引導啟動的k8s集群的apiserver初始配置中,默認支持`client證書`驗證和`serviceaccount`兩種身份驗證方式。 證書認證通過設置`--client-ca-file`根證書以及`--tls-cert-file`和`--tls-private-key-file`來開啟。
4. 在這個環節,apiserver會通過client證書或 `http header`中的字段(比如serviceaccount的`jwt token`)來識別出請求的`用戶身份`,包括”user”、”group”等,這些信息將在后面的`authorization`環節用到。
- Authorization:鑒權,你可以訪問哪些資源
1. 這個環節面對的輸入是`http request context`中的各種屬性,包括:`user`、`group`、`request path`(比如:`/api/v1`、`/healthz`、`/version`等)、 `request verb`(比如:`get`、`list`、`create`等)。
2. APIServer會將這些屬性值與事先配置好的訪問策略(`access policy`)相比較。APIServer支持多種`authorization mode`,包括`Node、RBAC、Webhook`等。
3. APIServer啟動時,可以指定一種`authorization mode`,也可以指定多種`authorization mode`,如果是后者,只要Request通過了其中一種mode的授權, 那么該環節的最終結果就是授權成功。在較新版本kubeadm引導啟動的k8s集群的apiserver初始配置中,`authorization-mode`的默認配置是`”Node,RBAC”`。
- Admission Control:[准入控制](http://docs.kubernetes.org.cn/144.html),一個控制鏈(層層關卡),偏集群安全控制、管理方面。為什么說是安全相關的機制?
- 以NamespaceLifecycle為例, 該插件確保處於Termination狀態的Namespace不再接收新的對象創建請求,並拒絕請求不存在的Namespace。該插件還可以防止刪除系統保留的Namespace:default,kube-system,kube-public。
- NodeRestriction, 此插件限制kubelet修改Node和Pod對象,這樣的kubelets只允許修改綁定到Node的Pod API對象,以后版本可能會增加額外的限制 。
為什么我們執行命令kubectl命令,可以直接管理k8s集群資源?
###### kubectl的認證授權
kubectl的日志調試級別:
| 信息 | 描述 |
| :--- | :----------------------------------------------------------- |
| v=0 | 通常,這對操作者來說總是可見的。 |
| v=1 | 當您不想要很詳細的輸出時,這個是一個合理的默認日志級別。 |
| v=2 | 有關服務和重要日志消息的有用穩定狀態信息,這些信息可能與系統中的重大更改相關。這是大多數系統推薦的默認日志級別。 |
| v=3 | 關於更改的擴展信息。 |
| v=4 | 調試級別信息。 |
| v=6 | 顯示請求資源。 |
| v=7 | 顯示 HTTP 請求頭。 |
| v=8 | 顯示 HTTP 請求內容。 |
| v=9 | 顯示 HTTP 請求內容,並且不截斷內容。 |
```powershell
$ kubectl get nodes -v=7
I0329 20:20:08.633065 3979 loader.go:359] Config loaded from file /root/.kube/config
I0329 20:20:08.633797 3979 round_trippers.go:416] GET https://192.168.136.128:6443/api/v1/nodes?limit=500
```
`kubeadm init`啟動完master節點后,會默認輸出類似下面的提示內容:
```bash
... ...
Your Kubernetes master has initialized successfully!
To start using your cluster, you need to run the following as a regular user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
... ...
```
這些信息是在告知我們如何配置`kubeconfig`文件。按照上述命令配置后,master節點上的`kubectl`就可以直接使用`$HOME/.kube/config`的信息訪問`k8s cluster`了。 並且,通過這種配置方式,`kubectl`也擁有了整個集群的管理員(root)權限。
很多K8s初學者在這里都會有疑問:
- 當`kubectl`使用這種`kubeconfig`方式訪問集群時,`Kubernetes`的`kube-apiserver`是如何對來自`kubectl`的訪問進行身份驗證(`authentication`)和授權(`authorization`)的呢?
- 為什么來自`kubectl`的請求擁有最高的管理員權限呢?
查看`/root/.kube/config`文件:
前面提到過apiserver的authentication支持通過`tls client certificate、basic auth、token`等方式對客戶端發起的請求進行身份校驗, 從kubeconfig信息來看,kubectl顯然在請求中使用了`tls client certificate`的方式,即客戶端的證書。
證書base64解碼:
```powershell
$ echo xxxxxxxxxxxxxx |base64 -d > kubectl.crt
```
說明在認證階段,`apiserver`會首先使用`--client-ca-file`配置的CA證書去驗證kubectl提供的證書的有效性,基本的方式 :
```powershell
$ openssl verify -CAfile /etc/kubernetes/pki/ca.crt kubectl.crt
kubectl.crt: OK
```
除了認證身份,還會取出必要的信息供授權階段使用,文本形式查看證書內容:
```powershell
$ openssl x509 -in kubectl.crt -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 4736260165981664452 (0x41ba9386f52b74c4)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=kubernetes
Validity
Not Before: Feb 10 07:33:39 2020 GMT
Not After : Feb 9 07:33:40 2021 GMT
Subject: O=system:masters, CN=kubernetes-admin
...
```
認證通過后,提取出簽發證書時指定的CN(Common Name),`kubernetes-admin`,作為請求的用戶名 (User Name), 從證書中提取O(Organization)字段作為請求用戶所屬的組 (Group),`group = system:masters`,然后傳遞給后面的授權模塊。
kubeadm在init初始引導集群啟動過程中,創建了許多`default`的`role、clusterrole、rolebinding`和`clusterrolebinding`, 在k8s有關RBAC的官方文檔中,我們看到下面一些`default clusterrole`列表:

其中第一個cluster-admin這個cluster role binding綁定了system:masters group,這和authentication環節傳遞過來的身份信息不謀而合。 沿着system:masters group對應的cluster-admin clusterrolebinding“追查”下去,真相就會浮出水面。
我們查看一下這一binding:
```yaml
$ kubectl describe clusterrolebinding cluster-admin
Name: cluster-admin
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
Role:
Kind: ClusterRole
Name: cluster-admin
Subjects:
Kind Name Namespace
---- ---- ---------
Group system:masters
```
我們看到在kube-system名字空間中,一個名為cluster-admin的clusterrolebinding將cluster-admin cluster role與system:masters Group綁定到了一起, 賦予了所有歸屬於system:masters Group中用戶cluster-admin角色所擁有的權限。
我們再來查看一下cluster-admin這個role的具體權限信息:
```powershell
$ kubectl describe clusterrole cluster-admin
Name: cluster-admin
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
*.* [] [] [*]
[*] [] [*]
```
非資源類,如查看集群健康狀態。

###### RBAC
Role-Based Access Control,基於角色的訪問控制, apiserver啟動參數添加--authorization-mode=RBAC 來啟用RBAC認證模式,kubeadm安裝的集群默認已開啟。[官方介紹](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
查看開啟:
```powershell
# master節點查看apiserver進程
$ ps aux |grep apiserver
```
RBAC模式引入了4個資源:
- Role,角色
一個Role只能授權訪問單個namespace
```yaml
## 示例定義一個名為pod-reader的角色,該角色具有讀取default這個命名空間下的pods的權限
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""] # "" indicates the core API group
resources: ["pods"]
verbs: ["get", "watch", "list"]
## apiGroups: "","apps", "autoscaling", "batch", kubectl api-versions
## resources: "services", "pods","deployments"... kubectl api-resources
## verbs: "get", "list", "watch", "create", "update", "patch", "delete", "exec"
```
- ClusterRole
一個ClusterRole能夠授予和Role一樣的權限,但是它是集群范圍內的。
```yaml
## 定義一個集群角色,名為secret-reader,該角色可以讀取所有的namespace中的secret資源
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
# "namespace" omitted since ClusterRoles are not namespaced
name: secret-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "watch", "list"]
```
- Rolebinding
將role中定義的權限分配給用戶和用戶組。RoleBinding包含主題(users,groups,或service accounts)和授予角色的引用。對於namespace內的授權使用RoleBinding,集群范圍內使用ClusterRoleBinding。
```yaml
## 定義一個角色綁定,將pod-reader這個role的權限授予給jane這個User,使得jane可以在讀取default這個命名空間下的所有的pod數據
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: read-pods
namespace: default
subjects:
- kind: User #這里可以是User,Group,ServiceAccount
name: jane
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role #這里可以是Role或者ClusterRole,若是ClusterRole,則權限也僅限於rolebinding的內部
name: pod-reader # match the name of the Role or ClusterRole you wish to bind to
apiGroup: rbac.authorization.k8s.io
```
*注意:rolebinding既可以綁定role,也可以綁定clusterrole,當綁定clusterrole的時候,subject的權限也會被限定於rolebinding定義的namespace內部,若想跨namespace,需要使用clusterrolebinding*
```yaml
## 定義一個角色綁定,將dave這個用戶和secret-reader這個集群角色綁定,雖然secret-reader是集群角色,但是因為是使用rolebinding綁定的,因此dave的權限也會被限制在development這個命名空間內
apiVersion: rbac.authorization.k8s.io/v1
# This role binding allows "dave" to read secrets in the "development" namespace.
# You need to already have a ClusterRole named "secret-reader".
kind: RoleBinding
metadata:
name: read-secrets
#
# The namespace of the RoleBinding determines where the permissions are granted.
# This only grants permissions within the "development" namespace.
namespace: development
subjects:
- kind: User
name: dave # Name is case sensitive
apiGroup: rbac.authorization.k8s.io
- kind: ServiceAccount
name: dave # Name is case sensitive
namespace: demo
roleRef:
kind: ClusterRole
name: secret-reader
apiGroup: rbac.authorization.k8s.io
```
考慮一個場景: 如果集群中有多個namespace分配給不同的管理員,每個namespace的權限是一樣的,就可以只定義一個clusterrole,然后通過rolebinding將不同的namespace綁定到管理員身上,否則就需要每個namespace定義一個Role,然后做一次rolebinding。
- ClusterRolebingding
允許跨namespace進行授權
```yaml
apiVersion: rbac.authorization.k8s.io/v1
# This cluster role binding allows anyone in the "manager" group to read secrets in any namespace.
kind: ClusterRoleBinding
metadata:
name: read-secrets-global
subjects:
- kind: Group
name: manager # Name is case sensitive
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: secret-reader
apiGroup: rbac.authorization.k8s.io
```

###### kubelet的認證授權
查看kubelet進程
```powershell
$ systemctl status kubelet
● kubelet.service - kubelet: The Kubernetes Node Agent
Loaded: loaded (/etc/systemd/system/kubelet.service; enabled; vendor preset: disabled)
Drop-In: /etc/systemd/system/kubelet.service.d
└─10-kubeadm.conf
Active: active (running) since Wed 2020-04-01 02:34:13 CST; 1 day 14h ago
Docs: https://kubernetes.io/docs/
Main PID: 851 (kubelet)
Tasks: 21
Memory: 127.1M
CGroup: /system.slice/kubelet.service
└─851 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf
```
查看`/etc/kubernetes/kubelet.conf`,解析證書:
```powershell
$ echo xxxxx |base64 -d >kubelet.crt
$ openssl x509 -in kubelet.crt -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 9059794385454520113 (0x7dbadafe23185731)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=kubernetes
Validity
Not Before: Feb 10 07:33:39 2020 GMT
Not After : Feb 9 07:33:40 2021 GMT
Subject: O=system:nodes, CN=system:node:master-1
```
得到我們期望的內容:
```bash
Subject: O=system:nodes, CN=system:node:k8s-master
```
我們知道,k8s會把O作為Group來進行請求,因此如果有權限綁定給這個組,肯定在clusterrolebinding的定義中可以找得到。因此嘗試去找一下綁定了system:nodes組的clusterrolebinding
```powershell
$ kubectl get clusterrolebinding|awk 'NR>1{print $1}'|xargs kubectl get clusterrolebinding -oyaml|grep -n10 system:nodes
98- roleRef:
99- apiGroup: rbac.authorization.k8s.io
100- kind: ClusterRole
101- name: system:certificates.k8s.io:certificatesigningrequests:selfnodeclient
102- subjects:
103- - apiGroup: rbac.authorization.k8s.io
104- kind: Group
105: name: system:nodes
106-- apiVersion: rbac.authorization.k8s.io/v1
107- kind: ClusterRoleBinding
108- metadata:
109- creationTimestamp: "2020-02-10T07:34:02Z"
110- name: kubeadm:node-proxier
111- resourceVersion: "213"
112- selfLink: /apis/rbac.authorization.k8s.io/v1/clusterrolebindings/kubeadm%3Anode-proxier
$ kubectl describe clusterrole system:certificates.k8s.io:certificatesigningrequests:selfnodeclient
Name: system:certificates.k8s.io:certificatesigningrequests:selfnodeclient
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
certificatesigningrequests.certificates.k8s.io/selfnodeclient [] [] [create]
```
結局有點意外,除了`system:certificates.k8s.io:certificatesigningrequests:selfnodeclient`外,沒有找到system相關的rolebindings,顯然和我們的理解不一樣。 嘗試去找[資料](https://kubernetes.io/docs/reference/access-authn-authz/rbac/),發現了這么一段 :
| Default ClusterRole | Default ClusterRoleBinding | Description |
| :----------------------------- | :---------------------------------- | :----------------------------------------------------------- |
| system:kube-scheduler | system:kube-scheduler user | Allows access to the resources required by the [scheduler](https://kubernetes.io/docs/reference/generated/kube-scheduler/)component. |
| system:volume-scheduler | system:kube-scheduler user | Allows access to the volume resources required by the kube-scheduler component. |
| system:kube-controller-manager | system:kube-controller-manager user | Allows access to the resources required by the [controller manager](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/) component. The permissions required by individual controllers are detailed in the [controller roles](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#controller-roles). |
| system:node | None | Allows access to resources required by the kubelet, **including read access to all secrets, and write access to all pod status objects**. You should use the [Node authorizer](https://kubernetes.io/docs/reference/access-authn-authz/node/) and [NodeRestriction admission plugin](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#noderestriction) instead of the `system:node` role, and allow granting API access to kubelets based on the Pods scheduled to run on them. The `system:node` role only exists for compatibility with Kubernetes clusters upgraded from versions prior to v1.8. |
| system:node-proxier | system:kube-proxy user | Allows access to the resources required by the [kube-proxy](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-proxy/)component. |
大致意思是說:之前會定義system:node這個角色,目的是為了kubelet可以訪問到必要的資源,包括所有secret的讀權限及更新pod狀態的寫權限。如果1.8版本后,是建議使用 [Node authorizer](https://kubernetes.io/docs/reference/access-authn-authz/node/) and [NodeRestriction admission plugin](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#noderestriction) 來代替這個角色的。
我們目前使用1.16,查看一下授權策略:
```powershell
$ ps axu|grep apiserver
kube-apiserver --authorization-mode=Node,RBAC --enable-admission-plugins=NodeRestriction
```
查看一下官網對Node authorizer的介紹:
*Node authorization is a special-purpose authorization mode that specifically authorizes API requests made by kubelets.*
*In future releases, the node authorizer may add or remove permissions to ensure kubelets have the minimal set of permissions required to operate correctly.*
*In order to be authorized by the Node authorizer, kubelets must use a credential that identifies them as being in the `system:nodes` group, with a username of `system:node:<nodeName>`*
###### Service Account
前面說,認證可以通過證書,也可以通過使用ServiceAccount(服務賬戶)的方式來做認證。大多數時候,我們在基於k8s做二次開發時都是選擇通過serviceaccount的方式。我們之前訪問dashboard的時候,是如何做的?
```yaml
## 新建一個名為admin的serviceaccount,並且把名為cluster-admin的這個集群角色的權限授予新建的serviceaccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: admin
namespace: kubernetes-dashboard
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: admin
annotations:
rbac.authorization.kubernetes.io/autoupdate: "true"
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: admin
namespace: kubernetes-dashboard
```
我們查看一下:
```powershell
$ kubectl -n kubernetes-dashboard get sa admin -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
creationTimestamp: "2020-04-01T11:59:21Z"
name: admin
namespace: kubernetes-dashboard
resourceVersion: "1988878"
selfLink: /api/v1/namespaces/kubernetes-dashboard/serviceaccounts/admin
uid: 639ecc3e-74d9-11ea-a59b-000c29dfd73f
secrets:
- name: admin-token-lfsrf
```
注意到serviceaccount上默認綁定了一個名為admin-token-lfsrf的secret,我們查看一下secret
```powershell
$ kubectl -n kubernetes-dashboard describe secret admin-token-lfsrf
Name: admin-token-lfsrf
Namespace: kubernetes-dashboard
Labels: <none>
Annotations: kubernetes.io/service-account.name: admin
kubernetes.io/service-account.uid: 639ecc3e-74d9-11ea-a59b-000c29dfd73f
Type: kubernetes.io/service-account-token
Data
====
ca.crt: 1025 bytes
namespace: 4 bytes
token: eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZW1vIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImFkbWluLXRva2VuLWxmc3JmIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiNjM5ZWNjM2UtNzRkOS0xMWVhLWE1OWItMDAwYzI5ZGZkNzNmIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlbW86YWRtaW4ifQ.ffGCU4L5LxTsMx3NcNixpjT6nLBi-pmstb4I-W61nLOzNaMmYSEIwAaugKMzNR-2VwM14WbuG04dOeO67niJeP6n8-ALkl-vineoYCsUjrzJ09qpM3TNUPatHFqyjcqJ87h4VKZEqk2qCCmLxB6AGbEHpVFkoge40vHs56cIymFGZLe53JZkhu3pwYuS4jpXytV30Ad-HwmQDUu_Xqcifni6tDYPCfKz2CZlcOfwqHeGIHJjDGVBKqhEeo8PhStoofBU6Y4OjObP7HGuTY-Foo4QindNnpp0QU6vSb7kiOiQ4twpayybH8PTf73dtdFt46UF6mGjskWgevgolvmO8A
```
開發的時候如何去調用k8s的api:
1. curl演示
```powershell
$ curl -k -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZW1vIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImFkbWluLXRva2VuLWxmc3JmIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiNjM5ZWNjM2UtNzRkOS0xMWVhLWE1OWItMDAwYzI5ZGZkNzNmIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlbW86YWRtaW4ifQ.ffGCU4L5LxTsMx3NcNixpjT6nLBi-pmstb4I-W61nLOzNaMmYSEIwAaugKMzNR-2VwM14WbuG04dOeO67niJeP6n8-ALkl-vineoYCsUjrzJ09qpM3TNUPatHFqyjcqJ87h4VKZEqk2qCCmLxB6AGbEHpVFkoge40vHs56cIymFGZLe53JZkhu3pwYuS4jpXytV30Ad-HwmQDUu_Xqcifni6tDYPCfKz2CZlcOfwqHeGIHJjDGVBKqhEeo8PhStoofBU6Y4OjObP7HGuTY-Foo4QindNnpp0QU6vSb7kiOiQ4twpayybH8PTf73dtdFt46UF6mGjskWgevgolvmO8A" https://62.234.214.206:6443/api/v1/namespaces/demo/pods?limit=500
```
2. postman
#### 查看etcd數據
拷貝etcdctl命令行工具:
```powershell
$ docker exec -ti etcd_container which etcdctl
$ docker cp etcd_container:/usr/local/bin/etcdctl /usr/bin/etcdctl
```
查看所有key值:
```powershell
$ ETCDCTL_API=3 etcdctl --endpoints=https://[127.0.0.1]:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt --key=/etc/kubernetes/pki/etcd/healthcheck-client.key get / --prefix --keys-only
```
查看具體的key對應的數據:
```powershell
$ ETCDCTL_API=3 etcdctl --endpoints=https://[127.0.0.1]:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt --key=/etc/kubernetes/pki/etcd/healthcheck-client.key get /registry/pods/jenkins/sonar-postgres-7fc5d748b6-gtmsb
```
#### 基於EFK實現kubernetes集群的日志平台(擴展) 錄屏!!!
##### EFK介紹
EFK工作示意

- Elasticsearch
一個開源的分布式、Restful 風格的搜索和數據分析引擎,它的底層是開源庫Apache Lucene。它可以被下面這樣准確地形容:
- 一個分布式的實時文檔存儲,每個字段可以被索引與搜索;
- 一個分布式實時分析搜索引擎;
- 能勝任上百個服務節點的擴展,並支持 PB 級別的結構化或者非結構化數據。
- Fluentd
一個針對日志的收集、處理、轉發系統。通過豐富的插件系統,可以收集來自於各種系統或應用的日志,轉化為用戶指定的格式后,轉發到用戶所指定的日志存儲系統之中。

Fluentd 通過一組給定的數據源抓取日志數據,處理后(轉換成結構化的數據格式)將它們轉發給其他服務,比如 Elasticsearch、對象存儲、kafka等等。Fluentd 支持超過300個日志存儲和分析服務,所以在這方面是非常靈活的。主要運行步驟如下
1. 首先 Fluentd 從多個日志源獲取數據
2. 結構化並且標記這些數據
3. 然后根據匹配的標簽將數據發送到多個目標服務
- Kibana
Kibana是一個開源的分析和可視化平台,設計用於和Elasticsearch一起工作。可以通過Kibana來搜索,查看,並和存儲在Elasticsearch索引中的數據進行交互。也可以輕松地執行高級數據分析,並且以各種圖標、表格和地圖的形式可視化數據。
##### 部署es服務
###### 部署分析
1. es生產環境是部署es集群,通常會使用statefulset進行部署,此例由於演示環境資源問題,部署為單點
2. 數據存儲掛載主機路徑
3. es默認使用elasticsearch用戶啟動進程,es的數據目錄是通過宿主機的路徑掛載,因此目錄權限被主機的目錄權限覆蓋,因此可以利用init container容器在es進程啟動之前把目錄的權限修改掉,注意init container要用特權模式啟動。
###### 部署並驗證
`efk/elasticsearch.yaml`
```powershell
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
addonmanager.kubernetes.io/mode: Reconcile
k8s-app: elasticsearch-logging
version: v7.4.2
name: elasticsearch-logging
namespace: logging
spec:
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
k8s-app: elasticsearch-logging
version: v7.4.2
serviceName: elasticsearch-logging
template:
metadata:
labels:
k8s-app: elasticsearch-logging
version: v7.4.2
spec:
nodeSelector:
log: "true" ## 指定部署在哪個節點。需根據環境來修改
containers:
- env:
- name: NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
- name: cluster.initial_master_nodes
value: elasticsearch-logging-0
- name: ES_JAVA_OPTS
value: "-Xms512m -Xmx512m"
image: 172.21.32.6:5000/elasticsearch/elasticsearch:7.4.2
name: elasticsearch-logging
ports:
- containerPort: 9200
name: db
protocol: TCP
- containerPort: 9300
name: transport
protocol: TCP
volumeMounts:
- mountPath: /usr/share/elasticsearch/data
name: elasticsearch-logging
dnsConfig:
options:
- name: single-request-reopen
initContainers:
- command:
- /sbin/sysctl
- -w
- vm.max_map_count=262144
image: alpine:3.6
imagePullPolicy: IfNotPresent
name: elasticsearch-logging-init
resources: {}
securityContext:
privileged: true
- name: fix-permissions
image: alpine:3.6
command: ["sh", "-c", "chown -R 1000:1000 /usr/share/elasticsearch/data"]
securityContext:
privileged: true
volumeMounts:
- name: elasticsearch-logging
mountPath: /usr/share/elasticsearch/data
volumes:
- name: elasticsearch-logging
hostPath:
path: /esdata
---
apiVersion: v1
kind: Service
metadata:
labels:
k8s-app: elasticsearch-logging
name: elasticsearch
namespace: logging
spec:
ports:
- port: 9200
protocol: TCP
targetPort: db
selector:
k8s-app: elasticsearch-logging
type: ClusterIP
```
```powershell
$ kubectl create namespace logging
## 給slave1節點打上label,將es服務調度到slave1節點
$ kubectl label node k8s-slave1 log=true
## 部署服務,可以先去部署es的節點把鏡像下載到本地
$ kubectl create -f elasticsearch.yaml
statefulset.apps/elasticsearch-logging created
service/elasticsearch created
## 等待片刻,查看一下es的pod部署到了k8s-slave1節點,狀態變為running
$ kubectl -n logging get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
elasticsearch-logging-0 1/1 Running 0 69m 10.244.1.104 k8s-slave1
# 然后通過curl命令訪問一下服務,驗證es是否部署成功
$ kubectl -n logging get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
elasticsearch ClusterIP 10.109.174.58 <none> 9200/TCP 71m
$ curl 10.109.174.58:9200
{
"name" : "elasticsearch-logging-0",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "uic8xOyNSlGwvoY9DIBT1g",
"version" : {
"number" : "7.4.2",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "2f90bbf7b93631e52bafb59b3b049cb44ec25e96",
"build_date" : "2019-10-28T20:40:44.881551Z",
"build_snapshot" : false,
"lucene_version" : "8.2.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
```
##### 部署kibana
###### 部署分析
2. kibana需要暴漏web頁面給前端使用,因此使用ingress配置域名來實現對kibana的訪問
3. kibana為無狀態應用,直接使用Deployment來啟動
4. kibana需要訪問es,直接利用k8s服務發現訪問此地址即可,http://elasticsearch:9200
###### 部署並驗證
資源文件 `efk/kibana.yaml`
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: kibana
namespace: logging
labels:
app: kibana
spec:
selector:
matchLabels:
app: kibana
template:
metadata:
labels:
app: kibana
spec:
containers:
- name: kibana
image: 172.21.32.6:5000/kibana/kibana:7.4.2
resources:
limits:
cpu: 1000m
requests:
cpu: 100m
env:
- name: ELASTICSEARCH_URL
value: http://elasticsearch:9200
ports:
- containerPort: 5601
---
apiVersion: v1
kind: Service
metadata:
name: kibana
namespace: logging
labels:
app: kibana
spec:
ports:
- port: 5601
protocol: TCP
targetPort: 5601
type: ClusterIP
selector:
app: kibana
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: kibana
namespace: logging
spec:
rules:
- host: kibana.devops.cn
http:
paths:
- path: /
backend:
serviceName: kibana
servicePort: 5601
```
```powershell
$ kubectl create -f kibana.yaml
deployment.apps/kibana created
service/kibana created
ingress/kibana created
# 然后查看pod,等待狀態變成running
$ kubectl -n logging get po
NAME READY STATUS RESTARTS AGE
elasticsearch-logging-0 1/1 Running 0 88m
kibana-944c57766-ftlcw 1/1 Running 0 15m
## 配置域名解析 kibana.devops.cn,並訪問服務進行驗證,若可以訪問,說明連接es成功
```
##### 部署fluentd
###### 部署分析
1. fluentd為日志采集服務,kubernetes集群的每個業務節點都有日志產生,因此需要使用daemonset的模式進行部署
2. 為進一步控制資源,會為daemonset指定一個選擇表情,fluentd=true來做進一步過濾,只有帶有此標簽的節點才會部署fluentd
3. 日志采集,需要采集哪些目錄下的日志,采集后發送到es端,因此需要配置的內容比較多,我們選擇使用configmap的方式把配置文件整個掛載出來
###### 部署服務
配置文件,`efk/fluentd-es-main.yaml`
```yaml
apiVersion: v1
data:
fluent.conf: |-
# This is the root config file, which only includes components of the actual configuration
#
# Do not collect fluentd's own logs to avoid infinite loops.
<match fluent.**>
@type null
</match>
@include /fluentd/etc/config.d/*.conf
kind: ConfigMap
metadata:
labels:
addonmanager.kubernetes.io/mode: Reconcile
name: fluentd-es-config-main
namespace: logging
```
配置文件,fluentd-config.yaml,注意點:
1. 數據源source的配置,k8s會默認把容器的標准和錯誤輸出日志重定向到宿主機中
2. 默認集成了 [kubernetes_metadata_filter](https://github.com/fabric8io/fluent-plugin-kubernetes_metadata_filter) 插件,來解析日志格式,得到k8s相關的元數據,raw.kubernetes
3. match輸出到es端的flush配置
```yaml
kind: ConfigMap
apiVersion: v1
metadata:
name: fluentd-config
namespace: logging
labels:
addonmanager.kubernetes.io/mode: Reconcile
data:
system.conf: |-
<system>
root_dir /tmp/fluentd-buffers/
</system>
containers.input.conf: |-
<source>
@id fluentd-containers.log
@type tail
path /var/log/containers/*.log
pos_file /var/log/es-containers.log.pos
time_format %Y-%m-%dT%H:%M:%S.%NZ
localtime
tag raw.kubernetes.*
format json
read_from_head true
</source>
# Detect exceptions in the log output and forward them as one log entry.
<match raw.kubernetes.**>
@id raw.kubernetes
@type detect_exceptions
remove_tag_prefix raw
message log
stream stream
multiline_flush_interval 5
max_bytes 500000
max_lines 1000
</match>
forward.input.conf: |-
# Takes the messages sent over TCP
<source>
@type forward
</source>
output.conf: |-
# Enriches records with Kubernetes metadata
<filter kubernetes.**>
@type kubernetes_metadata
</filter>
<match **>
@id elasticsearch
@type elasticsearch
@log_level info
include_tag_key true
host elasticsearch
port 9200
logstash_format true
request_timeout 30s
<buffer>
@type file
path /var/log/fluentd-buffers/kubernetes.system.buffer
flush_mode interval
retry_type exponential_backoff
flush_thread_count 2
flush_interval 5s
retry_forever
retry_max_interval 30
chunk_limit_size 2M
queue_limit_length 8
overflow_action block
</buffer>
</match>
```
daemonset定義文件,fluentd.yaml,注意點:
1. 需要配置rbac規則,因為需要訪問k8s api去根據日志查詢元數據
2. 需要將/var/log/containers/目錄掛載到容器中
3. 需要將fluentd的configmap中的配置文件掛載到容器內
4. 想要部署fluentd的節點,需要添加fluentd=true的標簽
`efk/fluentd.yaml`
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: fluentd-es
namespace: logging
labels:
k8s-app: fluentd-es
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: fluentd-es
labels:
k8s-app: fluentd-es
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
rules:
- apiGroups:
- ""
resources:
- "namespaces"
- "pods"
verbs:
- "get"
- "watch"
- "list"
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: fluentd-es
labels:
k8s-app: fluentd-es
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
subjects:
- kind: ServiceAccount
name: fluentd-es
namespace: logging
apiGroup: ""
roleRef:
kind: ClusterRole
name: fluentd-es
apiGroup: ""
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
labels:
addonmanager.kubernetes.io/mode: Reconcile
k8s-app: fluentd-es
name: fluentd-es
namespace: logging
spec:
selector:
matchLabels:
k8s-app: fluentd-es
template:
metadata:
labels:
k8s-app: fluentd-es
spec:
containers:
- env:
- name: FLUENTD_ARGS
value: --no-supervisor -q
image: 172.21.32.6:5000/fluentd-es-root:v1.6.2-1.0
imagePullPolicy: IfNotPresent
name: fluentd-es
resources:
limits:
memory: 500Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- mountPath: /var/log
name: varlog
- mountPath: /var/lib/docker/containers
name: varlibdockercontainers
readOnly: true
- mountPath: /home/docker/containers
name: varlibdockercontainershome
readOnly: true
- mountPath: /fluentd/etc/config.d
name: config-volume
- mountPath: /fluentd/etc/fluent.conf
name: config-volume-main
subPath: fluent.conf
nodeSelector:
fluentd: "true"
securityContext: {}
serviceAccount: fluentd-es
serviceAccountName: fluentd-es
volumes:
- hostPath:
path: /var/log
type: ""
name: varlog
- hostPath:
path: /var/lib/docker/containers
type: ""
name: varlibdockercontainers
- hostPath:
path: /home/docker/containers
type: ""
name: varlibdockercontainershome
- configMap:
defaultMode: 420
name: fluentd-config
name: config-volume
- configMap:
defaultMode: 420
items:
- key: fluent.conf
path: fluent.conf
name: fluentd-es-config-main
name: config-volume-main
```
```powershell
## 給slave1和slave2打上標簽,進行部署fluentd日志采集服務
$ kubectl label node k8s-slave1 fluentd=true
node/k8s-slave1 labeled
$ kubectl label node k8s-slave2 fluentd=true
node/k8s-slave2 labeled
# 創建服務
$ kubectl create -f fluentd-es-config-main.yaml
configmap/fluentd-es-config-main created
$ kubectl create -f fluentd-configmap.yaml
configmap/fluentd-config created
$ kubectl create -f fluentd.yaml
serviceaccount/fluentd-es created
clusterrole.rbac.authorization.k8s.io/fluentd-es created
clusterrolebinding.rbac.authorization.k8s.io/fluentd-es created
daemonset.extensions/fluentd-es created
## 然后查看一下pod是否已經在k8s-slave1和k8s-slave2節點啟動成功
$ kubectl -n logging get po -o wide
NAME READY STATUS RESTARTS AGE
elasticsearch-logging-0 1/1 Running 0 123m
fluentd-es-246pl 1/1 Running 0 2m2s
fluentd-es-4e21w 1/1 Running 0 2m10s
kibana-944c57766-ftlcw 1/1 Running 0 50m
```
##### EFK功能驗證
###### 驗證思路
k8s-slave1和slave2中啟動服務,同時往標准輸出中打印測試日志,到kibana中查看是否可以收集
###### 創建測試容器
```yaml
apiVersion: v1
kind: Pod
metadata:
name: counter
spec:
nodeSelector:
fluentd: "true"
containers:
- name: count
image: alpine:3.6
args: [/bin/sh, -c,
'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done']
```
```powershell
$ kubectl get po
NAME READY STATUS RESTARTS AGE
counter 1/1 Running 0 6s
```
###### 配置kibana
登錄kibana界面,按照截圖的順序操作:




也可以通過其他元數據來過濾日志數據,比如可以單擊任何日志條目以查看其他元數據,如容器名稱,Kubernetes 節點,命名空間等,比如kubernetes.pod_name : counter
到這里,我們就在 Kubernetes 集群上成功部署了 EFK ,要了解如何使用 Kibana 進行日志數據分析,可以參考 Kibana 用戶指南文檔:https://www.elastic.co/guide/en/kibana/current/index.html