第一章 准备
1.1 逐层剖析kubernetes知识体系
kubernetes核心知识体系
k8s管理员认证是什么 K8s 管理员认证通常指Certified Kubernetes Administrator(CKA,认证 Kubernetes 管理员) ,是由 Cloud Native Computing Foundation(CNCF,云原生计算基金会)与 Linux Foundation(LF,Linux 基金会)联合推出的官方认证,旨在验证从业者具备管理 Kubernetes 集群的核心技能与实操能力,是目前 Kubernetes 领域最具权威性的管理员认证之一。
除了核心的 CKA,CNCF 还推出了与 K8s 管理相关的进阶 / 专项认证,形成了完整的认证体系,其中与管理员能力相关的主要包括:
CKA(Certified Kubernetes Administrator)
定位 :核心的 K8s 管理员认证,聚焦 Kubernetes 集群的部署、配置、维护、故障排除 等基础且核心的管理能力。
考试形式 :120 分钟的实操考试,基于真实的 Kubernetes 集群环境(在线远程),考生需通过命令行完成一系列任务,而非选择题,重点考察实际操作能力。
考察内容 :涵盖集群架构与安装、ETCD 管理、网络配置(如 Service、Ingress)、存储管理、资源调度与配额、安全配置(RBAC、Secret)、故障排查等核心模块。
CKS(Certified Kubernetes Security Specialist)
定位 :K8s 安全管理员专项认证,是 CKA 的进阶认证,要求考生先通过 CKA 才能报考。
考察重点 :聚焦 Kubernetes 集群的安全配置、漏洞防护、合规检查、容器运行时安全、网络策略、镜像安全等安全管理能力。
CKAD(Certified Kubernetes Application Developer)
定位 :偏向 K8s 应用开发与部署,但也要求开发者掌握基础的 K8s 集群管理操作,与管理员的工作存在交叉,适合兼顾应用部署的管理员了解。
CKA 认证的核心价值
行业认可度 :作为 CNCF 官方认证,被全球科技企业(尤其是云原生、互联网、大厂)广泛认可,是衡量 K8s 管理员专业能力的重要标准。
实操能力验证 :考试以实操为主,避免了 “纸上谈兵”,能真实反映考生的实际集群管理能力。
职业发展助力 :在云原生技术普及的背景下,CKA 认证是后端运维、云原生工程师、K8s 管理员等岗位的重要加分项,也有助于提升职业竞争力与薪资水平。
报考与备考要点
报考条件 :无硬性学历或工作经验要求,任何人均可报考,但建议具备至少 6 个月的 Kubernetes 实操经验。
考试费用 :约 395 美元(含一次重考机会),需通过 Linux Foundation 的培训平台注册报名。
备考核心 :重点通过实操练习掌握 K8s 核心命令与集群管理流程,可结合 CNCF 官方的 K8s 文档、实操课程(如 LF 的 CKA 培训)、模拟考试环境进行准备。
知识体系
1.2 项目解析
实战部署架构图
实战成果
搭建好集群,并在集群pod中运行一个博客应用。
1.3 实战项目代码简介
https://github.com/ggw2021/kubeblog
前端
Thymeleaf 模版渲染引擎
SemanticUI
后端
HTTP servlet 请求调用链
数据库
1.4 博客应用开发环境搭建
这里我使用的ubuntu,没有跟着教程来
安装java环境
查看安装java步骤
先检查是否安装了 JDK
执行以下命令查看 JDK 安装情况:
1 2 3 java -version javac -version
找到 JDK 的安装路径
安装后,用以下命令定位 JDK 目录:
1 2 update-alternatives --config java
复制输出中的 “路径” 部分(去掉/bin/java后缀),比如/usr/lib/jvm/java-17-openjdk-amd64。
配置 JAVA_HOME 环境变量
编辑~/.zshrc(或~/.bashrc,根据你用的 shell):
在文件末尾添加:
1 2 3 export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64export PATH=$JAVA_HOME /bin:$PATH
生效配置并验证
1 2 3 4 5 6 7 source ~/.zshrcecho $JAVA_HOME java -version mvn -v
关键说明:
JAVA_HOME必须指向JDK 的根目录 (不是bin目录),否则 Maven 无法识别;
如果用的是 bash 而非 zsh,需编辑~/.bashrc并执行source ~/.bashrc;
不同 JDK 版本(如 11、21)的路径会略有差异,以update-alternatives的输出为准。
安装maven
http://maven.apache.org/download.cgi
1 2 3 4 5 6 7 8 9 10 11 12 ~/tool ................................................................................................................................................................................................................. 13s gavin@gavin-pc 14:31:51 > ll 总计 8.9M drwxr-xr-x 6 gavin gavin 4.0K 7月 12 18:30 apache-maven-3.9.11 -rw-r--r-- 1 gavin gavin 8.9M 11月 22 14:29 apache-maven-3.9.11-bin.zip ~/tool ................................................................................................................................................................................................................. 13s gavin@gavin-pc 14:33:23 > cat ~/.zshrc | tail -n 2 export PATH=/home/gavin/tool/apache-maven-3.9.11/bin:$PATH ~/tool ..................................................................................................................................................................................................................... gavin@gavin-pc 14:33:25 > source ~/.zshrc
进入项目的Final目录,执行
然后可以看到一个jar包kubeblog.jar
1 2 3 4 5 6 7 8 9 10 11 ~/workspace/ggw2021/kubeblog/Final/target main ............................................................................................................................................................................. gavin@gavin-pc 15:35:23 > ll 总计 46M drwxrwxr-x 5 gavin gavin 4.0K 11月 22 15:33 classes drwxrwxr-x 3 gavin gavin 4.0K 11月 22 15:33 generated-sources drwxrwxr-x 3 gavin gavin 4.0K 11月 22 15:33 generated-test-sources -rw-rw-r-- 1 gavin gavin 43M 11月 22 15:35 kubeblog.jar -rw-rw-r-- 1 gavin gavin 2.7M 11月 22 15:33 kubeblog.jar.original drwxrwxr-x 2 gavin gavin 4.0K 11月 22 15:33 maven-archiver drwxrwxr-x 3 gavin gavin 4.0K 11月 22 15:33 maven-status drwxrwxr-x 3 gavin gavin 4.0K 11月 22 15:33 test-classes
1.5 kvm + virt-manager安装
太轻量导致很多功能都没有,不方便配置,非常不推荐入门选择
以下是Ubuntu 系统 安装 KVM + virt-manager 的详细步骤,适配 Ubuntu 20.04/22.04/24.04 等主流版本:
一、检查硬件虚拟化支持
CPU 需开启 Intel VT-x(vmx)或 AMD-V(svm),且 BIOS/UEFI 中已启用虚拟化:
1 2 grep -E '(vmx|svm)' /proc/cpuinfo
若输出包含vmx(Intel)或svm(AMD),则支持;否则需进入 BIOS/UEFI 开启虚拟化(参考主板手册)。
二、安装 KVM 及依赖组件
1 2 3 4 5 sudo apt update sudo apt install -y qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virt-manager virt-viewer virtinst
qemu-kvm:KVM 的底层虚拟化引擎
libvirt-daemon-system:libvirt 服务(管理虚拟机)
virt-manager:图形化管理工具
virt-viewer:虚拟机控制台查看器
三、配置用户权限(避免每次 sudo)
将当前用户加入libvirt和kvm组:
1 2 sudo usermod -aG libvirt $USER sudo usermod -aG kvm $USER
注意 :需重新登录 (或重启)才能让组权限生效。
四、启动并启用 libvirt 服务
1 2 3 4 5 sudo systemctl enable --now libvirtd sudo systemctl status libvirtd
五、验证安装
1 2 3 4 5 lsmod | grep kvm virsh list --all
六、启动 virt-manager
常见问题解决
启动 virt-manager 提示权限不足 :重新登录用户(让组权限生效)。
虚拟机无网络
:默认 libvirt 会创建
虚拟网络,若未自动创建,执行:
1 2 sudo virsh net-start default sudo virsh net-autostart default
需要桥接网络(虚拟机直连物理网络):需配置 Ubuntu 桥接(如br0),可参考:
1 2 3 4 sudo apt install -y netplan.io sudo nano /etc/netplan/01-netcfg.yaml
配置示例(替换enp0s3为你的物理网卡名):
1 2 3 4 5 6 7 8 9 network: ethernets: enp0s3: dhcp4: no bridges: br0: dhcp4: yes interfaces: [enp0s3 ] version: 2
应用配置:
完成后即可通过 virt-manager 图形化界面创建、管理虚拟机(支持 Ubuntu/Windows/CentOS 等系统)。
1.6 virtual box安装虚拟机
之前装过kvm+virt-manager,需要卸载一下
1 sudo systemctl disable --now libvirtd
即使服务禁用,KVM 模块可能仍被加载,执行:
如果输出包含kvm或kvm_amd,手动卸载:
1 2 sudo rmmod kvm_amd sudo rmmod kvm
目标
virtual box安装好ubuntu虚拟机
2核4GB
双网卡
host-only: 宿主机和虚拟机通信
NAT网络: 虚拟机之间共用宿主机虚拟网卡,并且在一个局域网内,也就是能够上网,且虚拟机之间访问没问题
配置好后虚拟机可以访问外网,并且虚拟机宿主机之间可以互ping
host-only设置
step1: 创建虚拟网卡
step2: 设置虚拟机host-only,设置的时候选择刚才创建的即可
step3: 启动虚拟机
NAT网络设置 :
注意是NAT WORK,而不是NAT
step1:创建虚拟网卡,可以参考下面host-only的截图
step2: 选择后直接应用即可,不需要设置其他内容。
step3: 开启虚拟机,ping baidu.com失败,出现“Temporary failure in name resolution”
解决:sudo vim /etc/systemd/resolved.conf 添加域名解析服务器8.8.8.8,8.8.4.4,114.114.114.114sudo systemctl restart systemd-resolved.service生效
1 2 [Resolve] DNS=8.8.8.8 8.8.4.4 114.114.114.114i
静态IP设置
启动虚拟机,可以看到enp0s8没有配置
1 2 cd /etc/netplan sudo vim xxxx.yaml # 就只有一个yaml, 编辑一下
配置对应的host-only网卡,ip地址需要和刚才的IP地址在同一个网段内。
1 2 3 4 5 6 7 8 9 10 network: version: 2 ethernets: enp0s3: dhcp4: true dhcp-identifier: mac enp0s8: dhcp4: false addresses: - 192.168 .56 .10 /24
注意:后面复制虚拟机会发现enp0s3的ip都是相同的,尽管网卡的mac地址不同。原因是Ubuntu系统在请求DHCP服务时默认不再使用MAC地址作为DHCP的请求ID,而是使用/etc/machine-id,参考:https://cloud.tencent.com/developer/article/2540565。所以一定需要加上 dhcp-identifier: mac,为了让DHCP重新使用网卡的mac作为分配标识。
安装并开启ssh服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 更新软件包列表 sudo apt update # 安装OpenSSH服务器 sudo apt install openssh-server # 启动SSH服务 sudo systemctl start ssh # 设置开机自启 sudo systemctl enable ssh # 验证服务状态(确保显示active(running)) sudo systemctl status ssh
配置免密登录
宿主机执行,然后将公钥,复制复制到虚拟机的
设置主机名 - 重新连接shell生效
1 sudo hostnamectl set-hostname 新主机名
查看主机名
创建新用户并给到sudo权限
在 Ubuntu 系统中创建一个名为 nancy 的用户,为其设置密码 123456,并赋予该用户 sudo(管理员)权限。
操作步骤(需以 root 或已有 sudo 权限的用户执行)
以下命令需要在 Ubuntu 的终端中运行,操作前请确保你有管理员权限(比如用 sudo -i 切换到 root 用户)。
创建用户 nancy
首先执行创建用户的命令,同时会自动创建对应的家目录:
1 2 # -m:强制创建 home 目录;-k:指定骨架文件目录(默认 /etc/skel) sudo useradd -m -k /etc/skel nancy
-m 参数:强制创建用户的家目录(/home/nancy),这是创建普通用户的常用选项。
设置用户密码为 123456
执行密码设置命令,按提示输入指定密码:
运行后终端会提示:
1 2 New password: # 输入 123456(输入时无回显,正常现象) Retype new password: # 再次输入 123456 确认
输入完成后,终端会提示 passwd: password updated successfully,表示密码设置成功。
赋予 nancy sudo 权限
Ubuntu 中,默认将 sudo 权限赋予 sudo 用户组,因此只需将 nancy 添加到该组即可:
1 sudo usermod -aG sudo nancy
-aG 参数:-a 是追加(不覆盖原有组),-G 是指定要加入的组,这里加入 sudo 组就意味着拥有管理员权限。
验证权限是否生效(可选)
可以切换到 nancy 用户,执行一个需要 sudo 权限的命令验证:
1 2 su - nancy # 切换到 nancy 用户 sudo ls /root # 执行需要 sudo 权限的命令(/root 是 root 专属目录)
此时会提示输入 nancy 的密码(123456),输入后能正常列出 /root 目录内容,说明 sudo 权限已生效。
修改 nancy 的默认 shell 为 bash
执行以下命令,将 /bin/sh 改为 /bin/bash:
1 sudo usermod -s /bin/bash nancy
验证修改是否成功
再次查看 /etc/passwd 中 nancy 的配置:
正常输出应该是:
1 nancy:x:1001:1001::/home/nancy:/bin/bash
注意事项
生产环境中不建议使用 123456 这种简单密码 ,容易被破解,建议设置包含大小写字母、数字、特殊符号的复杂密码。
如果执行命令时提示 useradd: user 'nancy' already exists,说明该用户已存在,可跳过创建步骤,直接设置密码和添加 sudo 权限。
操作完成后,退出当前终端重新登录 nancy 用户,sudo 权限会完全生效。
总结
创建用户:sudo useradd -m nancy(-m 必加,确保生成家目录);
设置密码:sudo passwd nancy(按提示输入 123456 两次);
赋予 sudo 权限:sudo usermod -aG sudo nancy(核心是加入 sudo 用户组)。
1.7 docker安装&安装mysql server
ubuntu安装docker
http://vuepress.mirror.docker-practice.com/install/ubuntu/
使用docker安装mysql server
安装步骤自行搜索
建表
1 CREATE DATABASE IF NOT EXISTS blogDB DEFAULT CHARSET utf8 COLLATE utf8_general_ci
1.8 mysql客户端
在虚拟机内安装mysql,并且在宿主机安装mysql客户端。
虚拟机内部使用docker来安装,这里直接省略,自行搜索教程。
mysql创建数据库
宿主机安装mysql客户端
在 Ubuntu 系统中安装 MySQL 客户端的步骤如下:
更新 APT 包索引
首先确保系统的包列表是最新的,执行以下命令:
安装 MySQL 客户端
Ubuntu 默认源中提供了mysql-client包,直接安装即可(会自动包含核心客户端组件):
1 sudo apt install mysql-client -y
如果需要指定版本(如 MySQL 8.0),可安装特定版本的核心包:
1 sudo apt install mysql-client-core-8.0 -y
验证安装
安装完成后,检查客户端版本以确认安装成功:
输出示例(不同版本略有差异):
1 mysql Ver 8.0.36-0ubuntu0.22.04.1 for Linux on x86_64 ((Ubuntu))
连接远程 MySQL 服务器
使用以下命令连接远程 MySQL 服务(替换参数为实际值):
1 mysql -h 远程服务器IP/域名 -u 用户名 -p
例如:
1 mysql -h 192.168.1.100 -u root -p
输入密码后即可进入 MySQL 交互界面。
1.9 博客应用编译,打包,运行
项目编译,打包
在 IDEA 里设置 Maven 项目。
运行博客项目
1 java -jar target/kubeblog.jar
第二章 docker必知必会
2.1 Docker VS 虚拟机
Package Software into Standardized Units for Development, Shipment and Deployment.
特性
容器
虚拟机
启动
秒级
分钟级
硬盘使用
一般为MB
一般为GB
性能
接近原生
弱于
系统支持量
单机支持上千个容器
一般是几十个
2.2 Docker namespace 隔离
linux实现的 Namespace 隔离
unshare --fork --pid --mount-proc bash,这是 Linux 下用来创建一个独立的轻量级隔离环境 的常用命令,核心是通过 unshare 打破当前进程与宿主机的部分命名空间关联,最终启动一个独立的 bash 交互终端,整体比容器(如 Docker)更轻量,属于 Linux 命名空间的直接应用。
1、--fork
含义:以子进程的方式启动后续的命令(这里是 bash)
作用:unshare 本身会先创建隔离环境,再通过 fork 生成子进程运行 bash,避免隔离环境与当前终端进程强绑定,保证隔离的独立性。
2、--pid(你写的 --pif 是笔误,核心必选)
含义:创建一个独立的 PID 命名空间
核心作用:这是最关键的参数,PID 命名空间会让隔离环境内的进程拥有独立的进程 ID 编号 —— 环境内的第一个进程(这里是 bash)PID 会变成 1,就像一个 “迷你系统”,环境内看不到宿主机的其他进程,宿主机也不会把环境内的进程 ID 与自身混叠,实现进程的完全隔离 。
3、--mount-proc
含义:在新的隔离环境中重新挂载 /proc 文件系统
补充背景:/proc 是 Linux 的伪文件系统,存储着当前系统的进程信息、内核状态等,与 PID 命名空间强关联。
作用:如果不重新挂载,隔离环境内的 /proc 还是指向宿主机的,会看到宿主机的进程信息,失去 PID 隔离的意义。--mount-proc 会在隔离环境内重新生成一个只包含当前命名空间进程的 /proc,保证进程信息的隔离。
4、bash
含义:在创建好的所有隔离命名空间中,启动 bash 交互终端。
作用:让你能在这个独立的隔离环境中执行各种 Linux 命令,就像操作一个迷你的独立系统。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 gavin@gavin-vm05:~$ ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.3 22436 12800 ? Ss Jan15 0:33 /usr/lib/systemd/systemd --system --deserialize=72 root 2 0.0 0.0 0 0 ? S Jan15 0:00 [kthreadd] root 3 0.0 0.0 0 0 ? S Jan15 0:00 [pool_workqueue_release] root 4 0.0 0.0 0 0 ? I< Jan15 0:00 [kworker/R-rcu_g] root 5 0.0 0.0 0 0 ? I< Jan15 0:00 [kworker/R-rcu_p] root 6 0.0 0.0 0 0 ? I< Jan15 0:00 [kworker/R-slub_] root 7 0.0 0.0 0 0 ? I< Jan15 0:00 [kworker/R-netns] root 12 0.0 0.0 0 0 ? I< Jan15 0:00 [kworker/R-mm_pe] root 13 0.0 0.0 0 0 ? I Jan15 0:00 [rcu_tasks_kthread] root 14 0.0 0.0 0 0 ? I Jan15 0:00 [rcu_tasks_rude_kthread] root 15 0.0 0.0 0 0 ? I Jan15 0:00 [rcu_tasks_trace_kthread] root 16 0.0 0.0 0 0 ? S Jan15 0:08 [ksoftirqd/0] root 17 0.0 0.0 0 0 ? I Jan15 0:20 [rcu_preempt] root 18 0.0 0.0 0 0 ? S Jan15 0:05 [migration/0] root 19 0.0 0.0 0 0 ? S Jan15 0:00 [idle_inject/0] root 20 0.0 0.0 0 0 ? S Jan15 0:00 [cpuhp/0] ... gavin@gavin-vm05:~$ sudo unshare --fork --pid --mount-proc bash root@gavin-vm05:/home/gavin# ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.1 7604 4224 pts/1 S 07:52 0:00 bash root 8 0.0 0.1 10884 4480 pts/1 R+ 07:52 0:00 ps aux
和 Docker 容器的隔离,底层原理完全一致(只是封装不同)
你可以把这个命令理解为 “手动裸跑内核 Namespace” ,而 Docker 这类容器技术,底层同样是基于 Linux 内核 Namespace 实现隔离 ,只是做了更多上层封装:
Docker 会自动为容器创建全量命名空间 (PID、挂载、网络、UTS、IPC、用户等),而 unshare 是按需创建 (你指定 --pid 就只创 PID 命名空间,指定 --net 才创网络命名空间);
Docker 还结合了 Cgroups(资源限制) 、联合文件系统(独立文件环境) ,把内核的基础能力封装成了易用的 “容器”,而 unshare 只用到了 Namespace,是最纯粹的隔离。
一句话总结核心区别
操作方式
隔离实现者
封装程度
核心能力
unshare --pid ... bash
Linux 内核
无封装
纯 Namespace 隔离
Docker 运行容器
Linux 内核
高度封装
Namespace+Cgroups + 文件系统
简单说:不管是 unshare 还是 Docker,隔离的 “底层发动机” 都是 Linux 内核的 Namespace ,只是 unshare 是直接拧发动机旋钮,Docker 是给发动机装了个易用的方向盘。
Docker通过 Namespace 实现进程隔离
1 2 3 4 5 6 7 8 9 10 11 12 gavin@gavin-vm05:~$ docker run -it busybox Unable to find image 'busybox:latest' locally latest: Pulling from library/busybox 61dfb50712f5: Pull complete 96cfb76e59bd: Download complete Digest: sha256:e226d6308690dbe282443c8c7e57365c96b5228f0fe7f40731b5d84d37a06839 Status: Downloaded newer image for busybox:latest / # ps aux PID USER TIME COMMAND 1 root 0:00 sh 7 root 0:00 ps aux / #
2.3 Docker 的资源配额 CGroups
Cgroups (control groups)
2007年由谷歌工程师研发
2008年并入 Linux Kernel 2.6.24
C语言实现
CGroups 限制进程的 CPU使用时间。 Docker中的 CPU,内存,网络的限制均通过 cgroups 实现
实践 在宿主机上创建一个让 CPU 飙升到100%的进程: (此操作有风险,慎用)
记录下 PID = 104284
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 gavin@gavin-vm05:~$ while : ; do : ; done & [1] 104944 gavin@gavin-vm05:~$ top top - 08:11:03 up 16 days, 2:57, 1 user, load average: 0.17, 0.09, 0.09 Tasks: 130 total, 2 running, 128 sleeping, 0 stopped, 0 zombie %Cpu(s): 50.8 us, 0.5 sy, 0.0 ni, 48.4 id, 0.0 wa, 0.0 hi, 0.3 si, 0.0 st MiB Mem : 3916.0 total, 649.8 free, 931.9 used, 2624.3 buff/cache MiB Swap: 2287.0 total, 2268.1 free, 18.8 used. 2984.1 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 104944 gavin 20 0 9044 3576 1536 R 100.0 0.1 0:06.95 bash 77000 root 20 0 1241600 16192 11648 S 1.3 0.4 0:44.13 frpc 74493 65532 20 0 1262180 40088 23424 S 0.3 1.0 6:11.40 cloudflared 102859 root 20 0 0 0 0 I 0.3 0.0 0:00.79 kworker/0:1-mm_percpu_wq 104142 gavin 20 0 15124 7108 5120 S 0.3 0.2 0:00.28 sshd 1 root 20 0 22436 12800 8832 S 0.0 0.3 0:33.98 systemd 2 root 20 0 0 0 0 S 0.0 0.0 0:00.27 kthreadd
Ubuntu 20.04 及以上默认启用cgroup v2
1 2 3 4 5 6 7 8 9 cd /sys/fs/cgroup/cpu sudo mkdir cgroups_test_v2 sudo sh -c "echo +cpu > cgroups_test_v2/cgroup.subtree_control" sudo sh -c "echo 20000 100000 > cgroups_test_v2/cpu.max" sudo sh -c "echo 104944 > cgroups_test_v2/cgroup.procs"
1 2 3 4 5 6 7 8 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 104944 gavin 20 0 8780 3224 1408 R 20.0 0.1 0:48.25 bash 74493 65532 20 0 1262180 39820 23424 S 0.3 1.0 6:17.40 cloudflared 76750 root 20 0 1233584 10732 7168 S 0.3 0.3 0:50.64 containerd-shim 102859 root 20 0 0 0 0 I 0.3 0.0 0:05.51 kworker/0:1-events ... gavin@gavin-vm05:~$ kill 104944
docker 如何控制?
加参数进行资源配额 docker run -it --cpus=".5" busybox /bin/sh 进入容器查看是否有对应的 cgroup 设置
1 2 3 4 5 docker run -it --cpus=".5" busybox /bin/sh / # cd /sys/fs/cgroup/ /sys/fs/cgroup # cat cpu.max 50000 100000 /sys/fs/cgroup #
配置显示 500000,证明–cpus=".5"的参数已经生效
2.4 Docker镜像
Docker镜像的由来
虽然 Docker 实现了运行环境的隔离,但如何将一个运行的容器快速进行启动,复制,迁移到其他的主机上运行?
如果容器无法快速进行复制,迁移,那么和以 VMware 为代表的虚拟化技术相比并没有太多优势
Docker 镜像的特性
Docker 镜像具备了应用运行所需要的所有依赖
一次构建,处处运行
Docker 镜像的存储是基于 checksum 的去重存储,大大降低存储空间
2.5 编写博客 应用的 Dockerfile
在Final目录下
1 2 3 4 5 6 FROM openjdk:8-jdk-alpine MAINTAINER QingFeng VOLUME /tmp ADD target/kubeblog.jar /kubeblog.jar EXPOSE 5000 ENTRYPOINT ["java" ,"-jar" ,"/kubeblog.jar" ]
2.6 为博客应用构建 Docker 镜像
增加 mysql57 hosts 记录,作为数据库的域名(mysql57是同一台机器的另一个容器)
1 2 vi /etc/hosts 127.0.0.1 mysql57
1 2 3 git clone <git url>cd /root/kubeblog/Final mvn package
1 docker build -t kubeblog .
1 2 3 > ocker images REPOSITORY TAG IMAGE ID CREATED SIZE kubeblog 1.0 96a03d30c522 3 minutes ago 236MB
1、在docker hub上创建自己的仓库gwgong/kubeblog
1 docker tag kubeblog:1.0 gwgong/kubeblog:1.0
1 docker push gwgong/kubeblog:1.0
2.7 Docker run --link运行博客应用
1 2 3 # 二者效果一致,列出所有环境变量 env printenv
1 docker run --name kubeblog -d -p 5000:5000 --link mysql57 kubeblog:1.0
进入容器查看环境变量 evn
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 docker exec -it kubeblog sh env |grep MYSQL MYSQL57_ENV_MYSQL_MAJOR=5.7 MYSQL57_PORT_3306_TCP_ADDR=172.17.0.2 MYSQL57_ENV_MYSQL_ROOT_PASSWORD=password MYSQL57_ENV_GOSU_VERSION=1.12 MYSQL57_PORT_3306_TCP_PORT=3306 MYSQL57_PORT_3306_TCP_PROTO=tcp MYSQL57_PORT_33060_TCP_ADDR=172.17.0.2 MYSQL57_PORT=tcp://172.17.0.2:3306 MYSQL57_PORT_3306_TCP=tcp://172.17.0.2:3306 MYSQL57_PORT_33060_TCP_PORT=33060 MYSQL57_ENV_MYSQL_VERSION=5.7.30-1debian10 MYSQL57_PORT_33060_TCP_PROTO=tcp MYSQL57_NAME=/kubeblog/mysql57 MYSQL57_PORT_33060_TCP=tcp://172.17.0.2:33060
更新/etc/hosts文件 cat /etc/hosts 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters 172.17.0.2 mysql57 401b104b930a 172.17.0.3 2028007380c4
在宿主机访问 centos 虚拟机上的 kubeblog 应用,需要关闭虚拟机防火墙
1 2 systemctl stop firewalld192.168.99.101:5000
由于我这里使用了一个远程的mysql,所以容器启动命令改为
1 2 3 4 5 6 docker run \ --name kubeblog -d -p 5000:5000 \ -e MYSQL_SERVER=gxtree.com \ -e MYSQL_PORT=6606 \ -e MYSQL_PASSWORD_TEST=你的密码值 \ kubeblog:1.0
与项目里面要读取的的环境变量一致
1 2 3 4 5 6 spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://${MYSQL_SERVER:localhost}:${MYSQL_PORT:3306}/${MYSQL_DB_NAME:blogDB}?useUnicode=true&characterEncoding=utf-8 username: ${MYSQL_USER_TEST:root} password: ${MYSQL_PASSWORD_TEST:password}
第三章 k8s基础及集群搭建
学习目标:
了解 Kubernetes 的起源和发展
掌握如何使用 kubeadm 搭建 Kubernetes 集群
查看 Kubernetes 集群部署状态
3.1 Kubernetes 的起源和发展
Kubernetes 的起源
Kubernetes 最初源于谷歌内部的 Borg。Kubernetes 的最初目标是为应用的容器化编排部署提供一个最小化的平台,包含几个基本功能:
将应用水平扩容到多个集群
为扩容的实例提供负载均衡的策略
提供基本的健康检查和自愈能力
实现任务的统一调度
Kubernetes 的发展
2014 年 6 月 ,谷歌云计算专家 Eric Brewer 在旧金山的发布会为这款新的开源工具揭牌。
2015 年 7 月 22 日 ,K8S 迭代到 v 1.0 并在 OSCON 大会上正式对外公布。
为了建立容器编排领域的标准和规范,Google、RedHat 等开源基础设施领域玩家们,在 2015 年共同牵头发起了 CNCF(Cloud Native Computing Foundation)的基金会。Kubernetes 成为 CNCF 最核心的项目。发起成员:AT&T, Box, Cisco, Cloud Foundry Foundation, CoreOS, Cycle Computing, Docker, eBay, Goldman Sachs, Google, Huawei, IBM, Intel, Joyent, Kismatic, Mesosphere, Red Hat, Switch SUPERNAP, Twitter, Univa, VMware and Weaveworks。
2018 年 ,超过 170 名开发者成为 Kubernetes 项目社区贡献者,全球有 500 多场沙龙。国内出现大量基于 Kubernetes 的创业公司。
2020 年 ,Kubernetes 项目已经成为贡献者仅次于 Linux 项目的第二大开源项目。成为了业界容器编排的事实标准,各大厂商纷纷宣布支持 Kubernetes 作为容器编排的方案。
3.2 为什么需要 k8s?
传统的容器编排痛点
容器技术虽然解决了应用和基础设施异构的问题,让应用可以做到一次构建,多次部署,但在复杂的微服务场景,单靠 Docker 技术还不够,它仍然有以下问题没有解决:
集成和编排微服务模块
提供按需自动扩容,缩容能力
故障自愈
集群内的通信
Kubernetes 能解决的问题
按需的垂直扩容,新的服务器 (node) 能够轻易的增加或删除
按需的水平扩容,容器实例能够轻松扩容,缩容
副本控制器,你不用担心副本的状态
服务发现和路由
自动部署和回滚,如果应用状态错误,可以实现自动回滚
什么时候不适合使用 Kubernetes
应用是轻量级的单体应用,没有高并发的需求
团队文化不适应变革
3.3 k8s 的架构和核心概念
主控制节点组件
主控制节点组件对集群做出全局决策 (比如调度),以及检测和响应集群事件(例如,当不满足部署的replicas字段时,启动新的 pod)。
主控制节点组件可以在集群中的任何节点上运行。然而,为了简单起见,设置脚本通常会在同一个计算机上启动所有主控制节点组件,并且不会在此计算机上运行用户容器。
apiserver
主节点上负责提供 Kubernetes API 服务的组件;它是 Kubernetes 控制面的前端组件。
etcd
etcd 是兼具一致性和高可用性的键值数据库,可以作为保存 Kubernetes 所有集群数据的后台数据库。
kube-scheduler
主节点上的组件,该组件监视那些新创建的未指定运行节点的 Pod,并选择节点让 Pod 在上面运行。
调度决策考虑的因素包括单个 Pod 和 Pod 集合的资源需求、硬件 / 软件 / 策略约束、亲和性和反亲和性规则、数据位置、工作负载间的干扰和最后时限。
kube-controller-manager
在主节点上运行控制器的组件。
从逻辑上讲,每个控制器都是一个单独的进程,但是为了降低复杂性,它们都被编译到同一个可执行文件,并在一个进程中运行。这些控制器包括:
节点控制器(Node Controller) :负责在节点出现故障时进行通知和响应。
副本控制器(Replication Controller) :负责为系统中的每个副本控制器对象维护正确数量的 Pod。
终端控制器(Endpoints Controller) :填充终端(Endpoints)对象(即加入 Service 与 Pod)。
服务账户与令牌控制器(Service Account & Token Controllers) ,为新的命名空间创建默认账户和 API 访问令牌。
从节点组件
节点组件在每个节点上运行,维护运行的 Pod 并提供 Kubernetes 运行环境。
kubelet
一个在集群中每个节点上运行的代理。它保证容器都运行在 Pod 中。
kubelet 接收一组通过各类机制提供给它的 PodSpecs,确保这些 PodSpecs 中描述的容器处于运行状态且健康。kubelet 不会管理不是由 Kubernetes 创建的容器。
kube-proxy
kube-proxy 是集群中每个节点上运行的网络代理,实现 Kubernetes Service 概念的一部分。
kube-proxy 维护节点上的网络规则。这些网络规则允许从集群内部或外部的网络会话与 Pod 进行网络通信。
容器运行时(Container Runtime)
容器运行环境是负责运行容器的软件。
Kubernetes 支持多个容器运行环境:Docker、containerd、cri-o、rktlet 以及任何实现 Kubernetes CRI(容器运行时接口)的软件。
插件(Addons)
DNS
尽管其他插件并非严格意义上的必需组件,但几乎所有 Kubernetes 集群都应该有集群 DNS,因为很多示例都需要 DNS 服务。
Web 界面(仪表盘)
Dashboard 是 Kubernetes 集群的通用的、基于 Web 的用户界面。它使用户可以管理集群中运行的应用程序以及集群本身并进行故障排除。
容器资源监控
容器资源监控将关于容器的一些常见的时间序列度量值保存到一个集中的数据库中,并提供用于浏览这些数据的界面。
集群层面日志
集群层面日志机制负责将容器的日志数据保存到一个集中的日志存储中,该存储能提供搜索和浏览接口。
3.4 k8s 部署方案
部署目标
在所有节点上安装 Docker 和 kubeadm,kubelet
部署容器网络插件 flannel
部署架构
ip
域名
备注
安装软件
192.168.99.101
master
主节点
Docker Kubeadm kubelet kubectl flannel
192.168.99.102
node1
从节点 1
Docker Kubeadm kubelet kubectl flannel
192.168.99.103
node2
从节点 2
Docker Kubeadm kubelet kubectl flannel
环境准备
3 台虚拟机 CentOS7.x-86_x64
硬件配置:2GB 或更多 RAM,2 个 CPU 或更多 CPU,硬盘 30GB 或更多
集群中所有机器之间网络互通
可以访问外网,需要拉取镜像
禁止 swap 分区
3.5 安装基础软件
配置 Master 和 work 节点的域名
vi /etc/hosts
1 2 3 192.168.99.101 master 192.168.99.102 node1 192.168.99.103 node2
设置域名解析服务器 (可选)
vi /etc/resolv.conf
1 nameserver 114.114.114.114
配置时间同步服务
1 ntpdate 0.asia.pool.ntp.org
ubuntu一般自带时间同步,所以可以不用处理,如下,自带了systemd-timesyncd
1 2 gavin@gavin-vm01:~$ sudo systemctl list-units | grep systemd-timesyncd systemd-timesyncd.service loaded active running Network Time Synchronization
将桥接的 IPv4 流量传递到 iptables 的链
目的只有一个:让 Linux 内核把桥接网络的流量,交给 iptables 规则来处理 —— 这是 K8s 网络插件(Flannel/Calico/Weave 等)和 kube-proxy 能正常工作的必要前提 ,少了这步集群大概率会网络不通、Pod 无法通信。
下面用通俗的话拆解每一步的作用,先搞懂背后的核心逻辑,再看具体命令。
先搞懂:为什么需要 “桥接流量转 iptables”?
K8s 的网络核心是桥接网络 (Linux Bridge):
每个 K8s 节点都会创建虚拟网桥(比如 cbr0),节点上的所有 Pod 都通过这个网桥接入节点网络,Pod 之间的通信本质是桥接网络内的流量转发 ;
K8s 的核心网络功能(比如 Service 的负载均衡、网络策略的访问控制、kube-proxy 的端口转发),全都是通过iptables 规则 实现的(kube-proxy 默认模式就是 iptables)。
但 Linux 内核默认不会把桥接网络的流量交给 iptables 处理 —— 简单说,桥接的流量会 “绕过” iptables,K8s 配置的 iptables 规则就成了 “摆设”,最终导致 Pod 通信失败、Service 无法访问。
而这个操作,就是强制让内核开启这个转发功能 ,让桥接流量走 iptables 的规则链。
1 2 3 4 5 6 modprobe br_netfilter echo "1" >/proc/sys/net/bridge/bridge-nf-call-iptables echo "br_netfilter" > /etc/modules-load.d/k8s.conf vi /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1
逐行拆解命令的具体作用
1、modprobe br_netfilter
加载 Linux 内核的br_netfilter 模块 —— 这个模块是实现 “桥接流量与 iptables 交互” 的内核基础 ,没有这个模块,后续的配置全部无效。
modprobe 是临时加载内核模块的命令,重启节点后会失效,所以需要后续通过 sysctl 做永久配置。
2、echo "1" >/proc/sys/net/bridge/bridge-nf-call-iptables
临时开启 “桥接 IPv4 流量调用 iptables 规则” 的功能:
/proc/sys/ 是 Linux 内核的临时配置目录 ,写入 1 表示开启该功能,重启节点后会恢复默认值(0,关闭);
对应的还有bridge-nf-call-ip6tables,是针对 IPv6 桥接流量的,K8s 也建议一起开启。
3、编辑/etc/sysctl.d/k8s.conf并写入两行配置
1 2 net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1
这一步是做永久配置 :
/etc/sysctl.d/ 是 Linux 的内核参数永久配置目录 ,这里的配置会在系统启动时自动加载,不受节点重启影响;
同时开启 IPv4 和 IPv6 的桥接流量转 iptables,适配 K8s 的网络需求(即使你不用 IPv6,K8s 官方也建议开启,避免兼容问题)。
补充:少了这步会出现什么问题?
如果不做这个配置,集群搭建后大概率会遇到这些典型问题:
Pod 之间跨节点无法通信(同节点可能正常,因为无需网桥跨节点转发);
Service ClusterIP 无法访问,kube-proxy 配置的 iptables 规则不生效;
网络策略(NetworkPolicy)完全没用,无法限制 Pod 的访问;
kubelet / 控制器组件可能报 “网络超时”,集群状态异常。
还有一个关键步骤:让配置立即生效
你贴的命令里少了一步核心操作 (官方文档里必加),写完 sysctl 配置后,需要执行以下命令让配置立即生效,无需重启节点:
这个命令会重新加载/etc/sysctl.d/、/etc/sysctl.conf等所有内核参数配置文件,让br_netfilter的相关配置立即生效。
额外:如何确认配置成功?
执行以下命令,若返回结果都是1,说明配置成功:
1 2 3 4 # 检查IPv4桥接流量转iptables sysctl net.bridge.bridge-nf-call-iptables # 检查IPv6桥接流量转iptables sysctl net.bridge.bridge-nf-call-ip6tables
返回示例(成功状态):
1 2 net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1
关闭防火墙
在搭建 Kubernetes 集群时,关闭防火墙是一个常见的实践,但并非绝对的强制要求。其核心原因在于 Kubernetes 集群的组件(如 kubelet、kube-proxy、etcd 等)需要通过特定的网络端口进行通信,而防火墙的默认规则可能会阻止这些通信,从而导致集群部署失败或功能异常。
ubuntu可以不用管,ufw默认没开
1 2 gavin@gavin-vm01:~$ sudo ufw status Status: inactive
关闭SeLinux
1 2 setenforce 0 sed -i "s/SELINUX=enforcing/SELINUX=disabled/g" /etc/selinux/config
这两条命令是用来临时关闭并永久禁用 SELinux 的,通常在 CentOS/RHEL 这类发行版上搭建 K8s 集群时会用到,但Ubuntu 系统默认没有 SELinux ,所以在 Ubuntu 上执行这两条命令会报错哦(直接忽略这个设置即可)。
关闭 swap
1 2 swapoff -a yes | cp /etc/fstab /etc/fstab_bak
vi /etc/fstab #注释带swap的这一行
1 2 3 4 5 6 7 8 9 10 11 12 # /etc/fstab: static file system information. # # Use 'blkid' to print the universally unique identifier for a # device; this may be used with UUID= as a more robust way to name devices # that works even if disks are added and removed. See fstab(5). # # <file system> <mount point> <type> <options> <dump> <pass> # / was on /dev/ubuntu-vg/ubuntu-lv during curtin installation /dev/disk/by-id/dm-uuid-LVM-bvyWpKTGQSuOEvDrwKJfhQCBYj4vHTH7N1b6sUzGg7ENVM6Vtj1ljoBqz38tQypz / ext4 defaults 0 1 # /boot was on /dev/sda2 during curtin installation /dev/disk/by-uuid/eeab33da-639a-42ac-937d-241f05b7f0eb /boot ext4 defaults 0 1 # /swap.img none swap sw 0 0
验证:看到swap里面没有任何数据(free: 查看当前系统内存和交换分区(swap)使用情况)
1 2 3 4 gavin@gavin-vm01:~$ free -m total used free shared buff/cache available Mem: 3915 540 2420 1 1223 3375 Swap: 0 0 0
安装kubeadm
1 2 3 # 安装kubelet、kubeadm、kubectl yum install kubelet kubeadm kubectl -y systemctl enable kubelet
ubuntu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 更新 apt 包索引并安装依赖 sudo apt update sudo apt install -y apt-transport-https ca-certificates curl # 下载 Google Cloud 公开签名密钥 curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/kubernetes-apt-keyring.gpg # 添加 Kubernetes apt 仓库 echo 'deb [signed-by=/etc/apt/trusted.gpg.d/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list # 安装 kubelet、kubeadm、kubectl 并锁定版本 sudo apt update sudo apt install -y kubelet kubeadm kubectl sudo apt-mark hold kubelet kubeadm kubectl # 设置 kubelet 开机自启 sudo systemctl enable --now kubelet
完成上面的操作后,可以打一个快照作为k8s node的基本模版使用
3.6 初始化master节点
1 2 3 4 5 kubeadm init --kubernetes-version=1.19.2 \ --apiserver-advertise-address=192.168.99.101 \ --image-repository registry.aliyuncs.com/google_containers \ --service-cidr=10.1.0.0/16 \ --pod-network-cidr=10.244.0.0/16
命令参数解释 :
这个命令是用来初始化 K8s 主节点的,每个参数的作用如下:
--kubernetes-version=1.19.2:指定要安装的 Kubernetes 版本为 1.19.2。
--apiserver-advertise-address=192.168.99.101:设置 API Server 对外暴露的 IP 地址(即主节点的 IP)。
--image-repository registry.aliyuncs.com/google_containers:使用阿里云的镜像仓库拉取 K8s 组件镜像,避免国内网络问题导致拉取失败。
--service-cidr=10.1.0.0/16:定义 Service ClusterIP 的地址范围。
--pod-network-cidr=10.244.0.0/16:定义 Pod 的 IP 地址范围,这个配置需要和你后续选择的网络插件(如 Flannel)保持一致。
我的master节点ip: 192.168.56.200,并且使用新版本,v1.31.14,所以需要改一下
1 2 gavin@gavin-vm02:~$ kubeadm version kubeadm version: &version.Info{Major:"1", Minor:"31", GitVersion:"v1.31.14", GitCommit:"5e00b99bac504844579ec74886b6cc5c9611ca19", GitTreeState:"clean", BuildDate:"2025-11-11T20:23:36Z", GoVersion:"go1.24.9", Compiler:"gc", Platform:"linux/amd64"}
1 sudo apt update && sudo apt install -y conntrack ipvsadm ipset socat conntrackd
docker 默认关闭了cri插件,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # Copyright 2018-2022 Docker Inc. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. disabled_plugins = ["cri"] #root = "/var/lib/containerd" #state = "/run/containerd" #subreaper = true #oom_score = 0 #[grpc] # address = "/run/containerd/containerd.sock" # uid = 0 # gid = 0 #[debug] # address = "/run/containerd/debug.sock" # uid = 0 # gid = 0 # level = "info"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 sudo bash -c " # 1. 核心:删除CRI禁用配置(把disabled_plugins改成空) sed -i 's/disabled_plugins = \[\"cri\"\]/disabled_plugins = \[\]/g' /etc/containerd/config.toml # 2. 取消grpc配置注释(让k8s能连接containerd的sock) sed -i 's/^#\[grpc\]/\[grpc\]/g' /etc/containerd/config.toml sed -i 's/^# address = \"\/run\/containerd\/containerd.sock\"/ address = \"\/run\/containerd\/containerd.sock\"/g' /etc/containerd/config.toml # 3. 追加CRI核心配置(启用CRI+开v1+开SystemdCgroup) cat >> /etc/containerd/config.toml << EOF [plugins.\"io.containerd.grpc.v1.cri\"] enable_cri_v1 = true [plugins.\"io.containerd.grpc.v1.cri\".containerd] [plugins.\"io.containerd.grpc.v1.cri\".containerd.runtimes] [plugins.\"io.containerd.grpc.v1.cri\".containerd.runtimes.runc] runtime_type = \"io.containerd.runc.v2\" [plugins.\"io.containerd.grpc.v1.cri\".containerd.runtimes.runc.options] SystemdCgroup = true EOF # 4. 重启containerd生效所有配置 systemctl restart containerd # 5. 验证核心配置 echo '=== 验证CRI是否启用 ===' grep -E 'disabled_plugins|enable_cri_v1|SystemdCgroup' /etc/containerd/config.toml echo '=== containerd状态 ===' systemctl status containerd --no-pager | grep Active "
1 2 3 4 5 6 sudo kubeadm init \ --kubernetes-version=v1.31.14 \ --apiserver-advertise-address=192.168.56.200 \ --service-cidr=10.1.0.0/16 \ --pod-network-cidr=10.244.0.0/16 \ --ignore-preflight-errors=Swap,FileExisting-conntrack
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 gavin@gavin-vm02:~$ sudo kubeadm init \ --kubernetes-version=v1.31.14 \ --apiserver-advertise-address=192.168.56.200 \ ... ... ... [addons] Applied essential addon: kube-proxy Your Kubernetes control-plane 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 Alternatively, if you are the root user, you can run: export KUBECONFIG=/etc/kubernetes/admin.conf You should now deploy a pod network to the cluster. Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at: https://kubernetes.io/docs/concepts/cluster-administration/addons/ Then you can join any number of worker nodes by running the following on each as root: kubeadm join 192.168.56.200:6443 --token rn053b.jsv5*****bsnm2x8 \ --discovery-token-ca-cert-hash sha256:5cfe1e6afc081c***************4ba7061fef52d3d36d1 gavin@gavin-vm02:~$
执行上面输出的命令
1 2 3 mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config
让当前服务器的所有终端会话,永久自动加载 K8s 集群的管理员配置文件 ,避免每次操作 kubectl 命令都手动指定配置文件路径。
1 2 3 4 5 6 7 8 # 1. 把KUBECONFIG配置写入~/.bashrc(永久生效,所有终端都能用) echo "export KUBECONFIG=/etc/kubernetes/admin.conf" >> ~/.bashrc # 2. 让当前终端立即生效 source ~/.bashrc # 3. 验证 echo $KUBECONFIG
安装网络插件 Flannel
1 kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
如果下载文件失败,或者无法从 quay.io/coreos 下载镜像,则可以使用课程代码库:
1 kubectl apply -f kubeblog/docs/Chapter4/flannel.yaml
1 2 3 4 5 6 7 8 9 root@gavin-vm02:~# kubectl get po -n kube-system NAME READY STATUS RESTARTS AGE coredns-7c65d6cfc9-94tjc 1/1 Running 0 8m14s coredns-7c65d6cfc9-mpckv 1/1 Running 0 8m14s etcd-gavin-vm02 1/1 Running 0 8m17s kube-apiserver-gavin-vm02 1/1 Running 0 8m18s kube-controller-manager-gavin-vm02 1/1 Running 0 8m17s kube-proxy-qczf7 1/1 Running 0 8m14s kube-scheduler-gavin-vm02 1/1 Running 0 8m17s
3.7 设置worker节点
修改master节点的ip
先删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 重置kubeadm,清理核心集群残留 sudo kubeadm reset -f # 2. 清理kubeconfig配置(避免节点使用旧的IP配置连接apiserver) rm -rf $HOME/.kube/config sudo rm -rf /root/.kube/config # 3. 清理容器网络残留(flannel/Calico等网络插件的虚拟网卡、路由规则) sudo ip link delete cni0 sudo ip link delete flannel.1 # 如果用的flannel,没有则忽略此条 sudo ip link delete cali* # 如果用的Calico,没有则忽略此条 sudo systemctl restart network # 重启网络,确保IP相关路由干净 # 4. 清理残留的容器(避免旧的K8s组件容器占用资源) sudo crictl rm -f $(sudo crictl ps -a -q) 2>/dev/null || true sudo docker rm -f $(sudo docker ps -a -q | grep k8s) 2>/dev/null || true
再执行
1 2 3 4 5 6 sudo kubeadm init \ --kubernetes-version=v1.31.14 \ --apiserver-advertise-address=192.168.56.200 \ --service-cidr=10.1.0.0/16 \ --pod-network-cidr=10.244.0.0/16 \ --ignore-preflight-errors=Swap,FileExisting-conntrack
配置worker
修改hostname为自己定义的域名,教程中为node1,node2
同上,先删除
在使用之前master init的时候生成的命令
1 2 kubeadm join 192.168.56.200:6443 --token rn053b.jsv5*****bsnm2x8 \ --discovery-token-ca-cert-hash sha256:5cfe1e6afc081c***************4ba7061fef52d3d36d1
如果没有记录或者token过期的话可以使用这个命令生成一个新的
1 kubeadm token create --print-join-command
3.8 安装原理
Init 命令的工作流程
kubeadm init 命令会通过执行下列步骤来启动一个 Kubernetes Control Plane 节点。
运行一系列的预检项来验证系统状态。一些检查项仅仅触发警告,其它的则会被视为错误并且退出 kubeadm,除非问题得到解决或者用户指定了 --ignore-preflight-errors=all。
生成一个自签名的 CA 证书(或者使用现有的证书,如果提供的话)来为集群中的每一个组件建立身份标识。如果用户已经通过 --cert-dir 配置的证书目录(默认为 /etc/kubernetes/pki)提供了他们自己的 CA 证书以及 / 或者密钥,那么将会跳过这个步骤,正如文档使用自定义证书所述。
将 kubeconfig 文件写入 /etc/kubernetes/ 目录,以便 kubelet、控制器管理器和调度器用来连接到 API 服务器,它们每一个都有自己的身份标识,并且拥有一个不同的 kubeconfig 文件,用于管理操作。
为 API 服务器、控制器管理器和调度器生成静态 Pod 的清单文件。静态 Pod 的清单文件被写入到 /etc/kubernetes/manifests 目录;kubelet 会监视这个目录以便在系统启动的时候创建 Pod。一旦 Control Plane 的 Pod 都运行起来,kubeadm init 的工作流程就继续往下执行。
对 Control Plane 节点应用 labels 和 taints 标记以便不会在它上面运行其它的工作负载。
生成令牌以便其它节点可以使用这个令牌向 Control Plane 节点注册自己。
Kubeadm 会创建 configmap,提供添加节点所需要的信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 root@vmmaster:~# kubectl describe configmap kubeadm-config -n kube-system Name: kubeadm-config Namespace: kube-system Labels: <none> Annotations: <none> Data ==== ClusterConfiguration: ---- apiServer: {} apiVersion: kubeadm.k8s.io/v1beta4 caCertificateValidityPeriod: 87600h0m0s certificateValidityPeriod: 8760h0m0s certificatesDir: /etc/kubernetes/pki clusterName: kubernetes controllerManager: {} dns: {} encryptionAlgorithm: RSA-2048 etcd: local: dataDir: /var/lib/etcd imageRepository: registry.k8s.io kind: ClusterConfiguration kubernetesVersion: v1.31.14 networking: dnsDomain: cluster.local podSubnet: 10.244.0.0/16 serviceSubnet: 10.1.0.0/16 proxy: {} scheduler: {} BinaryData ==== Events: <none>
以 YAML 格式查看 kube-system 命名空间里,名为 coredns-7c65d6cfc9-xhsn8 的 Pod 的完整配置和状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 root@vmmaster:~# kubectl get po -n kube-system NAME READY STATUS RESTARTS AGE coredns-7c65d6cfc9-xhsn8 1/1 Running 0 58m coredns-7c65d6cfc9-xt8hp 1/1 Running 0 58m etcd-vmmaster 1/1 Running 0 58m kube-apiserver-vmmaster 1/1 Running 0 58m kube-controller-manager-vmmaster 1/1 Running 0 58m kube-proxy-44479 1/1 Running 0 58m kube-proxy-k68p7 1/1 Running 0 30m kube-proxy-mc785 1/1 Running 0 37m kube-scheduler-vmmaster 1/1 Running 0 58m root@vmmaster:~# kubectl get po coredns-7c65d6cfc9-xhsn8 -n kube-system -o yaml apiVersion: v1 kind: Pod metadata: creationTimestamp: "2026-02-01T09:30:16Z" generateName: coredns-7c65d6cfc9- labels: k8s-app: kube-dns pod-template-hash: 7c65d6cfc9 name: coredns-7c65d6cfc9-xhsn8 namespace: kube-system ownerReferences: - apiVersion: apps/v1 blockOwnerDeletion: true controller: true ... ...
可以通过拷贝到单独的文件进行修改,并使用kubectl apply xxx.yaml来修改。
Kubernetes 集群启动问题,用 journalctl 查看日志
1 2 3 systemctl status kubelet journalctl -xefu kubelet journalctl -u kube-apiserver
注意 :使用 Kubectl describe 查看日志,一定要注意是否包含命名空间
1 2 3 kubectl describe pod kubernetes-dashboard-849cd79b75-s2snt --namespace kube-system kubectl logs -f pods/monitoring-influxdb-fc8f8d5cd-dbs7d -n kube-system kubectl logs --tail 200 -f podname -n jenkins
执行 kubeadm reset,并且将 cni0、flannel.1、docker0 等网络规则删除,参考 Worker node 安装的章节。
修改 yaml 文件中的 image 路径
3.9 向 Kubernetes API 服务器证书添加IP
运行以下命令将配置提取到外部文件:
1 kubectl -n kube-system get configmap kubeadm-config -o jsonpath="{.data.ClusterConfiguration}" > kubeadm.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 apiServer: {} apiVersion: kubeadm.k8s.io/v1beta4 caCertificateValidityPeriod: 87600h0m0s certificateValidityPeriod: 8760h0m0s certificatesDir: /etc/kubernetes/pki clusterName: kubernetes controllerManager: {} dns: {} encryptionAlgorithm: RSA-2048 etcd: local: dataDir: /var/lib/etcd imageRepository: registry.k8s.io kind: ClusterConfiguration kubernetesVersion: v1.31.14 networking: dnsDomain: cluster.local podSubnet: 10.244.0.0/16 serviceSubnet: 10.1.0.0/16 proxy: {} scheduler: {}
修改:
1 2 3 4 5 6 7 apiServer: certSANs: - "10.1.0.1" - "192.168.56.200" - "124.2**.*.125" - "k8s.***.com" ...
更新完 kubeadm 配置文件后我们就可以更新证书了,首先我们移动现有的 APIServer 的证书和密钥,因为 kubeadm 检测到他们已经存在于指定的位置,它就不会创建新的了。
1 $ mv /etc/kubernetes/pki/apiserver.{crt,key} ~
然后直接使用 kubeadm 命令生成一个新的证书:
1 $ kubeadm init phase certs apiserver --config kubeadm.yaml
重启apiserver
前提准备(必做)
操作仅在master 节点 执行,先通过kubectl get node -o wide确认 master 节点名称并登录;
确保集群有多 master 高可用 (生产环境必备),单 master 重启会导致集群短暂不可用(apiserver 服务中断);
先检查 apiserver 状态,确认问题:
1 2 3 4 # 检查静态Pod状态(核心) kubectl get pod -n kube-system | grep kube-apiserver # 检查kubelet状态(kubelet负责重建静态Pod,必须正常) systemctl status kubelet
最新版 K8s 中,kube-apiserver 静态 Pod 的 YAML 配置文件默认路径:/etc/kubernetes/manifests/kube-apiserver.yaml(kubelet 会监控该文件,文件修改 / 容器删除都会触发重建)。
kubeadm 部署(静态 Pod 模式):推荐重启方式(最安全) :
这是最新版 K8s 最主流的部署方式,核心逻辑 :kubelet 会持续监控/etc/kubernetes/manifests/下的静态 Pod YAML,当 apiserver 容器被删除后,kubelet 会立即根据 YAML 重新创建一个全新的 apiserver 容器,实现重启。
步骤 1:登录 master 节点,查看 apiserver 容器名
1 2 3 # 查看节点上的apiserver容器(docker/containerd通用) crictl ps | grep kube-apiserver # 输出示例:f82a9s7d6k54 k8s.gcr.io/kube-apiserver:v1.29.0 Running 12h kube-system_kube-apiserver-master01_xxx
记住第一列的容器 ID (或直接用名称匹配)。
步骤 2:删除 apiserver 容器(kubelet 自动重建)
1 2 3 4 # 方式1:用容器ID删除(精准) crictl rm -f 【容器ID】 # 方式2:用名称匹配删除(无需记ID,更便捷) crictl rm -f $(crictl ps -q | grep kube-apiserver)
-f:强制删除运行中的容器,kubelet 检测到容器消失后,3-5 秒内会自动重建 apiserver;
若用 docker 作为容器运行时(旧版,最新版推荐 containerd),可替换为docker rm -f 【容器ID】。
步骤 3:验证重启成功
1 2 3 4 5 6 # 1. 检查apiserver Pod状态,显示Running即正常 kubectl get pod -n kube-system | grep kube-apiserver # 2. 检查Pod重启时间,AGE会重置为几秒/几分钟 kubectl describe pod -n kube-system 【kube-apiserver-xxx】 | grep "Start Time" # 3. 验证集群可用性 kubectl get node、kubectl create ns test-ns
最后:验证
1 openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text
参考
第四章 Pod
4.1 创建第一个Pod
Kubectl 创建 Nginx pod
编写 Nginx Pod 的 yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 apiVersion: v1 kind: Pod metadata: name: my-nginx labels: name: my-nginx spec: containers: - name: my-nginx image: nginx resources: limits: memory: "128Mi" cpu: "500m" ports: - containerPort: 80
apiVersion: v1,注意这里的v需要是小写
1 2 3 4 5 6 7 8 root@vmmaster:~# mkdir k8s root@vmmaster:~# cd k8s/ root@vmmaster:~/k8s# vim nginx.yaml root@vmmaster:~/k8s# kubectl create -f nginx.yaml pod/my-nginx created root@vmmaster:~/k8s# kubectl get po NAME READY STATUS RESTARTS AGE my-nginx 1/1 Running 0 2m6s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 root@vmmaster:~/k8s# kubectl describe pod my-nginx Name: my-nginx Namespace: default Priority: 0 Service Account: default Node: vmworker01/192.168.56.201 Start Time: Sun, 01 Feb 2026 11:01:36 +0000 Labels: name=my-nginx Annotations: <none> Status: Running IP: 10.244.1.2 IPs: IP: 10.244.1.2 Containers: my-nginx: Container ID: containerd://310944a348e43f94955a7e42d764426d7a2a47adef53da5e88c379287dc48adb Image: nginx Image ID: docker.io/library/nginx@sha256:c881927c4077710ac4b1da63b83aa163937fb47457950c267d92f7e4dedf4aec Port: 80/TCP Host Port: 0/TCP State: Running Started: Sun, 01 Feb 2026 11:01:54 +0000 Ready: True Restart Count: 0 Limits: cpu: 500m memory: 128Mi Requests: cpu: 500m memory: 128Mi Environment: <none> Mounts: /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-7pwnk (ro) Conditions: Type Status PodReadyToStartContainers True Initialized True Ready True ContainersReady True PodScheduled True Volumes: kube-api-access-7pwnk: Type: Projected (a volume that contains injected data from multiple sources) TokenExpirationSeconds: 3607 ConfigMapName: kube-root-ca.crt ConfigMapOptional: <nil> DownwardAPI: true QoS Class: Guaranteed Node-Selectors: <none> Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s node.kubernetes.io/unreachable:NoExecute op=Exists for 300s Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 2m39s default-scheduler Successfully assigned default/my-nginx to vmworker01 Normal Pulling 2m38s kubelet Pulling image "nginx" Normal Pulled 2m21s kubelet Successfully pulled image "nginx" in 16.973s (16.973s including waiting). Image size: 62870438 bytes. Normal Created 2m21s kubelet Created container: my-nginx Normal Started 2m21s kubelet Started container my-nginx root@vmmaster:~/k8s#
1 2 3 4 root@vmmaster:~/k8s# kubectl exec -it my-nginx -- sh # ls bin boot dev docker-entrypoint.d docker-entrypoint.sh etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var #
整体核心结构(K8s 配置文件必记)
所有 K8s 资源配置都绕不开这 4 行基础,记死就行:
1 2 3 4 apiVersion: v1 kind: Pod metadata: {} spec: {}
metadata 元数据(Pod 的唯一标识)
1 2 3 4 metadata: name: my-nginx labels: name: my-nginx
现阶段关注 :名字别乱写(小写 + 数字 ±/.),标签知道是给 Pod 做标记的就行。
spec 核心配置(Pod 里到底跑什么)
1 2 3 4 5 6 7 8 9 10 spec: containers: - name: my-nginx image: nginx resources: limits: memory: "128Mi" cpu: "500m" ports: - containerPort: 80
4.2 Pod原理
什么是 Pod?
Pod 的共享上下文包括一组 Linux 名字空间、控制组(cgroup)和可能一些其他的隔离方面,即用
来隔离 Docker 容器的技术。在 Pod 的上下文中,每个独立的应用可能会进一步实施隔离。
就 Docker 概念的术语而言,Pod 类似于共享名字空间和文件系统的一组 Docker 容器。
说明 :除了 Docker 之外,Kubernetes 支持很多其他容器运行时,Docker 是最有名的运行时,使用 Docker 的术语来描述 Pod 会很有帮助。
Pod 生命周期
和一个独立的应用容器一样,Pod 也被认为是相对临时性(而不是长期存在)的实体。Pod 会被创建、赋予一个唯一的 ID(UID),并被调度到节点,并在终止(根据重启策略)或删除之前一直运行在该节点。
如果一个节点死掉了,调度到该节点的 Pod 也被计划在预定超时期限结束后删除。
Pod 结构图例
一个包含多个容器的 Pod 中包含一个用来拉取文件的程序和一个 Web 服务器,均使用持久卷作为容器间共享的存储。
使用 Pod
通常你不需要直接创建 Pod,甚至实例化 Pod。相反,你会使用诸如 Deployment 或 Job 这类工作负载资源来创建 Pod。如果 Pod 需要跟踪状态(比如数据库),可以考虑 StatefulSet 资源。
k8s的两种用法:
运行单个容器的 Pod
“每个 Pod 一个容器” 模型是最常见的 Kubernetes 用例;在这种情况下,可以将 Pod 看作单个容器的包装器,并且 Kubernetes 直接管理 Pod,而不是容器。
运行多个协同工作的容器的 Pod
Pod 可能封装由多个紧密耦合且需要共享资源的共容器组成的应用程序。这些位于同一位置的容器可能形成单个内聚的服务单元 —— 一个容器将文件从共享卷提供给公众,而另一个单独的 “边车”(sidecar)容器则刷新或更新这些文件。Pod 将这些容器和存储资源打包为一个可管理的实体。
演示 :使用 Job 创建一个 Pod,打印信息后会暂停。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 apiVersion: batch/v1 kind: Job metadata: name: hello spec: completions: 5 template: spec: containers: - name: hello image: nginx command: ['sh' , '-c' , 'echo "Hello, Kubernetes!" && sleep 2' ] restartPolicy: OnFailure
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 root@vmmaster:~/k8s# vim hello_job.yaml root@vmmaster:~/k8s# kubectl create -f hello_job.yaml job.batch/hello created root@vmmaster:~/k8s# kubectl get job hello NAME STATUS COMPLETIONS DURATION AGE hello Running 0/5 5s 5s root@vmmaster:~/k8s# kubectl get po NAME READY STATUS RESTARTS AGE hello-rfcsd 0/1 Completed 0 22s hello-xvvsg 1/1 Running 0 5s my-nginx 1/1 Running 0 42m root@vmmaster:~/k8s# kubectl get po -w NAME READY STATUS RESTARTS AGE hello-nhbpd 0/1 Completed 0 14s hello-rcdzz 0/1 Completed 0 7s hello-rfcsd 0/1 Completed 0 39s hello-xvvsg 0/1 Completed 0 22s my-nginx 1/1 Running 0 43m hello-z852h 0/1 Pending 0 0s hello-z852h 0/1 Pending 0 0s hello-rcdzz 0/1 Completed 0 8s hello-z852h 0/1 ContainerCreating 0 0s hello-z852h 1/1 Running 0 3s hello-z852h 0/1 Completed 0 5s hello-z852h 0/1 Completed 0 6s hello-z852h 0/1 Completed 0 6s ^Croot@vmmaster:~/k8s# kubectl logs hello-nhbpd Hello, Kubernetes! root@vmmaster:~/k8s# kubectl get job hello NAME STATUS COMPLETIONS DURATION AGE hello Complete 5/5 46s 95s root@vmmaster:~/k8s# kubectl delete job hello job.batch "hello" deleted root@vmmaster:~/k8s# kubectl get job hello Error from server (NotFound): jobs.batch "hello" not found
Pod 网络
每个 Pod 都在每个地址族中获得一个唯一的 IP 地址。Pod 中的每个容器共享网络名字空间,包括 IP 地址和网络端口。Pod 内的容器可以使用 localhost 互相通信。当 Pod 中的容器与 Pod 之外的实体通信时,它们必须协调如何使用共享的网络资源(例如端口)。
4.3 容器和Pod的生命周期
容器阶段 Phase
Pending(挂起) :Pod 已被 Kubernetes 系统接受,但有一个或者多个容器尚未创建亦未运行。此阶段包括等待 Pod 被调度的时间和通过网络下载镜像的时间。
Running(运行中) :Pod 已经绑定到了某个节点,Pod 中所有的容器都已被创建。至少有一个容器仍在运行,或者正处于启动或重启状态。
Succeeded(成功) :Pod 中的所有容器都已成功终止,并且不会再重启。
Failed(失败) :Pod 中的所有容器都已终止,并且至少有一个容器是因为失败终止。也就是说,容器以非 0 状态退出或者被系统终止。
Unknown(未知) :因为某些原因无法取得 Pod 的状态。这种情况通常是因为与 Pod 所在主机通信失败。
容器状态 Status
一旦调度器将 Pod 分派给某个节点,kubelet 就通过容器运行时开始为 Pod 创建容器。容器的状态有三种:Waiting(等待) 、Running(运行中) 和 Terminated(已终止) 。
1 kubectl describe pod <pod名称>
1 2 3 4 root@vmmaster:~/k8s# kubectl describe pod my-nginx ... Status: Running ...
Waiting(等待)
如果容器并不处在 Running 或 Terminated 状态之一,它就处在 Waiting 状态。处于 Waiting 状态的容器仍在运行它完成启动所需要的操作,例如,从某个容器镜像仓库拉取容器镜像,或者向容器应用 Secret 数据等等。当你使用 kubectl 来查询包含 Waiting 状态的容器的 Pod 时,你也会看到一个 Reason 字段,其中给出了容器处于等待状态的原因。
Running(运行中)
Running 状态表明容器正在执行状态并且没有问题发生。如果配置了 postStart 回调,那么该回调已经执行完成。如果你使用 kubectl 来查询包含 Running 状态的容器的 Pod 时,你也会看到关于容器进入 Running 状态的信息。
Terminated(已终止)
处于 Terminated 状态的容器已经开始执行并且或者正常结束或者因为某些原因失败。如果你使用 kubectl 来查询包含 Terminated 状态的容器的 Pod 时,你会看到容器进入此状态的原因、退出代码以及容器执行期间的起止时间。
4.4 为容器生命周期提供事件处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 apiVersion: v1 kind: Pod metadata: name: lifecycle labels: name: lifecycle spec: containers: - name: lifecycle image: nginx lifecycle: postStart: exec: command: ["sh" , "-c" ,"echo lifecycle from postStart handle > /usr/share/message " ] resources: limits: memory: "128Mi" cpu: "500m" ports: - containerPort: 80
在上述配置文件中,可以看到postStart命令在容器的/usr/share目录下写入文件message。类似的命令还有preStop,例如用于负责优雅地终止 nginx 服务。当因为失败而导致容器终止时,这一处方式很有用。
1 2 3 4 5 6 7 8 9 10 root@vmmaster:~/k8s# vim lifecycle.yaml root@vmmaster:~/k8s# kubectl create -f lifecycle.yaml pod/lifecycle created root@vmmaster:~/k8s# kubectl get po NAME READY STATUS RESTARTS AGE lifecycle 1/1 Running 0 9s my-nginx 1/1 Running 0 146m root@vmmaster:~/k8s# kubectl exec -it lifecycle -- sh # cat /usr/share/message lifecycle from postStart handle
4.5 创建包含 Init 容器的 Pod
理解 Init 容器
每个 Pod 中可以包含多个容器,应用运行在这些容器里面,同时 Pod 也可以有一个或多个先于应用容器启动的 Init 容器。
Init 容器与普通的容器非常像,除了如下两点:
它们总是运行到完成。
每个都必须在下一个启动之前成功完成。
如果 Pod 的 Init 容器失败,Kubernetes 会不断地重启该 Pod,直到 Init 容器成功为止。然而,如果 Pod 对应的 restartPolicy 值为 Never,Kubernetes 不会重新启动 Pod。
与普通容器的不同之处
Init 容器支持应用容器的全部属性和特性,包括资源限制、数据卷和安全设置。
同时 Init 容器不支持 lifecycle、livenessProbe、readinessProbe 和 startupProbe,因为它们必须在 Pod 就绪之前运行完成。
实战 Init Pod
下面的例子定义了一个具有 2 个 Init 容器的简单 Pod。第一个等待 myservice 启动,第二个等待 mydb 启动。一旦这两个 Init 容器都启动完成,Pod 将启动 spec 节中的应用容器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 apiVersion: v1 kind: Pod metadata: name: myapp labels: name: myapp spec: containers: - name: myapp image: busybox:1.28 command: ['sh' , '-c' , 'date && sleep 3600' ] resources: limits: memory: "128Mi" cpu: "500m" ports: - containerPort: 80 initContainers: - name: init image: busybox:1.28 command: ['sh' , '-c' , 'date && sleep 10' ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 root@vmmaster:~/k8s# vim init_pod.yaml root@vmmaster:~/k8s# kubectl create -f init_pod.yaml pod/myapp created root@vmmaster:~/k8s# kubectl get po -w NAME READY STATUS RESTARTS AGE my-nginx 1/1 Running 0 3h12m myapp 0/1 Init:0/1 0 4s myapp 0/1 Init:0/1 0 12s myapp 0/1 PodInitializing 0 22s myapp 1/1 Running 0 23s ^Croot@vmmaster:~/k8s# kubectl logs myapp Defaulted container "myapp" out of: myapp, init (init) Sun Feb 1 14:14:28 UTC 2026 root@vmmaster:~/k8s# kubectl logs myapp -c init Sun Feb 1 14:14:17 UTC 2026
4.6 探针
探针的作用
探针 是由 kubelet 对容器执行的定期诊断。要执行诊断,kubelet 调用由容器实现的 Handler(处理程序)。有三种类型的处理程序:
ExecAction :在容器内执行指定命令。如果命令退出时返回码为 0 则认为诊断成功。
TCPSocketAction :对容器的 IP 地址上的指定端口执行 TCP 检查。如果端口打开,则诊断被认为是成功的。
HTTPGetAction :对容器的 IP 地址上指定端口和路径执行 HTTP Get 请求。如果响应的状态码大于等于 200 且小于 400,则诊断被认为是成功的。
每次探测都将获得以下三种结果之一:
Success(成功) :容器通过了诊断。
Failure(失败) :容器未通过诊断。
Unknown(未知) :诊断失败,因此不会采取任何行动。
何时该使用启动探针?
对于所包含的容器需要较长时间才能启动就绪的 Pod 而言,启动探针是有用的。你不再需要配置一个较长的存活态探测时间间隔,只需要设置另一个独立的配置选定,对启动期间的容器执行探测,从而允许使用远远超出存活态时间间隔所允许的时长。
如果你的容器启动时间通常超出 initialDelaySeconds + failureThreshold × periodSeconds 总值,你应该设置一个启动探测,对存活态探针所使用的同一端点执行检查。periodSeconds 的默认值是 30 秒。你应该将其 failureThreshold 设置得足够高,以便容器有充足的时间完成启动,并且避免更改存活态探针所使用的默认值。这一设置有助于减少死锁状况的发生。
initialDelaySeconds(初始延迟秒数)
含义 :容器启动后,等待多久才开始第一次探针检查 (不管是存活态 / 启动探针)。
默认值 :0 秒(容器启动后立即开始探测)。
核心作用 :给容器留基础的初始化时间 (比如加载配置、连接数据库),避免容器刚启动还没就绪,就被探针误判为失败。
启动探针场景 :一般无需设太大,因为启动探针的核心是靠后续参数撑长时间,而非初始延迟。
periodSeconds(探测周期秒数)
含义 :探针每次检查的时间间隔 ,第一次探测后,每隔这个时间就执行一次诊断。
默认值 :30 秒(你原文中明确提到的关键默认值)。
核心作用 :控制探针的执行频率,默认 30 秒 是 K8s 的通用合理值,也是你原文中要求避免修改 的默认值(改了会影响存活态探针的常规探测逻辑)。
启动探针场景 :直接沿用默认 30 秒即可,无需调整。
failureThreshold(失败阈值)
含义 :探针连续探测失败多少次 ,才判定为最终失败(启动探针失败 = 容器启动失败,存活态探针失败 = 容器异常会被重启)。
默认值 :3 次(存活态 / 就绪态探针的默认值,启动探针无通用默认,需手动设)。
核心作用 :这是你原文中最关键的配置项 ——通过调大这个值,为慢启动容器争取足够的启动时间 ,且不改动其他探针的默认参数。
启动探针场景 :按容器实际启动时间计算后手动设为足够大的值(比如容器需要 5 分钟启动,就设为 10,下文会讲计算逻辑)。
核心计算公式:initialDelaySeconds + failureThreshold × periodSeconds
含义 :这个公式的计算结果,是探针从容器启动到判定最终失败的「总容忍时间」 —— 也就是容器能在这个时间内完成启动 / 就绪,探针就不会判失败。
核心作用 :你原文的核心逻辑就是当容器实际启动时间 > 这个总值时,必须配置启动探针 ,且通过调大启动探针的 failureThreshold 来提高这个总值,满足慢启动容器的需求。
实战
kubectl apply -f https://k8s.io/examples/pods/probe/http-liveness.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 apiVersion: v1 kind: Pod metadata: labels: test: liveness name: liveness-http spec: containers: - name: liveness image: mirrorgooglecontainers/liveness args: - /server livenessProbe: httpGet: path: /healthz port: 8080 httpHeaders: - name: Custom-Header value: Awesome initialDelaySeconds: 3 periodSeconds: 3
在这个配置文件中,可以看到 Pod 也只有一个容器。periodSeconds 字段指定了 kubelet 每隔 3 秒执行一次存活探测。initialDelaySeconds 字段告诉 kubelet 在执行第一次探测前应该等待 3 秒。
kubelet 会向容器内运行的服务(服务会监听 8080 端口)发送一个 HTTP GET 请求来执行探测。如果服务器上 /healthz 路径下的处理程序返回成功代码,则 kubelet 认为容器是健康存活的。如果处理程序返回失败代码,则 kubelet 会杀死这个容器并且重新启动它。
任何大于或等于 200 并且小于 400 的返回代码标示成功,其它返回代码都标示失败。
可以在这里看服务的源码 server.go。
容器存活的最开始 10 秒中,/healthz 处理程序返回一个 200 的状态码。之后处理程序返回 500 的状态码。
1 2 3 4 5 6 7 8 9 10 http.HandleFunc("/healthz" , func (w http.ResponseWriter, r *http.Request) { duration := time.Now().Sub(started) if duration.Seconds() > 10 { w.WriteHeader(500 ) w.Write([]byte (fmt.Sprintf("error: %v" , duration.Seconds()))) } else { w.WriteHeader(200 ) w.Write([]byte ("ok" )) } })
Kubelet 在容器启动之后 3 秒开始执行健康检测。所以前几次健康检查都是成功的。但是 10 秒之后,健康检查会失败,并且 kubelet 会杀死容器再重新启动容器。
10 秒之后,通过看 Pod 事件来检测存活探测器已经失败了并且容器被重新启动了。
1 kubectl describe pod liveness-http
1 2 3 4 5 6 oot@vmmaster:~/k8s# kubectl create -f http-liveness.yaml pod/liveness-http created root@vmmaster:~/k8s# kubectl get po liveness-http -w NAME READY STATUS RESTARTS AGE liveness-http 1/1 Running 1 (21s ago) 43s liveness-http 1/1 Running 2 (4s ago) 47s
4.7 为容器设置启动时要执行的命令和参数
创建 Pod 时设置命令及参数
创建 Pod 时,可以为其下的容器设置启动时要执行的命令及其参数。
如果要设置命令,就填写在配置文件的 command 字段下。
如果要设置命令的参数,就填写在配置文件的 args 字段下。
一旦 Pod 创建完成,该命令及其参数就无法再进行更改了。
如果在配置文件中设置了容器启动时要执行的命令及其参数,那么容器镜像中自带的命令与参数将会被覆盖而不再执行。
如果配置文件中只是设置了参数,却没有设置其对应的命令,那么容器镜像中自带的命令会使用该新参数作为其执行时的参数。
说明 :在有些容器运行时中,command 字段对应 entrypoint
本示例中,将创建一个只包含单个容器的 Pod。在 Pod 配置文件中设置了一个命令与两个参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 apiVersion: v1 kind: Pod metadata: name: command-demo labels: purpose: demonstrate-command spec: containers: - name: command-demo-container image: debian command: ["printenv" ] args: ["HOSTNAME" , "KUBERNETES_PORT" ] restartPolicy: OnFailure
command: ["printenv"]:指定容器启动时执行的核心命令
作用 :定义容器启动后要运行的可执行程序 / 命令 ,等价于 Docker 镜像中的ENTRYPOINT(入口点),会覆盖镜像原本自带的启动命令 。
本配置中 :指定容器启动后执行printenv命令 —— 这是 Linux 系统的内置命令,作用是打印环境变量的值 (类似 Windows 的echo %变量名%)。
关键格式 :必须用数组形式 编写(["命令"]),K8s 要求的标准化写法,避免命令解析出错。
args: ["HOSTNAME", "KUBERNETES_PORT"]:给command指定的命令传运行参数
作用 :是command命令的执行参数 / 入参 ,等价于 Docker 镜像中的CMD,会传递给command定义的命令 作为运行时参数;如果只写args不写command,则参数会传给镜像默认的启动命令 。
本配置中 :给printenv命令传递两个参数HOSTNAME和KUBERNETES_PORT,意思是让printenv分别打印这两个环境变量的具体值 。
关键格式 :同样是数组形式 ,数组中每个元素对应一个参数,顺序和命令行执行时的参数顺序一致。
整体执行逻辑:command + args = 容器启动后实际执行的完整命令
K8s 中command和args组合后,等价于在容器的命令行中直接执行「命令 参 1 参 2」 ,本配置的组合效果就是:
1 2 # 容器启动后实际运行的完整命令 printenv HOSTNAME KUBERNETES_PORT
1 2 3 4 5 root@vmmaster:~/k8s# kubectl create -f command_demo.yaml pod/command-demo created root@vmmaster:~/k8s# kubectl logs command-demo command-demo tcp://10.1.0.1:443
使用环境变量来设置参数
在上面的示例中,我们直接将一串字符作为命令的参数。除此之外,我们还可以将环境变量作为命令的参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 apiVersion: v1 kind: Pod metadata: name: command-demo labels: purpose: demonstrate-command spec: containers: - name: command-demo-container image: debian env: - name: MESSAGE value: "hello world" command: ["/bin/echo" ] args: ["$(MESSAGE)" ] restartPolicy: OnFailure
这意味着你可以将那些用来设置环境变量的方法应用于设置命令的参数,其中包括了 ConfigMaps 与 Secrets。
1 2 3 4 root@vmmaster:~/k8s# kubectl create -f command_demo_env.yaml pod/command-demo created root@vmmaster:~/k8s# kubectl logs command-demo hello world
说明 :环境变量需要加上括号,类似于 "$(VAR)"。这是在 command 或 args 字段使用变量的格式要求。
4.8 为容器定义相互依赖的环境变量
当创建一个 Pod 时,你可以为运行在 Pod 中的容器设置相互依赖的环境变量。设置相互依赖的环境变量,你就可以在配置清单文件的 env 的 value 中使用 $(VAR_NAME)。
在本练习中,你会创建一个单容器的 Pod。此 Pod 的配置文件定义了一个已定义常用法的相互依赖的环境变量。下面是 Pod 的配置清单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 apiVersion: v1 kind: Pod metadata: name: dependent-envars-demo spec: containers: - name: dependent-envars-demo command: - sh - -c args: - while true ; do echo -en '\n' ; printf UNCHANGED_REFERENCE=$UNCHANGED_REFERENCE'\n'; printf SERVICE_ADDRESS=$SERVICE_ADDRESS'\n';printf ESCAPED_REFERENCE=$ESCAPED_REFERENCE'\n'; sleep 30 ; done; image: busybox env: - name: SERVICE_PORT value: "80" - name: SERVICE_IP value: "172.17.0.1" - name: UNCHANGED_REFERENCE value: "$(PROTOCOL)://$(SERVICE_IP):$(SERVICE_PORT)" - name: PROTOCOL value: "https" - name: SERVICE_ADDRESS value: "$(PROTOCOL)://$(SERVICE_IP):$(SERVICE_PORT)" - name: ESCAPED_REFERENCE value: "$$(PROTOCOL)://$(SERVICE_IP):$(SERVICE_PORT)"
1 2 3 4 5 6 7 root@vmmaster:~/k8s# kubectl create -f dependent_envs.yaml pod/dependent-envars-demo created root@vmmaster:~/k8s# kubectl logs dependent-envars-demo UNCHANGED_REFERENCE=$(PROTOCOL)://172.17.0.1:80 SERVICE_ADDRESS=https://172.17.0.1:80 ESCAPED_REFERENCE=$(PROTOCOL)://172.17.0.1:80
如上所示,你已经定义了 SERVICE_ADDRESS 的正确依赖引用,UNCHANGED_REFERENCE 的错误依赖引用,并跳过了 ESCAPED_REFERENCE 的依赖引用。
如果环境变量被引用时已事先定义,则引用可以正确解析,比如 SERVICE_ADDRESS 的例子。
当环境变量未定义或仅包含部分变量时,未定义的变量会被当做普通字符串对待,比如 UNCHANGED_REFERENCE 的例子。注意,解析不正确的环境变量通常不会阻止容器启动。
4.9 为容器和 Pods 分配 CPU 资源
创建一个命名空间
创建一个命名空间,以便将本练习中创建的资源与集群的其余部分资源隔离。
1 kubectl create namespace cpu-example
指定 CPU 请求和 CPU 限制
要为容器指定 CPU 请求,请在容器资源清单中包含 resources: requests 字段。要指定 CPU 限制,请包含 resources: limits。
在本练习中,你将创建一个具有一个容器的 Pod。容器将会请求 0.5 个 CPU,而且最多限制使用 1 个 CPU。这是 Pod 的配置文件 cpu-request-limit.yaml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 apiVersion: v1 kind: Pod metadata: name: cpu-demo namespace: cpu-example spec: containers: - name: cpu-demo-ctr image: nginx resources: limits: cpu: "1" requests: cpu: "0.5" args: - -cpus - "2"
配置说明
配置文件的 args 部分提供了容器启动时的参数。-cpus "2" 参数告诉容器尝试使用 2 个 CPU。
1 2 3 4 5 6 7 8 9 10 11 root@vmmaster:~/k8s# kubectl get pod cpu-demo -n cpu-example -o yaml ... imagePullPolicy: Always name: cpu-demo-ctr resources: limits: cpu: "1" requests: cpu: 500m ... root@vmmaster:~/k8s#
4.10 用节点亲和性把 Pods 分配到节点
本页展示在 Kubernetes 集群中,如何使用节点亲和性把 Kubernetes Pod 分配到特定节点。
给节点添加标签
列出集群中的节点及其标签:
1 kubectl get nodes --show-labels
输出类似于此:
1 2 3 4 NAME STATUS ROLES AGE VERSION LABELS vmmaster Ready control-plane 29h v1.31.14 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=vmmaster,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=,node.kubernetes.io/exclude-from-external-load-balancers= vmworker01 Ready <none> 29h v1.31.14 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=vmworker01,kubernetes.io/os=linux vmworker02 Ready <none> 29h v1.31.14 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=vmworker02,kubernetes.io/os=linux
选择一个节点,给它添加一个标签:
1 kubectl label nodes <your-node-name> disktype=ssd
其中 <your-node-name> 是你所选节点的名称。
验证你所选节点具有 disktype=ssd 标签:
1 kubectl get nodes --show-labels
输出类似于此:
1 2 3 4 5 6 7 root@vmmaster:/# kubectl label nodes vmworker02 disktype=ssd node/vmworker02 labeled root@vmmaster:/# kubectl get nodes --show-labels NAME STATUS ROLES AGE VERSION LABELS vmmaster Ready control-plane 29h v1.31.14 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=vmmaster,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=,node.kubernetes.io/exclude-from-external-load-balancers= vmworker01 Ready <none> 29h v1.31.14 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=vmworker01,kubernetes.io/os=linux vmworker02 Ready <none> 29h v1.31.14 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,disktype=ssd,kubernetes.io/arch=amd64,kubernetes.io/hostname=vmworker02,kubernetes.io/os=linux
依据强制的节点亲和性调度 Pod
下面清单描述了一个 Pod,它有一个节点亲和性配置 requiredDuringSchedulingIgnoredDuringExecution,disktype=ssd。这意味着 pod 只会被调度到具有 disktype=ssd 标签的节点上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 apiVersion: v1 kind: Pod metadata: name: nginx spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disktype operator: In values: - ssd containers: - name: nginx image: nginx imagePullPolicy: IfNotPresent
1 2 3 4 5 6 7 8 root@vmmaster:~/k8s# kubectl create -f affinity.yaml pod/nginx created root@vmmaster:~/k8s# kubectl describe pod nginx ... Service Account: default Node: vmworker02/192.168.56.202 Start Time: Mon, 02 Feb 2026 15:29:17 +0000 ...
4.11 将 ConfigMap 中的键值对配置为容器环境变量
解耦配置
创建一个包含多个键值对的 ConfigMap。以下是:configmap-multikeys.yaml
1 2 3 4 5 6 7 8 apiVersion: v1 kind: ConfigMap metadata: name: special-config namespace: default data: SPECIAL_LEVEL: very SPECIAL_TYPE: charm
创建 ConfigMap:
1 kubectl create -f configmap-multikeys.yaml
使用 envFrom 将所有 ConfigMap 的数据定义为容器环境变量,ConfigMap 中的键成为 Pod 中的环境变量名称。创建文件:pod-configmap-envFrom.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 apiVersion: v1 kind: Pod metadata: name: dapi-test-pod spec: containers: - name: test-container image: busybox command: ["/bin/sh" , "-c" , "env" ] envFrom: - configMapRef: name: special-config restartPolicy: Never
查看pod日志:
1 2 3 4 5 6 7 8 9 10 11 12 13 KUBERNETES_SERVICE_PORT=443 KUBERNETES_PORT=tcp://10.1.0.1:443 HOSTNAME=dapi-test-pod SHLVL=1 HOME=/root SPECIAL_LEVEL=very KUBERNETES_PORT_443_TCP_ADDR=10.1.0.1 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin KUBERNETES_PORT_443_TCP_PORT=443 KUBERNETES_PORT_443_TCP_PROTO=tcp KUBERNETES_SERVICE_PORT_HTTPS=443 KUBERNETES_PORT_443_TCP=tcp://10.1.0.1:443 KUBERNETES_SERVICE_HOST=10.1.0.1
4.12 容器 root 用户 VS privileged
以 Root 用户运行
容器里的 root 用户≠主机的 root 用户,而 --privileged(特权模式)就是把容器的 root 权限直接拉满,让它能操控宿主机的核心配置。
Docker 允许隔离其主机 OS 上的进程、功能和文件系统,并且出于实际原因,大多数容器默认实际上以 root 用户身份运行。
sysctl kernel.hostname=Attacker:容器内执行修改宿主机内核的主机名
1 2 3 4 5 6 7 8 9 $ docker run -it busybox sh # whoami root # Notice here, we are still root! # id -u 0 # hostname 382f1c400bd # sysctl kernel.hostname=Attacker sysctl: setting key "kernel.hostname": Read-only file system
启用 privileged ,一般不建议开启,默认是关闭的
1 2 3 4 5 6 7 8 9 10 11 12 $ docker run -it --privileged busybox sh # whoami root. # Root again # id -u 0 # hostname 86c62e9ba5e # sysctl kernel.hostname=Attacker kernel.hostname = Attacker # Except now we are privileged # hostname Attacker #
Kubernetes通过Security Context提供了相同的功能:
1 2 3 4 5 6 7 8 9 10 apiVersion: v1 kind: Pod metadata: name: nginx spec: containers: - name: nginx image: nginx securityContext: privileged: true
4.13 Pod 创建非 root 用户运行
要为 Pod 设置安全性设置,可在 Pod 规约中包含 securityContext 字段。securityContext 字段值是一个 PodSecurityContext 对象。你为 Pod 所设置的安全性配置会应用到 Pod 中所有 Container 上。
下面是一个 Pod 的配置文件,该 Pod 定义了 securityContext 和一个 emptyDir 卷。创建 security-context.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 apiVersion: v1 kind: Pod metadata: name: myapp labels: name: myapp spec: securityContext: runAsUser: 1000 runAsGroup: 3000 fsGroup: 2000 volumes: - name: security emptyDir: {} containers: - name: myapp image: busybox command: ["sh" ,"-c" ,"sleep 1h" ] volumeMounts: - name: security mountPath: /data/demo securityContext: allowPrivilegeEscalation: false resources: limits: memory: "128Mi" cpu: "500m"
在配置文件中,runAsUser 字段指定 Pod 中的所有容器内的进程都使用用户 ID 1000 来运行。runAsGroup 字段指定所有容器中的进程都以主组 ID 3000 来运行。如果忽略此字段,则容器的主组 ID 将是 root(0)。当 runAsGroup 被设置时,所有创建的文件也会划归用户 1000 和组 3000。由于 fsGroup 被设置,容器中所有进程也会是附组 ID 2000 的一部分。卷 /data/demo 及在该卷中创建的任何文件的属主都会是组 ID 2000。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 root@vmmaster:~/k8s# kubectl create -f security-context.yaml pod/myapp created root@vmmaster:~/k8s# kubectl exec -it myapp -- sh ~ $ id uid=1000 gid=3000 groups=2000,3000 ~ $ ps PID USER TIME COMMAND 1 1000 0:00 sleep 1h 7 1000 0:00 sh 14 1000 0:00 ps ~ $ cd /data/demo/ /data/demo $ ls -al total 8 drwxrwsrwx 2 root 2000 4096 Feb 2 16:18 . drwxr-xr-x 3 root root 4096 Feb 2 16:18 .. /data/demo $ touch a.txt /data/demo $ ls -al total 8 drwxrwsrwx 2 root 2000 4096 Feb 2 16:20 . drwxr-xr-x 3 root root 4096 Feb 2 16:18 .. -rw-r--r-- 1 1000 2000 0 Feb 2 16:20 a.txt /data/demo $ x
第五章 k8s的网络实现
5.1 Service 对象介绍及实践
为什么需要 Service —— 一个稳定的入口
每个 Pod 都有自己的 IP 地址,但是在 Deployment 中,在同一时刻运行的 Pod 集合可能与稍后运行该应用程序的 Pod 集合不同。
这导致了一个问题:如果一组 Pod(称为 “后端”)为群集内的其他 Pod(称为 “前端”)提供功能,那么前端如何找出并跟踪要连接的 IP 地址,以便前端可以使用后端部分?
Service 定义
将运行在一组 Pods 上的应用程序公开为网络服务的抽象方法。
使用 Kubernetes 服务无需修改应用程序即可使用通用的服务发现机制。Kubernetes 为 Pods 提供自己的 IP 地址,并为一组 Pod 提供相同的 DNS 名,并且可以在它们之间进行负载均衡。
定义 Service
Service 在 Kubernetes 中是一个 REST 对象,和 Pod 类似。像所有的 REST 对象一样,Service 定义可以基于 POST 方式,请求 API server 创建新的实例。
REST对象 在 Kubernetes 中,REST 对象 (也叫 Kubernetes API 对象)是指通过 Kubernetes API 服务器管理的资源实体,它遵循 REST 架构风格来进行创建、读取、更新和删除(CRUD)操作。
简单来说,你可以把它理解为:
数据与状态的载体 :每个 REST 对象都代表集群中的一个资源或配置,比如 Pod、Service、Deployment 等,它的定义以 JSON/YAML 格式存储,描述了资源的期望状态。
通过 API 交互 :你可以通过 kubectl 或直接发送 HTTP 请求(如 POST、GET、PUT、DELETE)来操作这些对象,API 服务器会负责维护对象的实际状态与期望状态一致。
统一的管理模型 :Pod、Service、Deployment 等都是 REST 对象,它们遵循相同的 API 交互模式,这让 Kubernetes 对各类资源的管理变得统一且可扩展。
举个例子,你用 YAML 文件定义一个 Service 并执行 kubectl apply,本质就是向 API 服务器发送了一个 POST 请求,创建了一个 Service 类型的 REST 对象,API 服务器会持久化这个对象并驱动集群满足它的定义。
1 2 3 4 5 6 7 8 9 10 11 apiVersion: v1 kind: Service metadata: name: my-service spec: selector: app: MyApp ports: - protocol: TCP port: 80 targetPort: 9376
1 2 3 4 5 6 7 8 root@vmmaster:~/k8s/svc# vim scv_first.yaml root@vmmaster:~/k8s/svc# kubectl create -f scv_first.yaml service/my-service created root@vmmaster:~/k8s/svc# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.1.0.1 <none> 443/TCP 2d5h my-service ClusterIP 10.1.92.141 <none> 80/TCP 11s root@vmmaster:~/k8s/svc#
上述配置创建一个名称为 “my-service” 的 Service 对象,它会将请求代理到使用 TCP 端口 9376,并且具有标签 “app=MyApp” 的 Pod 上。Kubernetes 为该服务分配一个 IP 地址(有时称为 “集群 IP”),该 IP 地址由服务代理使用。
说明:需要注意的是,Service 能够将一个接收 port 映射到任意的 targetPort。默认情况下,targetPort 将被设置为与 port 字段相同的值。
说明:kubernetes是系统api的入口,误删之后会自动恢复。
5.2 使用service来暴露pod的服务地址
Deployment后面会提到,这里只需要它帮忙创建和管理两个pod。
注意Pod不能写selector的,Pod是作为最小管理单元设计的,使用selector这样的管理手段取管理它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 apiVersion: apps/v1 kind: Deployment metadata: name: my-nginx spec: selector: matchLabels: run: my-nginx replicas: 2 template: metadata: labels: run: my-nginx spec: containers: - name: my-nginx image: nginx ports: - containerPort: 80
此时能够通过 ssh 登录到集群中的任何一个节点上,使用 curl 也能调通所有 IP 地址。需要注意的是,容器不会使用该节点上的 80 端口,也不会使用任何特定的 NAT 规则去路由流量到 Pod 上。这意味着可以在同一个节点上运行多个 Pod,使用相同的容器端口,并且可以从集群中任何其他的 Pod 或节点上使用 IP 的方式访问到它们。10.244.2.16是pod的ip
1 2 3 4 5 6 7 8 9 10 11 root@vmmaster:~/k8s/svc# kubectl create -f deployment_for_test_nginx.yaml deployment.apps/my-nginx created root@vmmaster:~/k8s/svc# kubectl get po NAME READY STATUS RESTARTS AGE my-nginx-65b446f6c4-jmt57 1/1 Running 0 2m17s my-nginx-65b446f6c4-qpvcr 1/1 Running 0 2m17s root@vmmaster:~/k8s/svc# curl 10.244.2.16 <!DOCTYPE html> <html> <head> ...
创建 Service
Kubernetes Service 从逻辑上定义了运行在集群中的一组 Pod,这些 Pod 提供了相同的功能。当每个 Service 创建时,会被分配一个唯一的 IP 地址(也称为 clusterIP)。这个 IP 地址与一个 Service 的生命周期绑定在一起,当 Service 存在的时候它也不会改变。可以配置 Pod 使它与 Service 进行通信,Pod 知道与 Service 通信将被自动负载均衡到该 Service 中的某些 Pod 上。
可以使用 kubectl expose 命令为 2 个 Nginx 副本创建一个 Service:
1 2 3 4 5 6 7 8 9 10 root@vmmaster:~/k8s/svc# kubectl expose deployment/my-nginx service/my-nginx exposed root@vmmaster:~/k8s/svc# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.1.0.1 <none> 443/TCP 2d6h my-nginx ClusterIP 10.1.41.16 <none> 80/TCP 45s root@vmmaster:~/k8s/svc# curl 10.1.41.16 <!DOCTYPE html> <html> ...
这等价于使用 kubectl create -f 命令创建,对应如下的 yaml 文件:
nginx-svc.yaml:
1 2 3 4 5 6 7 8 9 10 11 12 apiVersion: v1 kind: Service metadata: name: my-nginx labels: run: my-nginx spec: ports: - port: 80 protocol: TCP selector: run: my-nginx
上述规约将创建一个 Service,对应具有标签 run: my-nginx 的 Pod,目标 TCP 端口 80,并且在一个抽象的 Service 端口(targetPort:容器接收流量的端口;port:抽象的 Service 端口,可以使任何其它 Pod 访问该 Service 的端口)上暴露。查看你的 Service 资源:
1 2 3 4 5 6 7 8 9 10 root@vmmaster:~/k8s/svc# kubectl create -f nginx-svc.yaml service/my-nginx created root@vmmaster:~/k8s/svc# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.1.0.1 <none> 443/TCP 2d6h my-nginx ClusterIP 10.1.195.154 <none> 80/TCP 4s root@vmmaster:~/k8s/svc# curl 10.1.195.154 <!DOCTYPE html> <html> ...
现在,能够从集群中任意节点上用 curl 命令请求 Nginx Service <CLUSTER-IP>:<PORT>。注意
Service IP 完全是虚拟的,它从来没有走过网络。
5.3 k8s网络环境补充
一句话核心:CNI 是网络标准,flannel 是实现该标准的网络插件,负责给 Pod 分配独立 IP 并打通 Pod 间网络;Service 的 ClusterIP 来自专属 CIDR 网段,通过 iptables 做流量转发 / 负载均衡,把请求路由到匹配的 Pod 。
逐组件 + 关联说明(只讲核心逻辑)
Pod 网络:K8s 网络的基础
Pod 是 K8s 网络的最小独立网络单元 ,每个 Pod 有唯一的集群内 IP (PodIP),Pod 内所有容器共享这个 IP 和网络栈;
Pod 网络的核心要求:集群内所有 Pod 能跨节点互相通信,无需 NAT ,你的实验中节点能 curl 任意 PodIP 就是这个特性。
CNI:Pod 网络的「标准规则」
CNI(容器网络接口)不是具体组件,是K8s 定义的 Pod 网络标准化接口 ;
作用:规定了「如何给 Pod 分配 IP、如何打通 Pod 间网络」的统一规则,让不同网络插件能无缝对接 K8s,K8s 只认 CNI 标准,不管底层用什么插件实现。
flannel:CNI 标准的「具体实现者」
flannel 是最常用的CNI 网络插件 ,是落地 Pod 网络规则的实际组件;
核心工作:
为集群规划Pod 专属 CIDR 网段 (每个节点分配一个子网段);
给每个 Pod 分配唯一的 PodIP;
打通跨节点的 Pod 网络(通过封装数据包、配置节点路由),实现所有 Pod 互通。
Service CIDR:Service 集群 IP 的「专属地址池」
Service CIDR 是 K8s 集群提前规划的一段独立 IP 网段 (和 Pod CIDR 不重叠);
作用:所有 Service 的 ClusterIP(比如你的 my-nginx 的 10.1.105.77)都从这个网段中分配,是 Service 的专属身份 IP,生命周期和 Service 绑定不变。
iptables:Service 流量转发的「核心工具」
iptables 是节点内核的防火墙 / 流量转发工具 ,K8s 默认用它实现 Service 的核心功能;
核心工作:
Service 创建后,K8s 会自动在所有节点 生成对应的 iptables 规则;
当请求访问 Service 的 ClusterIP 时,iptables 规则会
做 2 件事:
① 把 ClusterIP 的请求转发到匹配的 PodIP(基于 Service 的 selector 标签匹配);
② 实现简单的负载均衡(随机 / 轮询),把请求分发到多个 Pod 副本。
整体流量链路(你的 Nginx 实验为例)
节点curl Service的ClusterIP → 节点 iptables 规则匹配该 ClusterIP → iptables 转发流量到某一个 Nginx Pod 的 PodIP → flannel 打通的 Pod 网络实现节点到 Pod 的通信 → Nginx Pod 返回结果。
核心总结(3 个关键关联)
CNI 是规则 ,flannel 是实现 ,二者共同完成 Pod 网络的创建和互通;
Service CIDR 是Service 的 IP 地址池 ,决定 ClusterIP 从哪来;
iptables 是Service 的流量路由器 ,把 ClusterIP 的请求转发到实际的 PodIP,实现 Service 的负载均衡和 Pod 解耦。
5.4 集群内 Pod 通信机制
Kubernetes 支持两种基本的服务发现模式 – 环境变量和 DNS。
方式一:环境变量
当 Pod 运行在 Node 上,kubelet 会为每个活跃的 Service 添加一组环境变量。它同时支持 Docker links、简单的{SVCNAME}_SERVICE_HOST和{SVCNAME}_SERVICE_PORT变量。这里 Service 的名称需大写,横线被转换成下划线。
举个例子,一个名称为 “my-nginx” 的 Service 暴露了 TCP 端口 80,同时给它分配了 Cluster IP 地址 10.1.195.154,这个 Service 生成了如下环境变量:
1 2 3 MY_NGINX_PORT_80_TCP_PORT=80 MY_NGINX_PORT_80_TCP_PROTO=tcp MY_NGINX_PORT_80_TCP_ADDR=10.1.195.154
说明
当您具有需要访问服务的 Pod 时,并且您正在使用环境变量方法将端口和集群 IP 发布到客户端 Pod 时,必须在客户端 Pod 出现之前创建服务。否则,这些客户端 Pod 将不会设定其环境变量。
先启动svc,再启动pod,可以看到有环境变量被注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 root@vmmaster:~/k8s/svc# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.1.0.1 <none> 443/TCP 2d7h my-nginx ClusterIP 10.1.195.154 <none> 80/TCP 88m root@vmmaster:~/k8s/svc# kubectl create -f deployment_for_test_nginx.yaml deployment.apps/my-nginx created root@vmmaster:~/k8s/svc# kubectl exec -it my-nginx-65b446f6c4-5c28w -- sh # printenv | grep NGINX MY_NGINX_SERVICE_PORT=80 MY_NGINX_PORT=tcp://10.1.195.154:80 MY_NGINX_PORT_80_TCP_ADDR=10.1.195.154 MY_NGINX_PORT_80_TCP_PORT=80 MY_NGINX_PORT_80_TCP_PROTO=tcp MY_NGINX_PORT_80_TCP=tcp://10.1.195.154:80 NGINX_VERSION=1.29.4 MY_NGINX_SERVICE_HOST=10.1.195.154 #
先启动pod,再启动svc则没有注入
1 2 3 4 5 6 7 root@vmmaster:~/k8s/svc# kubectl create -f deployment_for_test_nginx.yaml deployment.apps/my-nginx created root@vmmaster:~/k8s/svc# kubectl create -f nginx-svc.yaml service/my-nginx created root@vmmaster:~/k8s/svc# kubectl exec -it my-nginx-65b446f6c4-cxfph -- sh # printenv | grep NGINX NGINX_VERSION=1.29.4
方式二:DNS
可以使用附加组件为 Kubernetes 集群设置 DNS 服务。
支持集群的 DNS 服务器(例如 CoreDNS)监视 Kubernetes API 中的新服务,并为每个服务创建一组 DNS 记录。如果在整个集群中都启用了 DNS,则所有 Pod 应该能够通过其 DNS 名称自动解析服务。
例如,如果在 Kubernetes 命名空间 “my-ns” 中有一个名为 “my-service” 的服务,则控制节点和 DNS 服务为 my-service.my-ns 创建 DNS 记录。“my-ns” 命名空间中的 Pod 应该能够通过简单地对 my-service 进行名称查找来找到它(my-service.my-ns 也可以)。
其他命名空间中的 Pod 必须将名称限定为 my-service.my-ns。这些名称将解析为服务分配的集群 IP。
DNS后面详细说,知道有这种方式即可
5.5 实践 Service 创建 DNS 记录
Kubernetes DNS 在集群上调度 DNS Pod 和服务,并配置 kubelet 以告知各个容器使用 DNS 服务的 IP 来解析 DNS 名称。
服务
A/AAAA 记录
“普通” 服务会以 my-svc.my-namespace.svc.cluster-domain.example 这种名字的形式被分配一个 DNS A 或 AAAA 记录,取决于服务的 IP 协议。该名称会解析成对应服务的集群 IP。
Pods A/AAAA 记录
经由 Deployment 或者 DaemonSet 所创建的所有 Pods 都会有如下 DNS 解析项与之对应:
1 pod-ip-address.deployment-name.my-namespace.svc.cluster-domain.example.
Pod 的 hostname 和 subdomain 字段
当前,创建 Pod 时其主机名取自 Pod 的 metadata.name 值。
Pod 规约中包含一个可选的 hostname 字段,可以用来指定 Pod 的主机名。当这个字段被设置时,它将优先于 Pod 的名字成为该 Pod 的主机名。举个例子,给定一个 hostname 设置为 “my-host” 的 Pod,该 Pod 的主机名将被设置为 “my-host”。
Pod 规约还有一个可选的 subdomain 字段,可以用来指定 Pod 的子域名。举个例子,某 Pod 的 hostname 设置为 “foo”,subdomain 设置为 “bar”,在名字空间 “my-namespace” 中对应的完全限定名为 “foo.bar.my-namespace.svc.cluster-domain.example”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 apiVersion: v1 kind: Service metadata: name: default-subdomain spec: selector: name: busybox clusterIP: None ports: - name: foo port: 1234 targetPort: 1234 --- apiVersion: v1 kind: Pod metadata: name: busybox1 labels: name: busybox spec: hostname: busybox-1 subdomain: default-subdomain containers: - image: busybox:1.28 command: - sleep - "3600" name: busybox --- apiVersion: v1 kind: Pod metadata: name: busybox2 labels: name: busybox spec: hostname: busybox-2 subdomain: default-subdomain containers: - image: busybox:1.28 command: - sleep - "3600" name: busybox
Pod 将看到自己的 DNS 域名是 “busybox-1.default-subdomain.my-namespace.svc.cluster-domain.example”。DNS 会为此名字提供一个 A 记录或 AAAA 记录,指向该 Pod 的 IP。“busybox1” 和 “busybox2” 这两个 Pod 分别具有它们自己的 A 或 AAAA 记录。
1 2 3 4 5 6 7 8 root@vmmaster:/# hostname -f vmmaster root@vmmaster:/# kubectl exec -it busybox1 -- sh / # hostname busybox-1 / # hostname -f busybox-1.default-subdomain.default.svc.cluster.local / #
5.6 从集群外部访问 Service
从集群外部访问 Service 的方法
ClusterIP:
仅仅使用一个集群内部的 IP 地址 - 这是默认值。选择这个值意味着你只想这个服务在集群内部才可以被访问到
NodePort:
在集群内部 IP 的基础上,在集群的每一个节点的端口上开放这个服务。你可以在任意:NodePort 地址上访问到这个服务。
LoadBalancer:
在使用一个集群内部 IP 地址和在 NodePort 上开放一个服务之外,向云提供商申请一个负载均衡器,会让流量转发到这个在每个节点上以 NodePort 的形式开放的服务上。
在使用一个集群内部 IP 地址和在 NodePort 上开放一个 Service 的基础上,还可以向云提供者申请一个负载均衡器,将流量转发到已经以 NodePort 形式开发的 Service 上。
下面演示NodePort的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 apiVersion: apps/v1 kind: Deployment metadata: labels: app: nginx name: nginx-deploymen spec: replicas: 1 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - image: nginx name: nginx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 apiVersion: v1 kind: Service metadata: labels: app: nginx name: nginx-deployment spec: ports: - port: 80 name: nginx-service80 protocol: TCP targetPort: 80 nodePort: 30001 selector: app: nginx type: NodePort
1 2 3 4 5 6 7 8 9 10 11 12 13 14 root@vmmaster:~# kubectl get all NAME READY STATUS RESTARTS AGE pod/nginx-deploymen-676b6c5bbc-7d9tb 1/1 Running 0 58s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/default-subdomain ClusterIP None <none> 1234/TCP 11h service/kubernetes ClusterIP 10.1.0.1 <none> 443/TCP 3d20h service/nginx-deployment NodePort 10.1.201.49 <none> 80:30001/TCP 12s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/nginx-deploymen 1/1 1 1 58s NAME DESIRED CURRENT READY AGE replicaset.apps/nginx-deploymen-676b6c5bbc 1 1 1 58s
在宿主机中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 > curl 192.168.56.200:30001 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> html { color-scheme: light dark; } ... > curl 192.168.56.201:30001 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> html { color-scheme: light dark; } ... > curl 192.168.56.202:30001 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> html { color-scheme: light dark; } ...
5.7 Ingress 介绍
Ingress
Ingress 是对集群中服务的外部访问进行管理的 API 对象,典型的访问方式是 HTTP。
Ingress 公开了从集群外部到集群内服务的 HTTP 和 HTTPS 路由。流量路由由 Ingress 资源上定义的规则控制。
下面是一个将所有流量都发送到同一 Service 的简单 Ingress 示例:
可以将 Ingress 配置为服务提供外部可访问的 URL、负载均衡流量、终止 SSL/TLS,以及提供基于名称的虚拟主机等能力。Ingress 控制器通常负责通过负载均衡器来实现 Ingress。
环境准备
必须具有 Ingress 控制器 才能满足 Ingress 的要求。仅创建 Ingress 资源本身没有任何效果。需要部署 Ingress 控制器,例如 ingress-nginx。
Ingress 资源
一个最小的 Ingress 资源示例:minimal-ingress.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: minimal-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: / spec: rules: - http: paths: - path: /testpath pathType: Prefix backend: service: name: test port: number: 80
nginx.ingress.kubernetes.io/rewrite-target: / 是 Ingress 资源中非常核心的一个注解(annotation),这个注解是专门给 ingress-nginx 控制器用的路径重写规则 ,作用是:把用户请求的原始路径,替换成注解里指定的目标路径,再转发给后端服务 。
执行过程:
用户访问 http://你的域名/testpath(或 http://你的域名/testpath/abc)
ingress-nginx 控制器看到 rewrite-target: /,会把请求的路径从 /testpath(或 /testpath/abc)重写成 / (或 /abc)
然后把重写后的请求转发给 test-svc:80 这个后端服务
Ingress 规则
每个 HTTP 规则都包含以下信息:
可选的 host。在此示例中,未指定 host,因此该规则适用于通过指定 IP 地址的所有入站 HTTP 通信。如果提供了 host(例如 foo.bar.com),则 rules 适用于该 host。
路径列表 paths(例如,/testpath),每个路径都有一个由 serviceName 和 servicePort 定义的关联后端。在负载均衡器将流量定向到引用的服务之前,主机和路径都必须匹配传入请求的内容。
backend(后端)是 Service 文档中所述的服务和端口名称的组合。与规则的 host 和 path 匹配的对 Ingress 的 HTTP(和 HTTPS)请求将发送到列出的 backend。
安装 Nginx Ingress 控制器
Ingress 控制器
为了让 Ingress 资源工作,集群必须有一个正在运行的 Ingress 控制器。
与其他类型的控制器不同,Ingress 控制器不是随集群自动启动的。
1 kubectl apply -f ingress-nginx-controller.yaml
ingress-nginx-controller.yaml 1 kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/baremetal/deploy.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 apiVersion: v1 kind: Namespace metadata: labels: app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx name: ingress-nginx --- apiVersion: v1 automountServiceAccountToken: true kind: ServiceAccount metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx namespace: ingress-nginx --- apiVersion: v1 kind: ServiceAccount metadata: labels: app.kubernetes.io/component: admission-webhook app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx-admission namespace: ingress-nginx --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx namespace: ingress-nginx rules: - apiGroups: - "" resources: - namespaces verbs: - get - apiGroups: - "" resources: - configmaps - pods - secrets - endpoints verbs: - get - list - watch - apiGroups: - "" resources: - services verbs: - get - list - watch - apiGroups: - networking.k8s.io resources: - ingresses verbs: - get - list - watch - apiGroups: - networking.k8s.io resources: - ingresses/status verbs: - update - apiGroups: - networking.k8s.io resources: - ingressclasses verbs: - get - list - watch - apiGroups: - coordination.k8s.io resourceNames: - ingress-nginx-leader resources: - leases verbs: - get - update - apiGroups: - coordination.k8s.io resources: - leases verbs: - create - apiGroups: - "" resources: - events verbs: - create - patch - apiGroups: - discovery.k8s.io resources: - endpointslices verbs: - list - watch - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: labels: app.kubernetes.io/component: admission-webhook app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx-admission namespace: ingress-nginx rules: - apiGroups: - "" resources: - secrets verbs: - get - create --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx rules: - apiGroups: - "" resources: - configmaps - endpoints - nodes - pods - secrets - namespaces verbs: - list - watch - apiGroups: - coordination.k8s.io resources: - leases verbs: - list - watch - apiGroups: - "" resources: - nodes verbs: - get - apiGroups: - "" resources: - services verbs: - get - list - watch - apiGroups: - networking.k8s.io resources: - ingresses verbs: - get - list - watch - apiGroups: - "" resources: - events verbs: - create - patch - apiGroups: - networking.k8s.io resources: - ingresses/status verbs: - update - apiGroups: - networking.k8s.io resources: - ingressclasses verbs: - get - list - watch - apiGroups: - discovery.k8s.io resources: - endpointslices verbs: - list - watch - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/component: admission-webhook app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx-admission rules: - apiGroups: - admissionregistration.k8s.io resources: - validatingwebhookconfigurations verbs: - get - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx namespace: ingress-nginx roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: ingress-nginx subjects: - kind: ServiceAccount name: ingress-nginx namespace: ingress-nginx --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: app.kubernetes.io/component: admission-webhook app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx-admission namespace: ingress-nginx roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: ingress-nginx-admission subjects: - kind: ServiceAccount name: ingress-nginx-admission namespace: ingress-nginx --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: ingress-nginx subjects: - kind: ServiceAccount name: ingress-nginx namespace: ingress-nginx --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: app.kubernetes.io/component: admission-webhook app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx-admission roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: ingress-nginx-admission subjects: - kind: ServiceAccount name: ingress-nginx-admission namespace: ingress-nginx --- apiVersion: v1 data: allow-snippet-annotations: "true" kind: ConfigMap metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx-controller namespace: ingress-nginx --- apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx-controller namespace: ingress-nginx spec: ipFamilies: - IPv4 ipFamilyPolicy: SingleStack ports: - appProtocol: http name: http port: 80 protocol: TCP targetPort: http - appProtocol: https name: https port: 443 protocol: TCP targetPort: https selector: app.kubernetes.io/component: controller app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx type: NodePort --- apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx-controller-admission namespace: ingress-nginx spec: ports: - appProtocol: https name: https-webhook port: 443 targetPort: webhook selector: app.kubernetes.io/component: controller app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx type: ClusterIP --- apiVersion: apps/v1 kind: Deployment metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx-controller namespace: ingress-nginx spec: minReadySeconds: 0 revisionHistoryLimit: 10 selector: matchLabels: app.kubernetes.io/component: controller app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx template: metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 spec: containers: - args: - /nginx-ingress-controller - --election-id=ingress-nginx-leader - --controller-class=k8s.io/ingress-nginx - --ingress-class=nginx - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller - --validating-webhook=:8443 - --validating-webhook-certificate=/usr/local/certificates/cert - --validating-webhook-key=/usr/local/certificates/key env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: LD_PRELOAD value: /usr/local/lib/libmimalloc.so image: registry.k8s.io/ingress-nginx/controller:v1.8.1@sha256:e5c4824e7375fcf2a393e1c03c293b69759af37a9ca6abdb91b13d78a93da8bd imagePullPolicy: IfNotPresent lifecycle: preStop: exec: command: - /wait-shutdown livenessProbe: failureThreshold: 5 httpGet: path: /healthz port: 10254 scheme: HTTP initialDelaySeconds: 10 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 1 name: controller ports: - containerPort: 80 name: http protocol: TCP - containerPort: 443 name: https protocol: TCP - containerPort: 8443 name: webhook protocol: TCP readinessProbe: failureThreshold: 3 httpGet: path: /healthz port: 10254 scheme: HTTP initialDelaySeconds: 10 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 1 resources: requests: cpu: 100m memory: 90Mi securityContext: allowPrivilegeEscalation: true capabilities: add: - NET_BIND_SERVICE drop: - ALL runAsUser: 101 volumeMounts: - mountPath: /usr/local/certificates/ name: webhook-cert readOnly: true dnsPolicy: ClusterFirst nodeSelector: kubernetes.io/os: linux serviceAccountName: ingress-nginx terminationGracePeriodSeconds: 300 volumes: - name: webhook-cert secret: secretName: ingress-nginx-admission --- apiVersion: batch/v1 kind: Job metadata: labels: app.kubernetes.io/component: admission-webhook app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx-admission-create namespace: ingress-nginx spec: template: metadata: labels: app.kubernetes.io/component: admission-webhook app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx-admission-create spec: containers: - args: - create - --host=ingress-nginx-controller-admission,ingress-nginx-controller-admission.$(POD_NAMESPACE).svc - --namespace=$(POD_NAMESPACE) - --secret-name=ingress-nginx-admission env: - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v20230407@sha256:543c40fd093964bc9ab509d3e791f9989963021f1e9e4c9c7b6700b02bfb227b imagePullPolicy: IfNotPresent name: create securityContext: allowPrivilegeEscalation: false nodeSelector: kubernetes.io/os: linux restartPolicy: OnFailure securityContext: fsGroup: 2000 runAsNonRoot: true runAsUser: 2000 serviceAccountName: ingress-nginx-admission --- apiVersion: batch/v1 kind: Job metadata: labels: app.kubernetes.io/component: admission-webhook app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx-admission-patch namespace: ingress-nginx spec: template: metadata: labels: app.kubernetes.io/component: admission-webhook app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx-admission-patch spec: containers: - args: - patch - --webhook-name=ingress-nginx-admission - --namespace=$(POD_NAMESPACE) - --patch-mutating=false - --secret-name=ingress-nginx-admission - --patch-failure-policy=Fail env: - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v20230407@sha256:543c40fd093964bc9ab509d3e791f9989963021f1e9e4c9c7b6700b02bfb227b imagePullPolicy: IfNotPresent name: patch securityContext: allowPrivilegeEscalation: false nodeSelector: kubernetes.io/os: linux restartPolicy: OnFailure securityContext: fsGroup: 2000 runAsNonRoot: true runAsUser: 2000 serviceAccountName: ingress-nginx-admission --- apiVersion: networking.k8s.io/v1 kind: IngressClass metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: nginx spec: controller: k8s.io/ingress-nginx --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: labels: app.kubernetes.io/component: admission-webhook app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/version: 1.8 .1 name: ingress-nginx-admission webhooks: - admissionReviewVersions: - v1 clientConfig: service: name: ingress-nginx-controller-admission namespace: ingress-nginx path: /networking/v1/ingresses failurePolicy: Fail matchPolicy: Equivalent name: validate.nginx.ingress.kubernetes.io rules: - apiGroups: - networking.k8s.io apiVersions: - v1 operations: - CREATE - UPDATE resources: - ingresses sideEffects: None
https://zhuanlan.zhihu.com/p/421898348
runAsUser: 33 -> runAsUser: 101,和镜像设置的一致
1 2 vmworker02:/etc/nginx$ id uid=101(www-data) gid=82(www-data) groups=82(www-data)
1 2 kubectl get pods -n ingress-nginx \ -l app.kubernetes.io/name=ingress-nginx --watch
1 2 3 4 5 6 root@vmmaster:/# kubectl get pods -n ingress-nginx \ -l app.kubernetes.io/name=ingress-nginx --watch NAME READY STATUS RESTARTS AGE ingress-nginx-admission-create-t6js5 0/1 Completed 0 117s ingress-nginx-admission-patch-6d87z 0/1 Completed 2 117s ingress-nginx-controller-b6f65647c-cn7pk 1/1 Running 0 117s
1 2 3 POD_NAMESPACE=ingress-nginx POD_NAME=$(kubectl get pods -n $POD_NAMESPACE -l app.kubernetes.io/name=ingress-nginx --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') kubectl exec -it $POD_NAME -n $POD_NAMESPACE -- /nginx-ingress-controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 root@vmmaster:/# POD_NAMESPACE=ingress-nginx root@vmmaster:/# POD_NAME=$(kubectl get pods -n $POD_NAMESPACE -l app.kubernetes.io/name=ingress-nginx --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}') root@vmmaster:/# kubectl exec -it $POD_NAME -n $POD_NAMESPACE -- /nginx-ingress-controller ------------------------------------------------------------------------------- NGINX Ingress controller Release: v1.8.1 Build: dc88dce9ea5e700f3301d16f971fa17c6cfe757d Repository: https://github.com/kubernetes/ingress-nginx nginx version: nginx/1.21.6 ------------------------------------------------------------------------------- F0205 17:25:22.016269 94 main.go:67] port 80 is already in use. Please check the flag --http-port command terminated with exit code 255
现在执行:
1 kubectl get all -n ingress-nginx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 root@vmmaster:/# kubectl get all -n ingress-nginx NAME READY STATUS RESTARTS AGE pod/ingress-nginx-admission-create-t6js5 0/1 Completed 0 3m24s pod/ingress-nginx-admission-patch-6d87z 0/1 Completed 2 3m24s pod/ingress-nginx-controller-b6f65647c-cn7pk 1/1 Running 0 3m24s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/ingress-nginx-controller NodePort 10.1.161.232 <none> 80:32149/TCP,443:30908/TCP 3m24s service/ingress-nginx-controller-admission ClusterIP 10.1.121.147 <none> 443/TCP 3m26s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/ingress-nginx-controller 1/1 1 1 3m24s NAME DESIRED CURRENT READY AGE replicaset.apps/ingress-nginx-controller-b6f65647c 1 1 1 3m24s NAME STATUS COMPLETIONS DURATION AGE job.batch/ingress-nginx-admission-create Complete 1/1 12s 3m24s job.batch/ingress-nginx-admission-patch Complete 1/1 29s 3m24s root@vmmaster:/#
5.8 Ingress 实战
步骤 1:创建 Nginx Deployment + Service 资源文件
新建 nginx-deploy-svc.yaml,内容如下(包含 Deployment 部署 Nginx 容器,Service 暴露容器端口):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 apiVersion: apps/v1 kind: Deployment metadata: name: test-nginx-deploy spec: replicas: 1 selector: matchLabels: app: test-nginx template: metadata: labels: app: test-nginx spec: containers: - name: nginx image: nginx:alpine ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: test-nginx-svc spec: selector: app: test-nginx ports: - port: 80 targetPort: 80 type: ClusterIP
步骤 2:创建 Ingress 转发规则资源文件
在当前目录新建 nginx-ingress.yaml,内容如下(核心是匹配 Ingress Controller 的 ingressClassName,并指向上面的 Service):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: test-nginx-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: / spec: ingressClassName: nginx rules: - host: "" http: paths: - path: /to_nginx pathType: Prefix backend: service: name: test-nginx-svc port: number: 80
步骤 3:执行部署命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1. 部署 Nginx Deployment + Service kubectl apply -f nginx-deploy-svc.yaml # 2. 部署 Ingress 规则 kubectl apply -f nginx-ingress.yaml # 3. 检查资源状态(确认都正常) # 检查 Deployment kubectl get deploy test-nginx-deploy # 检查 Pod kubectl get pods -l app=test-nginx # 检查 Service kubectl get svc test-nginx-svc # 检查 Ingress kubectl get ingress test-nginx-ingress
步骤 4:获取 Ingress Controller 的访问端口并测试
首先找到 Ingress Controller 暴露的 NodePort 端口(HTTP 端口):
1 2 # 查看 ingress-nginx 命名空间下的 Service,找 NodePort 端口 kubectl get svc -n ingress-nginx
输出示例(重点看 PORT(S) 列的 NodePort,比如 30080):
1 2 3 4 root@vmmaster:/# kubectl get svc -n ingress-nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE ingress-nginx-controller NodePort 10.1.161.232 <none> 80:32149/TCP,443:30908/TCP 14m ingress-nginx-controller-admission ClusterIP 10.1.121.147 <none> 443/TCP 14m
然后测试访问(替换 <节点IP> 为你的 K8s 节点 IP,<NodePort> 为上面查到的端口):
1 2 3 4 5 6 7 8 9 10 root@vmmaster:/# curl vmmaster:32149/to_nginx <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> html { color-scheme: light dark; } body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } ...
正常会返回 Nginx 的默认首页内容,说明转发成功!
第六章 k8s控制器
6.1 使用 ReplicaSet 控制副本
ReplicaSet 核心定义
ReplicaSet 的目的是维护一组在任何时候都处于运行状态的 Pod 副本的稳定集合。
它通常用来保证给定数量的、完全相同的 Pod 的可用性。
ReplicaSet 的工作原理
ReplicaSet 通过一组字段来定义,包括:
选择符 :用来识别可获得的 Pod 的集合
副本个数数值 :标明应该维护的副本数量
Pod 模板 :指定创建新 Pod 时要使用的模板
ReplicaSet 会根据需要创建和删除 Pod,使副本个数达到期望值。
当需要创建新的 Pod 时,它会使用提供的 Pod 模板。
ReplicaSet 通过 Pod 上的 metadata.ownerReferences 字段连接到附属 Pod,
该字段给出当前对象的属主资源。
ReplicaSet 所管理的 Pod 都会在其 ownerReferences 字段中包含属主 ReplicaSet 的标识信息,
正是通过这一连接,ReplicaSet 才能感知 Pod 集合的状态并执行操作。
如何使用
虽然 ReplicaSet 可以独立使用,但目前它主要被 Deployment 用作协调 Pod 创建、删除和更新的机制。
当使用 Deployment 时,你无需额外管理它创建的 ReplicaSet,Deployment 会直接拥有并管理这些 ReplicaSet。
YAML 配置示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 apiVersion: apps/v1 kind: ReplicaSet metadata: name: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: name: nginx labels: app: nginx spec: containers: - name: nginx image: nginx ports: - containerPort: 80
1 2 3 4 5 6 root@vmmaster:/# kubectl get replicaset NAME DESIRED CURRENT READY AGE nginx 3 3 3 2m39s root@vmmaster:/# kubectl delete pod nginx-qxhc6 pod "nginx-qxhc6" deleted root@vmmaster:/#
1 2 3 4 5 6 7 8 9 10 11 12 13 root@vmmaster:/# kubectl get pod -w NAME READY STATUS RESTARTS AGE nginx-lv78z 1/1 Running 0 3m3s nginx-n5ksw 1/1 Running 0 3m3s nginx-qxhc6 1/1 Running 0 3m3s nginx-qxhc6 1/1 Terminating 0 3m23s nginx-m2g5d 0/1 Pending 0 0s nginx-m2g5d 0/1 Pending 0 0s nginx-m2g5d 0/1 ContainerCreating 0 0s nginx-qxhc6 0/1 Completed 0 3m23s nginx-qxhc6 0/1 Completed 0 3m24s nginx-qxhc6 0/1 Completed 0 3m24s nginx-m2g5d 1/1 Running 0 4s
6.2 深入理解 Deployment
Deployments
一个 Deployment 控制器为 Pods 和 ReplicaSets 提供声明式的更新能力。
你负责描述 Deployment 中的目标状态,而 Deployment 控制器可以更改实际状态,使其变为期望状态。你可以定义 Deployment 以创建新的 ReplicaSet,或删除现有 Deployment,并通过新的 Deployment 适配其资源。
使用场景
以下是 Deployments 的典型使用场景:
创建 Deployment 以将 ReplicaSet 上线。ReplicaSet 在后台创建 Pods。检查 ReplicaSet 的上线状态,查看其是否成功。
通过更新 Deployment 的 PodTemplateSpec,声明 Pod 的新状态。新的 ReplicaSet 会被创建,Deployment 以受控速率将 Pod 从旧 ReplicaSet 迁移到新 ReplicaSet。每个新的 ReplicaSet 都会更新 Deployment 的修订版本。
如果 Deployment 的当前状态不稳定,回滚到较早的 Deployment 版本。每次回滚都会更新 Deployment 的修订版本。
扩大 Deployment 规模以承担更多负载。
暂停 Deployment 以应用对 PodTemplateSpec 所作的多项修改,然后恢复执行以启动新的上线版本。
使用 Deployment 状态来判定上线过程是否出现停滞。
清理较旧的不再需要的 ReplicaSet。
创建 Deployment
下面是 Deployment 示例,其中创建了一个 ReplicaSet,负责启动三个 nginx Pods:nginx-deployment.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80
配置说明
创建名为 nginx-deployment(由 .metadata.name 字段标明)的 Deployment。
该 Deployment 创建三个(由 replicas 字段标明)Pod 副本。
selector 字段定义 Deployment 如何查找要管理的 Pods。在这里,你只需选择在 Pod 模板中定义的标签(app: nginx)。不过,更复杂的选择规则也是可以的,只要 Pod 模板本身满足所给规则即可。
通过命令创建 Deployment
运行以下命令创建 Deployment:
1 kubectl apply -f nginx-deployment.yaml
说明 :你可以设置 --record 标志将所执行的命令写入资源注解 kubernetes.io/change-cause 中。这对于以后的检查是有用的。例如,要查看针对每个 Deployment 修订版本所执行过的命令。
运行 kubectl get deployments 检查 Deployment 是否已创建。如果仍在创建 Deployment,则输出类似于:
1 2 NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE nginx-deployment 3 0 0 0 1s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 root@vmmaster:~/k8s/deployment# vim nginx-deployment.yaml root@vmmaster:~/k8s/deployment# kubectl create -f nginx-deployment.yaml deployment.apps/nginx-deployment created root@vmmaster:~/k8s/deployment# kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE nginx-deployment 3/3 3 3 75s root@vmmaster:~/k8s/deployment# vim nginx-deployment.yaml root@vmmaster:~/k8s/deployment# kubectl apply -f nginx-deployment.yaml Warning: resource deployments/nginx-deployment is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically. deployment.apps/nginx-deployment configured root@vmmaster:~/k8s/deployment# kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE nginx-deployment 1/1 1 1 3m33s root@vmmaster:~/k8s/deployment#
6.3 有状态应用 StatefulSets
一、StatefulSets 基础概念
StatefulSet 是 Kubernetes 中用于管理有状态应用 的工作负载 API 对象。
核心作用 :管理和扩展一组 Pod,并为每个 Pod 提供稳定的序号 和唯一的身份标识 。
与 Deployment 的区别:
相同点:都管理基于相同容器定义的 Pod。
不同点:StatefulSet 为每个 Pod 维护固定且永久不变的 ID,Pod 之间不能相互替换;Deployment 管理的是无状态 Pod,可随意替换。
二、StatefulSets 的适用场景
当应用满足以下一个或多个需求时,适合使用 StatefulSets:
✅ 稳定、唯一的网络标识符 :Pod 重新调度后,网络标识保持不变。
✅ 稳定、持久的存储 :每个 Pod 绑定专属的存储卷,存储不会随 Pod 销毁而丢失。
✅ 有序、优雅的部署和缩放 :Pod 按序号顺序创建(0→N)、删除(N→0)。
✅ 有序、自动的滚动更新 :更新操作按序号顺序执行,确保服务可用性。
如果应用不需要稳定标识或有序操作,建议使用 Deployment 或 ReplicaSet 等无状态控制器。
三、StatefulSets 的限制
存储依赖 :Pod 的存储需由 PersistentVolume 提供,可基于 StorageClass 动态创建,或由管理员预先配置。
存储卷保留 :删除或缩容 StatefulSet 时,关联的存储卷不会被删除,以保障数据安全。
依赖 Headless Service :必须搭配 Headless Service 为 Pod 提供网络标识,需自行创建该服务。
Pod 终止无保证 :直接删除 StatefulSet 不会保证 Pod 有序终止,建议先将副本数缩为 0 再删除。
四、关键配置解析(以 Nginx 示例为例)
Headless Service 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 apiVersion: v1 kind: Service metadata: name: nginx labels: app: nginx spec: ports: - port: 80 name: web clusterIP: None selector: app: nginx
为什么 clusterIP 设置为 None :
普通 Service 会分配一个 ClusterIP,通过负载均衡转发请求到后端 Pod。
clusterIP: None 表示这是一个Headless Service ,它不会分配 ClusterIP,也不提供负载均衡。
作用:让 DNS 直接解析到 Pod 的 IP 地址,从而实现客户端与 Pod 的一对一直接通信 ,这是 StatefulSet 为 Pod 提供稳定网络标识的基础。
适用场景:有状态应用(如数据库集群)需要直接访问特定 Pod,而非通过负载均衡转发。
StatefulSet 核心配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 apiVersion: apps/v1 kind: StatefulSet metadata: name: web spec: selector: matchLabels: app: nginx serviceName: "nginx" replicas: 3 template: metadata: labels: app: nginx spec: terminationGracePeriodSeconds: 10 containers: - name: nginx image: registry.cn-beijing.aliyuncs.com/qingfeng666/nginx ports: - containerPort: 80 name: web volumeMounts: - name: www mountPath: /usr/share/nginx/html volumeClaimTemplates: - metadata: name: www spec: accessModes: [ "ReadWriteOnce" ] storageClassName: "my-storage-class" resources: requests: storage: 1Gi
核心字段说明 :
serviceName: "nginx":指定关联的 Headless Service,确保 Pod 获得稳定的 DNS 名称。
volumeClaimTemplates:为每个 Pod 动态创建专属的 PersistentVolumeClaim(PVC),实现稳定存储。
持久存储后面再介绍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 kind: PersistentVolume apiVersion: v1 metadata: name: datadir1 labels: type: local spec: storageClassName: my-storage-class capacity: storage: 5Gi accessModes: - ReadWriteOnce hostPath: path: "/tmp/data1" --- kind: PersistentVolume apiVersion: v1 metadata: name: datadir2 labels: type: local spec: storageClassName: my-storage-class capacity: storage: 5Gi accessModes: - ReadWriteOnce hostPath: path: "/tmp/data2" --- kind: PersistentVolume apiVersion: v1 metadata: name: datadir3 labels: type: local spec: storageClassName: my-storage-class capacity: storage: 5Gi accessModes: - ReadWriteOnce hostPath: path: "/tmp/data3" --- apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer reclaimPolicy: Delete
五、常用操作命令说明
查看 Pod 详细信息
1 kubectl get pod web-0 -o wide
命令解析 :
kubectl get pod:获取 Pod 列表。
web-0:指定查看名为 web-0 的 Pod(StatefulSet 生成的 Pod 名称格式为 <StatefulSet 名称>-<序号>)。
-o wide:输出更详细的信息,包括 Pod 的节点 IP、Pod IP 等。
用途:用于验证 Pod 的网络标识、运行节点等信息,排查网络或调度问题。
访问 Nginx 服务
直接通过 Pod 的 IP 访问服务,这是 Headless Service 场景下的典型访问方式。
1 2 3 4 5 6 7 8 9 10 11 12 root@vmmaster:/# kubectl get pod web-0 -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES web-0 1/1 Running 0 70s 10.244.1.32 vmworker01 <none> <none> root@vmmaster:/# curl 10.244.1.32 <html> <head><title>403 Forbidden</title></head> <body> <center><h1>403 Forbidden</h1></center> <hr><center>nginx/1.29.5</center> </body> </html> root@vmmaster:/#
6.4 DaemonSet 后台任务
一、DaemonSet 核心概念
DaemonSet 用于确保集群中全部(或指定部分)节点 上都运行一个 Pod 副本。
当新节点加入集群时,会自动为其创建对应的 Pod
当节点从集群移除时,对应的 Pod 会被自动回收
删除 DaemonSet 会删除它创建的所有 Pod
二、典型使用场景
存储守护进程 :在每个节点运行 glusterd、ceph 等存储组件
日志收集 :在每个节点运行 fluentd、logstash 等日志采集工具
监控采集 :在每个节点运行 Prometheus Node Exporter、collectd 等监控组件
三、编写 DaemonSet Spec
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 apiVersion: apps/v1 kind: DaemonSet metadata: name: fluentd-elasticsearch namespace: kube-system labels: k8s-app: fluentd-logging spec: selector: matchLabels: name: fluentd-elasticsearch template: metadata: labels: name: fluentd-elasticsearch spec: tolerations: - key: node-role.kubernetes.io/master effect: NoSchedule containers: - name: fluentd-elasticsearch image: registry.cn-beijing.aliyuncs.com/qingfeng666/fluentd:v2.5.2 resources: limits: memory: 200Mi requests: cpu: 100m memory: 200Mi volumeMounts: - name: varlog mountPath: /var/log - name: varlibdockercontainers mountPath: /var/lib/docker/containers readOnly: true terminationGracePeriodSeconds: 30 volumes: - name: varlog hostPath: path: /var/log - name: varlibdockercontainers hostPath: path: /var/lib/docker/containers
必需字段
和所有 Kubernetes 资源一样,DaemonSet 必须包含:
apiVersion:通常为 apps/v1
kind:固定为 DaemonSet
metadata:包含 name、namespace、labels 等元信息
.spec:核心配置段,其中唯一必需字段是 .spec.template (Pod 模板)
Pod 模板(.spec.template)
是一个内嵌的 Pod 定义,无需指定 apiVersion 和 kind
必须指定合理的标签,且需与 .spec.selector 匹配
必须设置 RestartPolicy: Always(默认值即为 Always)
Pod 选择器(.spec.selector)
从 Kubernetes 1.8 开始为必填项,且必须与 .spec.template.metadata.labels 完全匹配,否则会被 API 拒绝
由 matchLabels(简单标签匹配)和 matchExpressions(复杂条件匹配)组成,两者同时存在时为逻辑与(AND)关系
一旦创建 DaemonSet,.spec.selector 就无法修改 ,否则可能导致 Pod 意外悬浮
仅在部分节点运行 Pod
通过 .spec.template.spec.nodeSelector 选择匹配标签的节点
通过 .spec.template.spec.affinity 配置节点亲和性来选择节点
若不指定以上配置,DaemonSet 会在所有节点 上创建 Pod
6.5 Daemon Pods 的调度
Daemon Pods 的调度
DaemonSet 的核心作用是确保所有符合条件的节点都运行一个 Pod 副本,它的调度方式有两种:
传统调度方式
由 DaemonSet 控制器直接创建和调度 Pod
问题:
Pod 行为不一致 :普通 Pod 创建后会进入 Pending 状态等待调度,而 DaemonSet Pod 不会,容易让用户产生困惑。
抢占能力缺失 :Pod 抢占功能由默认调度器处理,DaemonSet 控制器调度时不考虑 Pod 优先级和抢占规则。
使用默认调度器(推荐)
通过开启 ScheduleDaemonSetPods 特性,让默认调度器来调度 DaemonSet Pod。
实现方式:DaemonSet 控制器会给 Pod 添加 NodeAffinity 条件(而非直接设置 .spec.nodeName),由默认调度器完成最终绑定。
示例配置:
1 2 3 4 5 6 7 8 nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchFields: - key: metadata.name operator: In values: - target-host-name
系统会自动为 DaemonSet Pod 添加容忍度:node.kubernetes.io/unschedulable: NoSchedule,确保它能被调度到不可调度节点上。
与 Daemon Pods 通信的方式
NodeIP + 已知端口
Pod 使用
暴露端口,客户端通过节点 IP 列表访问对应端口上的 Pod。
DNS 发现
创建一个
无头服务
(ClusterIP: None),通过
资源或 DNS 的 A 记录,发现所有 DaemonSet Pod。
Service 访问
创建普通 Service,通过 Service 随机访问到某个节点上的 Daemon Pod,但无法指定特定节点。
更新 DaemonSet
节点标签变化
当节点标签被修改时,DaemonSet 会自动在新匹配的节点上创建 Pod,并删除不再匹配的节点上的 Pod。
Pod 模板更新
你可以修改 DaemonSet 创建的 Pod,但并非所有字段都支持更新;新节点创建 Pod 时,仍会使用最初的模板。
删除 DaemonSet
直接删除:关联的 Pod 会被一起删除。
使用 kubectl delete daemonset <name> --cascade=false:仅删除 DaemonSet 控制器,Pod 会被保留在节点上。
新的、使用相同选择器的 DaemonSet 可以 “收养” 这些被保留的 Pod。
更新策略
Pod 需要被替换时,DaemonSet 会根据其
字段定义的策略来执行替换。
DaemonSet 的替代方案
直接在节点上用 init、upstartd 或 systemd 等工具运行守护进程是一种替代方案,但 DaemonSet 有明显优势:
统一运维 :和其他应用一样,提供监控、日志管理能力。
工具链一致 :使用 Pod 模板、kubectl 等 Kubernetes 原生工具,配置语言和体验统一。
隔离性更好 :守护进程运行在资源受限的容器中,与应用容器天然隔离。
6.6 Job
一、Job 核心概念
Job 是 Kubernetes 中用于运行一次性任务的资源对象,其核心特点如下:
任务目标 :创建一个或多个 Pod,并确保指定数量的 Pod 成功终止。
完成机制 :跟踪成功完成的 Pod 数量,当达到预设的成功个数阈值时,任务即结束。
失败恢复 :当 Pod 失败或被删除(如节点故障、重启)时,Job 会自动启动新的 Pod 来重试。
清理机制 :删除 Job 时,会自动清理其创建的所有 Pod。
运行模式 :既可以运行单个 Pod 直到完成,也可以并行运行多个 Pod。
二、运行示例解析
这个示例是一个计算 π 值到小数点后 100 位的 Job,配置文件解读如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 apiVersion: batch/v1 kind: Job metadata: name: pi spec: template: spec: containers: - name: pi image: registry.cn-beijing.aliyuncs.com/google_registry/perl:5.26 command: ["perl" , "-Mbignum=bpi" , "-wle" , "print bpi(100)" ] restartPolicy: Never backoffLimit: 4
三、Job 典型应用场景
数据处理 :一次性的数据导出、ETL 任务。
批量计算 :科学计算、模型训练等一次性计算任务。
定时任务 :与 CronJob 结合,实现周期性运行的任务。
第七章 k8s持久化存储
7.1 Kubernetes 的卷 Volume
卷
容器中的文件在磁盘上是临时存放的,这给容器中运行的特殊应用程序带来一些问题:
当容器崩溃时,kubelet 将重新启动容器,容器中的文件将会丢失 —— 因为容器会以干净的状态重建。
当在一个 Pod 中同时运行多个容器时,常常需要在这些容器之间共享文件。
Kubernetes 抽象出 Volume 对象来解决这两个问题。
背景
Docker 也有 Volume 的概念,但对它只有少量且松散的管理。在 Docker 中,Volume 是磁盘上或者另外一个容器内的一个目录。直到最近,Docker 才支持对基于本地磁盘的 Volume 的生存期进行管理。虽然 Docker 现在也能提供 Volume 驱动程序,但是目前功能还非常有限(例如,截至 Docker 1.7,每个容器只允许有一个 Volume 驱动程序,并且无法将参数传递给卷)。
另一方面,Kubernetes 卷具有明确的生命周期 —— 与包裹它的 Pod 相同。因此,卷比 Pod 中运行的任何容器的存活期都长,在容器重新启动时数据也会得到保留。当然,当一个 Pod 不再存在时,卷也将不再存在。也许更重要的是,Kubernetes 可以支持许多类型的卷,Pod 也能同时使用任意数量的卷。
卷的核心是包含一些数据的目录,Pod 中的容器可以访问该目录。特定的卷类型可以决定这个目录如何形成的,并能决定它支持何种介质,以及目录中存放什么内容。
使用卷时,Pod 声明中需要提供卷的类型(spec.volumes 字段)和卷挂载的位置(spec.containers.volumeMounts 字段)。
emptyDir
当 Pod 指定到某个节点上时,首先创建的是一个 emptyDir 卷,并且只要 Pod 在该节点上运行,卷就一直存在。就像它的名称表示的那样,卷最初是空的。尽管 Pod 中的容器挂载 emptyDir 卷的路径可能相同也可能不同,但是这些容器都可以读写 emptyDir 卷中相同的文件。当 Pod 因为某些原因被从节点上删除时,emptyDir 卷中的数据也会永久删除。
说明 :容器崩溃并不会导致 Pod 被从节点上移除,因此容器崩溃时 emptyDir 卷中的数据是安全的。
emptyDir 的一些用途
缓存空间,例如基于磁盘的归并排序。
为耗时较长的计算任务提供检查点,以便任务能方便地从崩溃前状态恢复执行。
在 Web 服务器容器服务数据时,保存内容管理器类型容器获取的文件。
hostPath
hostPath 卷能将主机节点文件系统上的文件或目录挂载到 Pod 中。虽然这不是大多数 Pod 需要的,但是它为一些应用程序提供了强大的持久化能力。
示例配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 apiVersion: v1 kind: Pod metadata: name: hostpath-pod spec: containers: - name: test-container image: nginx volumeMounts: - mountPath: /test-nginx name: myhostpath volumes: - name: myhostpath hostPath: path: /tmp/nginx type: DirectoryOrCreate
验证:
进入容器挂载目录,建一个文件
1 2 3 4 5 6 7 8 9 10 11 root@vmmaster:/# kubectl get po NAME READY STATUS RESTARTS AGE hostpath-pod 1/1 Running 0 4m28s root@vmmaster:/# kubectl exec -it hostpath-pod -- sh # cd test-nginx # pwd /test-nginx # ls # touch test_file # ls test_file
在宿主机上检查
1 2 3 4 5 6 root@vmworker01:/# cd /tmp/nginx root@vmworker01:/tmp/nginx# ll total 8 drwxr-xr-x 2 root root 4096 Feb 7 15:18 ./ drwxrwxrwt 17 root root 4096 Feb 7 15:15 ../ -rw-r--r-- 1 root root 0 Feb 7 15:18 test_file
7.2 实战挂载 NFS 卷(Ubuntu 版本)
一、应用场景
很多应用需要在集群内部有一个统一的地方存储文件(如图片、日志等)。
使用 hostPath 方式不够灵活,因为它需要指定具体的节点地址。
而 NFS 可以提供集群内共享的存储,满足跨节点文件访问需求。
二、挂载 NFS 卷步骤(Ubuntu 适配版)
在 Master(vmmaster)和 Worker(vmworker01/vmworker02)节点安装 NFS 服务
1 2 3 4 5 6 7 # 更新软件源(可选,确保能找到最新包) sudo apt update # 安装 NFS 相关工具(master 需完整安装,worker 安装客户端即可) # 在 vmmaster 节点执行 sudo apt install -y nfs-kernel-server nfs-common # 在 vmworker01、vmworker02 节点执行 sudo apt install -y nfs-common
创建共享目录并修改 NFS 配置(仅在 vmmaster 执行)
1 2 3 4 5 6 # 1. 创建共享目录 sudo mkdir -p /nfsdata # 2. 修改目录权限(确保所有客户端可读写) sudo chmod 777 /nfsdata # 3. 编辑 NFS 配置文件 sudo vi /etc/exports
添加以下共享目录配置(和 CentOS 核心参数一致,Ubuntu 兼容):
1 /nfsdata *(rw,sync,no_root_squash,no_subtree_check)
参数说明:
rw:读写权限
sync:同步写入,保证数据不丢失
no_root_squash:允许客户端以 root 身份访问共享目录
no_subtree_check:关闭子目录检查(Ubuntu 推荐添加,提升稳定性)
在 vmmaster 节点启动并启用 NFS 服务
Ubuntu 使用 systemd 管理服务,命令和 CentOS 略有差异,但核心逻辑一致:
1 2 3 4 5 6 # 重新加载 NFS 配置(修改 exports 后必须执行) sudo exportfs -r # 启动并设置开机自启 NFS 服务 sudo systemctl enable --now nfs-server # 验证 NFS 服务状态 sudo systemctl status nfs-server
验证 NFS 共享(可选,确保集群节点可访问)
在任意 worker 节点(如 vmworker01)执行,测试能否挂载 master 的 NFS 目录:
1 2 3 4 5 6 7 8 # 临时挂载测试 sudo mount -t nfs vmmaster:/nfsdata /mnt # 写入测试文件 sudo touch /mnt/test.txt # 查看文件(能看到则说明共享正常) ls /mnt # 卸载临时挂载 sudo umount /mnt
创建 Pod 引用 NFS 存储
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 apiVersion: v1 kind: Pod metadata: name: nfs-pd spec: containers: - name: test-container image: nginx volumeMounts: - mountPath: /usr/share/nginx/html name: test-volume volumes: - name: test-volume nfs: server: vmmaster path: /nfsdata
执行命令部署 Pod:
1 2 3 4 5 6 7 8 9 10 11 kubectl apply -f 8-2-nfs.yaml # 验证 Pod 状态 kubectl get pods nfs-pd # 进入 Pod 验证挂载是否成功 kubectl exec -it nfs-pd -- bash # 在 Pod 内查看挂载点 df -h | grep /usr/share/nginx/html # 写入文件测试 echo "NFS Test" > /usr/share/nginx/html/index.html # 退出 Pod 后,在 vmmaster 的 /nfsdata 目录查看文件(能看到则挂载成功) sudo ls /nfsdata
补充注意事项
确保集群节点间(vmmaster/vmworker01/vmworker02)能通过 hostname 互通(可修改 /etc/hosts 配置)。
Ubuntu 防火墙(ufw)若开启,需放行 NFS 相关端口(111、2049),否则会导致挂载失败。
NFS 服务重启后,需执行 exportfs -r 重新加载配置。
7.3 持久化存储 PersistentVolume
介绍
存储的管理是一个与计算实例的管理完全不同的问题。PersistentVolume 子系统为用户和管理员提供了一组 API,将存储如何供应的细节从其如何被使用中抽象出来。为了实现这点,引入了两个新的 API 资源:PersistentVolume 和 PersistentVolumeClaim。
持久卷(PersistentVolume,PV)是集群中的一块存储,可以由管理员事先供应,或者使用存储类(Storage Class)来动态供应。持久卷是集群资源,就像节点也是集群资源一样。PV 持久卷和普通的 Volume 一样,也是使用卷插件来实现的,只是它们拥有独立于任何使用 PV 的 Pod 的生命周期。此 API 对象中记述了存储的实现细节,无论其背后是 NFS、iSCSI 还是特定于云平台的存储系统。
PV 配置示例
1 2 3 4 5 6 7 8 9 10 11 12 13 apiVersion: v1 kind: PersistentVolume metadata: name: pv-demo spec: capacity: storage: 5Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Recycle nfs: path: /nfsdata server: master
持久卷申领(PersistentVolumeClaim,PVC)
持久卷申领(PersistentVolumeClaim,PVC)表达的是用户对存储的请求。概念上与 Pod 类似:
Pod 会耗用节点资源,而 PVC 申领会耗用 PV 资源
Pod 可以请求特定数量的资源(CPU 和内存);同样 PVC 申领也可以请求特定的大小和访问模式(例如,可以要求 PV 卷能够以 ReadWriteOnce、ReadOnlyMany 或 ReadWriteMany 模式之一来挂载,参见访问模式)
尽管 PersistentVolumeClaim 允许用户消耗抽象的存储资源,常见的情况是针对不同的问题用户需要的是具有不同属性(如,性能)的 PersistentVolume 卷。集群管理员需要能够提供不同性质的 PersistentVolume,并且这些 PV 卷之间的差别不仅限于卷大小和访问模式,同时又不能将卷是如何实现的这些细节暴露给用户。为了满足这类需求,就有了存储类(StorageClass)资源
7.4 PVC 持久化卷 Claim(PersistentVolumeClaims)
每个 PVC 对象都包含 spec 和 status 两部分,分别对应 Claim 的规约 和状态 。
基础 PVC 示例
1 2 3 4 5 6 7 8 9 10 kind: PersistentVolumeClaim apiVersion: v1 metadata: name: pvc-demo spec: accessModes: - ReadWriteOnce resources: requests: storage: 2Gi
1 2 3 4 5 6 root@vmmaster:/# kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE pv-demo 5Gi RWO Recycle Bound default/pvc-demo <unset> 13m root@vmmaster:/# kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE pvc-demo Bound pv-demo 5Gi RWO <unset> 68s
PVC 核心配置项
访问模式
Claim 在请求具有特定访问模式的存储时,使用与卷相同的访问模式约定。
卷模式
Claim 使用与卷相同的约定来表明是将卷作为文件系统 还是块设备 来使用。
资源
Claim 和 Pod 一样,也可以请求特定数量的资源。在这个上下文中,请求的资源是存储。卷和 Claim 都使用相同的资源模型。
选择算符
Claim 可以设置标签选择算符来进一步过滤卷集合。只有标签与选择算符相匹配的卷能够绑定到 Claim 上。
选择算符包含两个字段:
matchLabels :卷必须包含带有此值的标签
matchExpressions :通过设定键(key)、值列表和操作符(operator)来构造的需求。合法的操作符有 In、NotIn、Exists 和 DoesNotExist
来自 matchLabels 和 matchExpressions 的所有需求都按逻辑与 的方式组合在一起,这些需求都必须被满足才被视为匹配。
类(StorageClass)
Claim 可以通过为 storageClassName 属性设置 StorageClass 的名称来请求特定的存储类。只有属于所请求的类的 PV 卷(即 storageClassName 值与 PVC 设置相同的 PV 卷)才能绑定到 PVC Claim。
如果 PVC 的 storageClassName 属性值设置为 "",则被视为要请求的是没有设置存储类的 PV 卷。
未设置 storageClassName 的 PVC 会被集群作不同处理,具体筛选方式取决于 DefaultStorageClass 准入控制器插件是否被启用。
当某 PVC 除了请求 StorageClass 之外还设置了 selector,则这两种需求会按逻辑与 关系处理:只有隶属于所请求类且带有所请求标签的 PV 才能绑定到 PVC。
完整 PVC 示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 apiVersion: v1 kind: PersistentVolumeClaim metadata: name: myclaim spec: accessModes: - ReadWriteOnce volumeMode: Filesystem resources: requests: storage: 8Gi storageClassName: slow selector: matchLabels: release: "stable" matchExpressions: - {key: environment , operator: In , values: [dev ]}
PV 是一整块、不能切、不能分。
PVC 只要满足 “容量 ≥ 我要的”,就直接把整个 PV 拿走 ,不是只拿一部分。
7.5 存储类 Storage Class
什么是 StorageClass
Kubernetes 提供了一套可以自动创建 PV 的机制,即 Dynamic Provisioning 。这个机制的核心就是 StorageClass 这个 API 对象。
StorageClass 对象会定义两部分内容:
PV 的属性 :比如存储类型、Volume 的大小等。
创建这种 PV 需要用到的存储插件 (Provisioner)。
为什么需要 StorageClass
在大规模的 Kubernetes 集群中,可能存在成千上万的 PVC。这会带来两个主要问题:
运维负担重 :运维人员需要提前创建大量 PV,并且在有新 PVC 提交时持续添加新的 PV,否则 Pod 会因 PVC 无法绑定 PV 而创建失败。
存储需求不匹配 :不同应用对存储性能(如读写速度、并发能力)的要求不同,仅通过 PVC 请求固定容量的存储无法满足这些差异化需求。
StorageClass 的引入解决了这些问题:
管理员可以将存储资源定义为不同类型(如快速存储、慢速存储),用户可以根据应用特性申请合适的存储。
它通过动态供应(Dynamic Provisioning)自动创建 PV,无需人工提前准备。
StorageClass 核心特性
命名重要性 :用户会通过 StorageClass 的名称来请求特定类型的存储。
不可更新 :StorageClass 对象创建后,其名称和其他参数无法再更新。
默认存储类 :管理员可以为未指定 storageClassName 的 PVC 设置一个默认的存储类。
StorageClass 配置示例
1 2 3 4 5 6 7 apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: managed-nfs-storage provisioner: gavin-nfs-storage parameters: archiveOnDelete: "false"
7.6 实战 PV,PVC 挂载 NFS
创建用户和角色 RBAC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 apiVersion: v1 kind: ServiceAccount metadata: name: nfs-client-provisioner namespace: default --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: nfs-client-provisioner-runner rules: - apiGroups: ["" ] resources: ["persistentvolumes" ] verbs: ["get" , "list" , "watch" , "create" , "delete" ] - apiGroups: ["" ] resources: ["persistentvolumeclaims" ] verbs: ["get" , "list" , "watch" , "update" ] - apiGroups: ["storage.k8s.io" ] resources: ["storageclasses" ] verbs: ["get" , "list" , "watch" ] - apiGroups: ["" ] resources: ["events" ] verbs: ["create" , "update" , "patch" ]--- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: run-nfs-client-provisioner subjects: - kind: ServiceAccount name: nfs-client-provisioner namespace: default roleRef: kind: ClusterRole name: nfs-client-provisioner-runner apiGroup: rbac.authorization.k8s.io --- kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: leader-locking-nfs-client-provisioner namespace: default rules: - apiGroups: ["" ] resources: ["endpoints" ] verbs: ["get" , "list" , "watch" , "create" , "update" , "patch" ]--- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: leader-locking-nfs-client-provisioner subjects: - kind: ServiceAccount name: nfs-client-provisioner namespace: default roleRef: kind: Role name: leader-locking-nfs-client-provisioner apiGroup: rbac.authorization.k8s.io
创建 Persistent Volume Provisioner
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 apiVersion: apps/v1 kind: Deployment metadata: name: nfs-client-provisioner labels: app: nfs-client-provisioner namespace: default spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: nfs-client-provisioner template: metadata: labels: app: nfs-client-provisioner spec: serviceAccountName: nfs-client-provisioner containers: - name: nfs-client-provisioner image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2 volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes env: - name: PROVISIONER_NAME value: gavin-nfs-storage - name: NFS_SERVER value: vmmaster - name: NFS_PATH value: /nfsdata volumes: - name: nfs-client-root nfs: server: vmmaster path: /nfsdata
创建Storage Class
1 2 3 4 5 6 7 apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: managed-nfs-storage provisioner: gavin-nfs-storage parameters: archiveOnDelete: "false"
创建pvc
1 2 3 4 5 6 7 8 9 10 11 12 kind: PersistentVolumeClaim apiVersion: v1 metadata: name: test-claim annotations: volume.beta.kubernetes.io/storage-class: "managed-nfs-storage" spec: accessModes: - ReadWriteMany resources: requests: storage: 1Mi
测试创建pod,引用这个Storage Class (pvc)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 kind: Pod apiVersion: v1 metadata: name: test-pod spec: containers: - name: test-pod image: nginx command: - "/bin/sh" args: - "-c" - "touch /mnt/SUCCESS && exit 0 || exit 1" volumeMounts: - name: nfs-pvc mountPath: "/mnt" restartPolicy: "Never" volumes: - name: nfs-pvc persistentVolumeClaim: claimName: test-claim
1 2 3 root@vmmaster:/# cd /nfsdata/ root@vmmaster:/nfsdata# ls default-test-claim-pvc-34d2e41b-2d7a-423d-a330-c0d0289e29a2
第八章 k8s之应用与配置分离
8.1 ConfigMap 概念和实战
一、基本概念
ConfigMap 是 Kubernetes 中的一种 API 对象,用于将非机密性数据 存储在键值对中。
它的核心作用是将环境配置信息与容器镜像解耦 ,让应用配置的修改更灵活,无需重新构建镜像。
⚠️ 注意:ConfigMap 不提供保密或加密功能,机密信息请使用 Secret 对象存储。
二、设计来源
使用 ConfigMap 可以将配置数据与应用程序代码分离 。
例如:
本地开发时,环境变量 DATABASE_HOST 设置为 localhost
云上部署时,该变量指向 Kubernetes 集群中的数据库 Service
这样就可以在本地和云上复用同一套代码,仅通过配置切换环境
三、ConfigMap 对象结构
核心字段 :和其他 Kubernetes 对象不同,ConfigMap 使用 data 块存储键值对,而非 spec。
命名规则 :ConfigMap 的名字必须是合法的 DNS 子域名。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 apiVersion: v1 kind: ConfigMap metadata: name: game-demo data: player_initial_lives: "3" ui_properties_file_name: "user-interface.properties" game.properties: | enemy.types=aliens,monsters player.maximum-lives=5 user-interface.properties: | color.good=purple color.bad=yellow allow.textmode=true
四、ConfigMap 与 Pod 的关联
Pod 与引用的 ConfigMap 必须处于同一个命名空间 中。
你可以通过四种方式在 Pod 中使用 ConfigMap 配置容器:
容器 entrypoint 的命令行参数
容器的环境变量
在只读卷中添加文件,供应用读取
编写代码,在 Pod 中通过 Kubernetes API 读取 ConfigMap
💡 前三种方式由 kubelet 在启动容器时注入配置;
第四种需要代码实现,但支持订阅 ConfigMap 更新,且可跨命名空间访问。
通过环境变量方式使用 ConfigMap
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 apiVersion: v1 kind: Pod metadata: name: pod-test spec: containers: - name: test image: busybox command: ["/bin/sh" , "-c" , "env" ] envFrom: - configMapRef: name: game-demo restartPolicy: Never
⚠️ 注意:环境变量名受 POSIX 命名规范约束,不能以数字开头。
若配置包含非法字符,系统会跳过该环境变量的创建,并记录 Event,但不会阻止 Pod 启动。
8.2 Secret 密钥管理实战
一、Secret 概述
Secret 是 Kubernetes 中用于存储敏感信息的资源对象,比如密码、OAuth 令牌和 SSH 密钥。
将敏感信息放在 Secret 中,比直接写在 Pod 定义或容器镜像里更安全、更灵活。
Pod 可以通过以下三种方式使用 Secret:
作为卷挂载到容器中 :以文件形式被容器访问
作为容器的环境变量 :直接注入到容器进程的环境里
由 kubelet 在拉取镜像时使用 :用于访问私有镜像仓库的认证
二、创建自己的 Secret
从文件创建
1 2 3 4 5 6 7 8 9 10 11 12 13 echo -n 'admin' > ./username.txt echo -n '1f2d1e2e67df' > ./password.txt kubectl create secret generic db-user-pass \ --from-file=./username.txt \ --from-file=./password.txt kubectl create secret generic db-user-pass \ --from-file=username=./username.txt \ --from-file=password=./password.txt
从字面量直接创建
当密码包含特殊字符时,用单引号包裹即可:
1 2 3 kubectl create secret generic dev-db-secret \ --from-literal=username=devuser \ --from-literal=password='S!B\*d$zDsb='
查看 Secret
1 2 3 4 5 # 查看所有 Secret kubectl get secrets # 查看 Secret 详情(默认不显示敏感内容) kubectl describe secrets db-user-pass
三、在 Pod 中使用 Secret
方式 1:以卷的形式挂载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 apiVersion: v1 kind: Pod metadata: name: secret-pod spec: containers: - name: my-container image: nginx volumeMounts: - name: secret-volume mountPath: /etc/secret readOnly: true volumes: - name: secret-volume secret: secretName: db-user-pass
挂载后,容器内 /etc/secret/username.txt 和 /etc/secret/password.txt 文件会包含对应的敏感数据。
方式 2:作为环境变量注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 apiVersion: v1 kind: Pod metadata: name: secret-env-pod spec: containers: - name: my-container image: nginx env: - name: DB_USERNAME valueFrom: secretKeyRef: name: db-user-pass key: username.txt - name: DB_PASSWORD valueFrom: secretKeyRef: name: db-user-pass key: password.txt
容器内可以直接通过 $DB_USERNAME 和 $DB_PASSWORD 环境变量获取敏感信息。
方式 3:用于拉取私有镜像
1 2 3 4 5 6 7 8 9 10 apiVersion: v1 kind: Pod metadata: name: private-reg-pod spec: containers: - name: my-container image: private-registry.com/my-image:v1 imagePullSecrets: - name: my-registry-secret
这个 my-registry-secret 是提前创建好的、包含镜像仓库认证信息的 Secret。
8.3 k8s 配置管理与最佳实践总结
一、普通配置
API 版本 :定义配置时,请指定最新的稳定 API 版本。
版本控制 :在推送到集群之前,配置文件应存储在版本控制中,以便快速回滚、集群重建和恢复。
配置文件格式 :优先使用 YAML 而不是 JSON 编写配置文件,YAML 更用户友好。
文件组织 :只要有意义,就将相关对象分组到一个文件中,单个文件比多个文件更容易管理。
批量操作 :可以在目录上直接调用 kubectl apply 等命令,批量处理配置文件。
二、Pod 与控制器
避免独立 Pod :尽量不要使用未绑定到 ReplicaSet 或 Deployment 的独立 Pod(Naked Pods),因为节点故障时不会自动重新调度。
优先使用 Deployment :Deployment 会创建 ReplicaSet 来确保所需数量的 Pod 始终可用,并支持滚动更新等策略。除 restartPolicy: Never 场景外,优先使用 Deployment 而非直接创建 Pod。
Job 适用场景 :对于一次性任务,Job 也是合适的选择。
三、服务(Service)
创建顺序 :必须在需要访问服务的 Pod 之前创建服务,否则 Pod 启动时无法注入服务的环境变量(DNS 方式无此限制)。
DNS 插件 :强烈推荐部署 DNS 服务器插件,它会为新服务自动创建 DNS 记录,让所有 Pod 能自动解析服务名称。
端口与网络:
除非必要,否则不要为 Pod 指定 hostPort,因为它会限制 Pod 调度能力(每个 <hostIP, hostPort, protocol> 组合必须唯一)。
调试端口可使用 apiserver proxy 或 kubectl port-forward。
若需在节点上公开 Pod 端口,优先考虑 NodePort 服务而非 hostPort。
避免使用 hostNetwork,原因与 hostPort 类似。
无头服务 :当不需要 kube-proxy 负载均衡时,可使用无头服务(ClusterIP 设置为 None)来实现服务发现。
四、标签(Labels)
语义化标签 :定义并使用标签来识别应用或 Deployment 的语义属性(如 app: myapp, tier: frontend, phase: test),以便资源选择和管理。
跨 Deployment 服务 :通过在选择器中省略特定版本标签,可让服务跨越多个 Deployment,实现 Deployment 不停机更新。
Deployment 期望状态 :Deployment 描述了对象的期望状态,控制器会以受控速率将实际状态调整为期望状态。
五、容器镜像
镜像拉取策略(imagePullPolicy):
IfNotPresent:仅当镜像本地不存在时才拉取。
Always:每次启动 Pod 时都会拉取镜像。
Never:假设镜像已在本地,不会尝试拉取。
省略时,若镜像标签为 :latest 或不存在,默认使用 Always;若指定了非 :latest 标签,默认使用 IfNotPresent。
生产环境建议 :避免使用 :latest 标签,因为难以跟踪版本和回滚。
镜像缓存优化 :底层镜像驱动(如 Docker)的缓存机制,能让 imagePullPolicy: Always 的配置也保持高效。
六、kubectl 使用
批量应用配置 :使用 kubectl apply -f <目录>,它会自动查找目录中所有 .yaml、.yml 和 .json 配置文件并应用。
标签选择器 :使用标签选择器进行 get 和 delete 操作,而非指定具体对象名称,更灵活高效。
第九章 k8s容器镜像中心
本章学习目标
熟悉 Docker 镜像中心的概念并掌握 DockerHub 的使用
学会搭建私有 Docker 镜像中心
9.1 深入理解容器镜像中心
DockerHub 介绍
DockerHub 是官方的公共 Docker 镜像仓库,地址:https://registry.hub.docker.com/
配置国内 Docker 镜像源
国内镜像源可以显著提升镜像拉取速度,常见的有:(有可能失效,docker镜像源变化很快)
网易镜像:https://hub-mirror.c.163.com
阿里云镜像(需自行获取个人加速地址)
七牛云镜像:https://reg-mirror.qiniu.com
配置步骤 :
编辑 Docker 配置文件
1 vi /etc/docker/daemon.json
在配置文件中添加镜像源地址
1 2 3 4 { "registry-mirrors": ["https://registry.docker-cn.com"], "live-restore": true }
说明:live-restore: true 表示重启 Docker 服务时不停止正在运行的容器
重启 Docker 服务使配置生效
1 systemctl restart docker
推送镜像到 DockerHub
登录 DockerHub
执行后输入你的 DockerHub 用户名和密码,显示 Login Succeeded 即登录成功。
1 2 3 Username: wangqingjiewa Password: Login Succeeded
为镜像打标签
格式:docker tag [本地镜像名:版本] [DockerHub用户名]/[镜像名:版本]
1 docker tag kubeblog:1.0 wangqingjiewa/kubeblog:1.0
推送镜像到 DockerHub
1 docker push wangqingjiewa/kubeblog:1.0
补充:拉取镜像
从 DockerHub 拉取镜像的基础命令:
1 2 3 docker pull [镜像名:版本] # 例如 docker pull nginx:latest
如果使用了国内镜像源,这个拉取操作会自动从配置的镜像源获取。
dockerhub只能有一个私有的镜像仓库,公开的没限制,所以可以考虑一下腾讯云或者阿里云
9.2 扩容&隔离jfrog数据(可选)
当前逻辑卷只分配了11.5GB,安装jfrog会报错,所以我们需要先对lvm做扩容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 gavin@vmmaster:~$ gavin@vmmaster:~$ sudo fdisk -l [sudo] password for gavin: Disk /dev/sda: 25 GiB, 26843545600 bytes, 52428800 sectors Disk model: VBOX HARDDISK Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: gpt Disk identifier: 743E14DE-DB3B-4A00-A70C-ED1173FE0B81 Device Start End Sectors Size Type /dev/sda1 2048 4095 2048 1M BIOS boot /dev/sda2 4096 4198399 4194304 2G Linux filesystem /dev/sda3 4198400 52426751 48228352 23G Linux filesystem Disk /dev/sdb: 40 GiB, 42949672960 bytes, 83886080 sectors Disk model: VBOX HARDDISK Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disk /dev/mapper/ubuntu--vg-ubuntu--lv: 11.5 GiB, 12343836672 bytes, 24109056 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes gavin@vmmaster:~$
直接复制这 3 条命令,依次执行:
1 2 3 4 5 6 7 8 # 1. 扩展物理卷(确认 LVM 物理卷可用空间) sudo pvresize /dev/sda3 # 2. 给根逻辑卷增加 5GB 空间 sudo lvextend -L +5G /dev/mapper/ubuntu--vg-ubuntu--lv # 3. 在线调整文件系统大小(让系统识别扩容后的空间) sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
执行完查看结果:
你会看到根分区从 11.5G → 16.5G 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 gavin@vmmaster:~$ sudo pvresize /dev/sda3 Physical volume "/dev/sda3" changed 1 physical volume(s) resized or updated / 0 physical volume(s) not resized gavin@vmmaster:~$ sudo lvextend -L +5G /dev/mapper/ubuntu--vg-ubuntu--lv Size of logical volume ubuntu-vg/ubuntu-lv changed from <11.50 GiB (2943 extents) to <16.50 GiB (4223 extents). Logical volume ubuntu-vg/ubuntu-lv successfully resized. gavin@vmmaster:~$ sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv resize2fs 1.47.0 (5-Feb-2023) Filesystem at /dev/mapper/ubuntu--vg-ubuntu--lv is mounted on /; on-line resizing required old_desc_blocks = 2, new_desc_blocks = 3 The filesystem on /dev/mapper/ubuntu--vg-ubuntu--lv is now 4324352 (4k) blocks long. gavin@vmmaster:~$ df -h / Filesystem Size Used Avail Use% Mounted on /dev/mapper/ubuntu--vg-ubuntu--lv 17G 8.0G 7.5G 52% /
接下来:把 40GB 新盘(sdb)挂载给 JFrog 做持久化存储
你新加的盘是 /dev/sdb,40GB,专门给 JFrog 存镜像、层、缓存。
1)分区(整个盘分一个区)
按顺序输入:
2)格式化
1 sudo mkfs.ext4 /dev/sdb1
3)创建 JFrog 数据目录
1 sudo mkdir -p /var/opt/jfrog/artifactory
4)挂载
1 sudo mount /dev/sdb1 /var/opt/jfrog/artifactory
5)设置开机自动挂载(非常重要)
获取 UUID:
编辑 fstab:
加一行(把 UUID 换成你自己的):
1 UUID=你的UUID /var/opt/jfrog/data ext4 defaults 0 2
测试挂载:如果没有任何报错,说明配置没问题;如果报错,大概率是 UUID 输错了,核对一下即可。
6)给 JFrog 权限(Docker 版必须)
1 sudo chown -R 1030:1030 /var/opt/jfrog/artifactory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 gavin@vmmaster:~$ sudo fdisk /dev/sdb Welcome to fdisk (util-linux 2.39.3). Changes will remain in memory only, until you decide to write them. Be careful before using the write command. Device does not contain a recognized partition table. Created a new DOS (MBR) disklabel with disk identifier 0x54c49b75. Command (m for help): n Partition type p primary (0 primary, 0 extended, 4 free) e extended (container for logical partitions) Select (default p): p Partition number (1-4, default 1): 1 First sector (2048-83886079, default 2048): Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-83886079, default 83886079): Created a new partition 1 of type 'Linux' and of size 40 GiB. Command (m for help): w The partition table has been altered. Calling ioctl() to re-read partition table. Syncing disks. gavin@vmmaster:~$ sudo mkfs.ext4 /dev/sdb1 mke2fs 1.47.0 (5-Feb-2023) Creating filesystem with 10485504 4k blocks and 2621440 inodes Filesystem UUID: d91e98a9-72d3-4fae-9ea7-60fe74f20c54 Superblock backups stored on blocks: 32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 4096000, 7962624 Allocating group tables: done Writing inode tables: done Creating journal (65536 blocks): done Writing superblocks and filesystem accounting information: done gavin@vmmaster:~$ sudo mkdir -p /var/opt/jfrog/artifactory gavin@vmmaster:~$ sudo mount /dev/sdb1 /var/opt/jfrog/artifactory gavin@vmmaster:~$ sudo blkid /dev/sdb1 /dev/sdb1: UUID="d91e98a9-72d3-4fae-9ea7-60fe74f20c54" BLOCK_SIZE="4096" TYPE="ext4" PARTUUID="54c49b75-01" gavin@vmmaster:~$ sudo vim /etc/fstab gavin@vmmaster:~$ sudo mount -a gavin@vmmaster:~$ sudo chown -R 1030:1030 /var/opt/jfrog/artifactory gavin@vmmaster:~$
完成后的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 gavin@vmmaster:~$ sudo fdisk -l Disk /dev/sda: 25 GiB, 26843545600 bytes, 52428800 sectors Disk model: VBOX HARDDISK Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: gpt Disk identifier: 743E14DE-DB3B-4A00-A70C-ED1173FE0B81 Device Start End Sectors Size Type /dev/sda1 2048 4095 2048 1M BIOS boot /dev/sda2 4096 4198399 4194304 2G Linux filesystem /dev/sda3 4198400 52426751 48228352 23G Linux filesystem Disk /dev/sdb: 40 GiB, 42949672960 bytes, 83886080 sectors Disk model: VBOX HARDDISK Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0x54c49b75 Device Boot Start End Sectors Size Id Type /dev/sdb1 2048 83886079 83884032 40G 83 Linux Disk /dev/mapper/ubuntu--vg-ubuntu--lv: 16.5 GiB, 17712545792 bytes, 34594816 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes gavin@vmmaster:~$
9.3 实践搭建私有镜像中心
安装 JFrog Container Registry (JCR)
https://www.jfrogchina.com/docs/docker-install-artifactory-1/
操作节点
在 Master 节点上执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 1. 创建指定的目录结构(替换为新路径) mkdir -p /var/opt/jfrog/artifactory/var/etc # 2. 进入目标配置目录 cd /var/opt/jfrog/artifactory/var/etc # 3. 创建 system.yaml 配置文件 touch system.yaml # 4. 设置目录权限和属主(适配 Artifactory 容器内的用户ID) chown -R 1030:1030 /var/opt/jfrog/artifactory/var/ chmod -R 755 /var/opt/jfrog/artifactory/var/ # 生产环境不建议777,755更安全 # 5. 启动容器(替换路径和镜像) docker run --name artifactory-jcr \ -v /var/opt/jfrog/artifactory/var/:/var/opt/jfrog/artifactory \ -d -p 8081:8081 -p 8082:8082 \ gwgong/artifactory-jcr:1.0
8082: ui
默认账号admin,默认密码password
9.4 配置私有镜像中心
在 Master,node 节点上分别配置 JCR 的本地域名art.local解析
1 2 3 192.168.56.200 vmmaster art.local 192.168.56.201 vmworker01 192.168.56.202 vmworker02
配置 Docker insecure registry
不存在的话直接创建即可,ubuntu中默认不存在,直接使用默认配置,但是能够识别这个路径。
1 vi /etc/docker/daemon.json
1 2 3 4 { ... "insecure-registries" : [ "art.local:8081" ] }
1 systemctl restart docker
最后再登录
1 2 3 4 5 6 7 8 9 gavin@vmworker01:~$ sudo docker login art.local:8081 Username: admin Password: WARNING! Your credentials are stored unencrypted in '/root/.docker/config.json'. Configure a credential helper to remove this warning. See https://docs.docker.com/go/credential-store/ Login Succeeded
9.5 配置 JCR 仓库
配置 JCR
登录 JCR 并创建 5 个仓库
分别创建:docker-local, docker-test, docker-release, docker-remote 和 docker virtual 仓库
9.6 推送到私有镜像中心
打tag
1 docker tag kubeblog:1.0 art.local:8081/docker-local/kubeblog:1.0
推送
1 docker push art.local:8081/docker-local/kubeblog:1.0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ~ .......................................................................................................................................................................................................................... gavin@gavin-pc 10:31:11 > docker tag kubeblog:1.0 art.local:8081/docker-local/kubeblog:1.0 ~ .......................................................................................................................................................................................................................... gavin@gavin-pc 10:31:19 > docker images REPOSITORY TAG IMAGE ID CREATED SIZE art.local:8081/docker-local/kubeblog 1.0 572ac833702b 2 weeks ago 236MB kubeblog 1.0 572ac833702b 2 weeks ago 236MB ... ~ .................................................................................................................................................................................................................. INT 31s gavin@gavin-pc 10:32:07 > sudo docker push art.local:8081/docker-local/kubeblog:1.0 The push refers to repository [art.local:8081/docker-local/kubeblog] 41b6a3fba4ee: Pushed edb656a97ef1: Pushed 0e4cac74827b: Pushed c903a7c04a30: Pushed c1cbd6f27944: Pushed cce92674e987: Pushed 1.0: digest: sha256:4f0f553dc2f14c8841e4bff67ea54bd9efaf79a0f0a17646bf10f42811517a60 size: 1579
第十章 k8s部署博客项目
10.1 StatefulSet部署mysql
先跳过了,之前在虚拟机上单独部署了一个mysql,先用这个,之后再迁移
1 mysql -h gxtree.com -P 6606 -u root -p
10.2 编写博客应用的Service, Deployment文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 apiVersion: v1 kind: Service metadata: name: kubeblog spec: selector: app: kubeblog type: NodePort ports: - port: 5000 targetPort: 5000 nodePort: 30002 --- apiVersion: apps/v1 kind: Deployment metadata: name: kubeblog spec: selector: matchLabels: app: kubeblog template: metadata: labels: app: kubeblog spec: containers: - name: kubeblog image: gwgong/kubeblog:1.1 resources: limits: memory: "512Mi" cpu: "500m" ports: - containerPort: 5000 env: - name: MYSQL_PORT value: "6606" - name: MYSQL_SERVER value: "gxtree.com" - name: MYSQL_PASSWORD_TEST value: "xxx"
MYSQL_PASSWORD_TEST先hardcode了。
10.3 使用私有镜像中心拉取镜像
1 2 3 4 5 kubectl create secret docker-registry regcred-local \ --docker-server=art.local:8081 \ --docker-username=admin \ --docker-password=xxx \ --docker-email=gavin.gong.it@gmail.com
docker-registry 参数的核心含义
docker-registry 并不是一个 “普通参数”,而是 kubectl create secret 命令的子类型(Subtype) ,也可以理解为 “密钥类型标识符”。
它的作用是:告诉 Kubernetes,你要创建的这个 Secret 是专门用于访问 Docker/OCI 镜像仓库(Registry)的认证密钥 。
其他参数:
--docker-username 和 --docker-password :这是认证的核心,必须与私有镜像仓库(art.local:8081)的账号密码完全一致,否则无法拉取镜像。
--docker-server :必须准确指向你的私有仓库地址,否则 Kubernetes 会尝试向错误的服务器进行认证。
--docker-email :在大多数现代镜像仓库(如 Harbor、Registry v2)中,这个字段不是必填项 ,可以填写任意有效的邮箱格式(如 xxx@qq.com),甚至留空也能创建 Secret。它主要用于旧版 Docker Hub 的通知,对认证过程没有影响。
所以,账号和密码是关键,而邮箱可以按你的喜好填写。
配置containerd
没有配置:
1 2 3 4 # crictl pull art.local:8081/docker-local/kubeblog:1.1 WARN[0000] image connect using default endpoints: [unix:///run/containerd/containerd.sock unix:///run/crio/crio.sock unix:///var/run/cri-dockerd.sock]. As the default settings are now deprecated, you should set the endpoint instead. E0221 02:33:49.317208 2856465 log.go:32] "PullImage from image service failed" err="rpc error: code = Unknown desc = failed to pull and unpack image \"art.local:8081/docker-local/kubeblog:1.1\": failed to resolve image: failed to do request: Head \"https://art.local:8081/v2/docker-local/kubeblog/manifests/1.1\": http: server gave HTTP response to HTTPS client" image="art.local:8081/docker-local/kubeblog:1.1" FATA[0000] pulling image: failed to pull and unpack image "art.local:8081/docker-local/kubeblog:1.1": failed to resolve image: failed to do request: Head "https://art.local:8081/v2/docker-local/kubeblog/manifests/1.1": http: server gave HTTP response to HTTPS client
接下来开始配置:
注意网上教程中直接配置/etc/containerd/config.toml的方法,都是旧版本1.x的,不适用于2.x
1 2 # containerd --version containerd containerd.io v2.2.1 dea7da592f5d1d2b7755e3a161be07f43fad8f75
参考:https://comate.baidu.com/zh/page/oyrebl3ardh#9
我的版本是2.2.1,所以按照下面的步骤来配置http私有镜像中心,我的地址为http://art.local:8081
step1: 配置对应地址
1 2 mkdir -p /etc/containerd/certs.d/art.local:8081 vim hosts.toml
1 2 3 4 5 6 7 8 9 10 server = "http://art.local:8081" [host."http://art.local:8081"] capabilities = ["pull" , "resolve" , "push" ] skip_verify = true [host."http://art.local:8081".auth] username = "admin" password = "2wsxxxxog"
step2: 指定配置的位置
1 vim /etc/containerd/config.toml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 version = 3 [grpc] address = "/run/containerd/containerd.sock" [plugins.'io.containerd.cri.v1.images'] snapshotter = 'overlayfs' [plugins.'io.containerd.cri.v1.images'.registry] config_path = '/etc/containerd/certs.d' [plugins.'io.containerd.cri.v1.runtime'] [plugins.'io.containerd.cri.v1.runtime'.containerd] default_runtime_name = 'runc' [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes] [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc] runtime_type = 'io.containerd.runc.v2' [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options] SystemdCgroup = true [plugins.'io.containerd.grpc.v1.cri'] disable_tcp_service = true stream_server_address = '127.0.0.1' stream_server_port = '0'
注意网上的教程大部分都是说配置这个,但是[plugins."io.containerd.grpc.v1.cri".registry]已经更新为[plugins.'io.containerd.cri.v1.images'.registry],所以配置的时候要确认这个位置
1 2 [plugins."io.containerd.grpc.v1.cri".registry] config_path = "/etc/containerd/certs.d"
step3: 重启服务
1 systemctl daemon-reload && systemctl restart containerd.service && sudo systemctl restart kubelet
step4: 测试效果
1 crictl pull art.local:8081/docker-local/kubeblog:1.1
出现认证失败而不是协议失败就成功了
1 2 3 4 # crictl pull art.local:8081/docker-local/kubeblog:1.1 WARN[0000] image connect using default endpoints: [unix:///run/containerd/containerd.sock unix:///run/crio/crio.sock unix:///var/run/cri-dockerd.sock]. As the default settings are now deprecated, you should set the endpoint instead. E0221 02:33:39.432800 2906992 log.go:32] "PullImage from image service failed" err="rpc error: code = Unknown desc = failed to pull and unpack image \"art.local:8081/docker-local/kubeblog:1.1\": failed to resolve image: failed to authorize: failed to fetch anonymous token: unexpected status from GET request to http://art.local:8081/artifactory/api/docker/docker-local/v2/token?scope=repository%3Adocker-local%2Fkubeblog%3Apull&scope=repository%3Akubeblog%3Apull&service=art.local%3A8081: 401 Unauthorized" image="art.local:8081/docker-local/kubeblog:1.1" FATA[0000] pulling image: failed to pull and unpack image "art.local:8081/docker-local/kubeblog:1.1": failed to resolve image: failed to authorize: failed to fetch anonymous token: unexpected status from GET request to http://art.local:8081/artifactory/api/docker/docker-local/v2/token?scope=repository%3Adocker-local%2Fkubeblog%3Apull&scope=repository%3Akubeblog%3Apull&service=art.local%3A8081: 401 Unauthorized
认证的话,本小节最开始已经使用k8s的secret搞定了,之后k8s就可以正常拉取私服镜像了🙏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 apiVersion: v1 kind: Service metadata: name: kubeblog spec: selector: app: kubeblog type: NodePort ports: - port: 5000 targetPort: 5000 nodePort: 30002 --- apiVersion: apps/v1 kind: Deployment metadata: name: kubeblog spec: selector: matchLabels: app: kubeblog template: metadata: labels: app: kubeblog spec: containers: - name: kubeblog image: art.local:8081/docker-local/kubeblog:1.1 resources: limits: memory: "512Mi" cpu: "500m" ports: - containerPort: 5000 env: - name: MYSQL_PORT value: "6606" - name: MYSQL_SERVER value: "gxtree.com" - name: MYSQL_PASSWORD_TEST valueFrom: secretKeyRef: name: mysql-password-test key: MYSQL_PASSWORD_TEST imagePullSecrets: - name: regcred-local
10.4 使用secret导入password
step1: 设置secret
1 kubectl create secret generic mysql-password-test --from-literal=MYSQL_PASSWORD_TEST=xxx
step2: 使用secret
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 apiVersion: v1 kind: Service metadata: name: kubeblog spec: selector: app: kubeblog type: NodePort ports: - port: 5000 targetPort: 5000 nodePort: 30002 --- apiVersion: apps/v1 kind: Deployment metadata: name: kubeblog spec: selector: matchLabels: app: kubeblog template: metadata: labels: app: kubeblog spec: containers: - name: kubeblog image: art.local:8081/docker-local/kubeblog:1.1 resources: limits: memory: "512Mi" cpu: "500m" ports: - containerPort: 5000 env: - name: MYSQL_PORT value: "6606" - name: MYSQL_SERVER value: "gxtree.com" - name: MYSQL_PASSWORD_TEST valueFrom: secretKeyRef: name: mysql-password-test key: MYSQL_PASSWORD_TEST imagePullSecrets: - name: regcred-local
10.5 空间隔离和镜像晋级
创建 Test 和 Prod namespace
1 2 kubectl create namespace test kubectl create namespace live
创建私有镜像中心登录secret
1 2 3 4 5 6 7 8 9 10 11 12 13 kubectl create secret docker-registry regcred-local \ --docker-server=art.local:8081 \ --docker-username=admin \ --docker-password=xxx \ --docker-email=gavin.gong.it@gmail.com \ -n test kubectl create secret docker-registry regcred-local \ --docker-server=art.local:8081 \ --docker-username=admin \ --docker-password=xxx \ --docker-email=gavin.gong.it@gmail.com \ -n live
创建mysql secret
1 2 kubectl create secret generic mysql-password-test --from-literal=MYSQL_PASSWORD_TEST=xxx -n test kubectl create secret generic mysql-password-test --from-literal=MYSQL_PASSWORD_TEST=xxx -n live
修改资源文件,部署到test/live命名空间下
关键修改说明
Service 部分 :在 metadata 节点下添加 namespace: test,表示这个 Service 资源归属 test 命名空间。
Deployment 部分 :同样在 metadata 节点下添加 namespace: test,表示 Deployment 及其管理的 Pod 都会部署到 test 命名空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 apiVersion: v1 kind: Service metadata: name: kubeblog namespace: test spec: selector: app: kubeblog type: NodePort ports: - port: 5000 targetPort: 5000 nodePort: 30002 --- apiVersion: apps/v1 kind: Deployment metadata: name: kubeblog namespace: test spec: selector: matchLabels: app: kubeblog template: metadata: labels: app: kubeblog spec: containers: - name: kubeblog image: art.local:8081/docker-local/kubeblog:1.1 resources: limits: memory: "512Mi" cpu: "500m" ports: - containerPort: 5000 env: - name: MYSQL_PORT value: "6606" - name: MYSQL_SERVER value: "gxtree.com" - name: MYSQL_PASSWORD_TEST valueFrom: secretKeyRef: name: mysql-password-test key: MYSQL_PASSWORD_TEST imagePullSecrets: - name: regcred-local
镜像晋级:
现在只是简单在jfrog的ui上点击copy来进行晋级,生产环境需要使用api定制一下。
第十一章 Helm
11-1 Helm 介绍和安装
概念
Kubernetes 中的包管理工具
用 Yaml 管理多个应用同时部署
实现了部署的版本管理
应用和配置分离
优点
管理复杂的多容器部署(微服务发布)
版本更新更加容易
共享版本更加方便
一键回滚多个容器版本
安装
按下面步骤在控制节点执行:
1 2 3 4 5 # 下载并安装最新版 Helm 3 curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash # 验证安装结果(输出版本且无报错即为成功) helm version
1 2 3 4 5 6 7 8 9 10 root@vmmaster:~# curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 11929 100 11929 0 0 66275 0 --:--:-- --:--:-- --:--:-- 66642 Downloading https://get.helm.sh/helm-v3.20.0-linux-amd64.tar.gz Verifying checksum... Done. Preparing to install helm into /usr/local/bin helm installed into /usr/local/bin/helm root@vmmaster:~# helm version version.BuildInfo{Version:"v3.20.0", GitCommit:"b2e4314fa0f229a1de7b4c981273f61d69ee5a59", GitTreeState:"clean", GoVersion:"go1.25.6"}
11.2 部署一个 Helm Chart
一、创建一个 Helm Chart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 root@vmmaster:~/k8s/helm# ll total 12 drwxr-xr-x 3 root root 4096 Feb 23 02:22 ./ drwxr-xr-x 8 root root 4096 Feb 23 02:22 ../ drwxr-xr-x 4 root root 4096 Feb 23 02:22 foo/ root@vmmaster:~/k8s/helm# ll foo/ total 32 drwxr-xr-x 4 root root 4096 Feb 23 02:22 ./ drwxr-xr-x 3 root root 4096 Feb 23 02:22 ../ -rw-r--r-- 1 root root 349 Feb 23 02:22 .helmignore -rw-r--r-- 1 root root 1139 Feb 23 02:22 Chart.yaml drwxr-xr-x 2 root root 4096 Feb 23 02:22 charts/ drwxr-xr-x 3 root root 4096 Feb 23 02:22 templates/ -rw-r--r-- 1 root root 5249 Feb 23 02:22 values.yaml
说明:
这个命令会在当前目录下生成一个名为 foo 的目录,里面是一个标准的 Helm Chart 模板结构。
包含了 Chart 的配置文件(Chart.yaml)、默认值(values.yaml)、模板文件(templates/)等,方便你快速开始编写自己的应用部署配置。
看到的这个 foo 目录是 Helm 自动生成的标准 Chart 目录结构 ,每个文件 / 目录都有明确的作用:
核心文件 / 目录说明(按重要性排序)
文件 / 目录
作用(大白话解释)
类比(方便理解)
Chart.yaml
Chart 的 “身份证” :记录这个 Helm 应用的核心信息(名称、版本、描述、K8s 版本要求等)
Git 仓库的 README(核心说明)
values.yaml
默认配置文件 :存放所有可自定义的参数(比如镜像地址、容器端口、副本数),部署时可通过 --set 覆盖
程序的 config.yaml(配置项)
templates/
K8s 资源模板目录 :里面是带 Helm 语法的 YAML 模板(Deployment、Service、Ingress 等),Helm 会把这里的模板 + values.yaml 的配置渲染成最终的 K8s YAML
代码的模板文件(填参数就可用)
.helmignore
忽略文件 :指定打包 Chart 时不需要包含的文件(比如日志、临时文件、.git 目录)
Git 的 .gitignore(忽略无关文件)
charts/
依赖目录 :存放当前 Chart 依赖的其他子 Chart(比如部署 WordPress 依赖 MySQL Chart)
项目的 node_modules(依赖包)
补充两个关键细节
模板渲染逻辑
Helm 部署时,会把 templates/里的模板文件,结合 values.yaml(或 --set)的参数,替换成真实的 K8s 资源 YAML,再提交给 K8s 集群。
比如 templates/deployment.yaml里有 {{ .Values.image.repository }},Helm 会把它替换成values.yaml里配置的镜像地址。
空目录的意义 :你看到的 charts/ 现在是空的,因为这是刚创建的空 Chart,没有依赖;templates/ 里有默认的模板文件(比如 deployment、service),你可以直接改,也可以删了自己写。
总结
Chart.yaml 是 Chart 的核心元信息,必填且不能乱改;
values.yaml 是配置入口,想改部署参数优先改这里;
templates/ 是 K8s 资源的模板核心,决定最终部署哪些 K8s 资源;
剩下的 .helmignore 和 charts/ 是辅助性的,初期用不到也没关系。
二、部署一个 Helm Chart
1 2 # 格式:helm install [发布名称] [Chart路径] --set [自定义参数] helm install my-foo-app ./foo --set name=foo
说明:
my-foo-app 是你给这次部署起的唯一名称(可以自定义,比如 foo-demo),后续删除、升级都要用到这个名称。
helm install 是部署 Chart 的核心命令。
--set name=foo 用于在部署时覆盖 values.yaml 中的默认配置,这里将 name 参数设置为 foo。
./foo 指定了要部署的 Chart 所在的本地路径(即刚才 helm create 生成的目录)。
执行后,Helm 会根据 Chart 中的模板和配置,在 Kubernetes 集群中创建对应的资源(如 Deployment、Service 等)。
三、查看已部署的 Release
会看到你刚部署的 my-foo-app,状态为 deployed。
四、删除部署
说明:
这个命令会删除名为 my-foo-app 的 Helm 发布(Release)。
它会清理掉该发布在 Kubernetes 中创建的所有相关资源,实现一键卸载。
11.3 配置JCR Helm仓库
类似的docker的配置,helm-local,helm-test,helm-release, helm-remote 和 helm-virtual
完成后:
1 2 root@vmmaster:~/k8s/helm# helm repo add helm http://art.local:8081/artifactory/helm --username admin --password AP3GjvK1YJ8h9XctaU8iZUjL28f "helm" has been added to your repositories
11.4 创建博客应用的 Helm Chart
做一个 Helm Chart 的标准流程就是 :
先写好正常的 K8s YAML
deployment.yaml、service.yaml、ingress.yaml 等
→ 能直接用 kubectl apply 跑起来的那种。
把这些 YAML 丢进 templates/ 目录
把里面会变的内容,一个个抽成变量
比如:
镜像名字
副本数
端口
资源限制(cpu/mem)
环境变量
标签、注解
从这种:
变成这种:
1 image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
把这些变量统一放到 values.yaml
1 2 3 image: repository: nginx tag: 1.25
Helm 会自动把模板 + 变量 拼成最终 YAML 再发给 K8s。
一句话总结(你可以背下来)
Helm = 模板引擎 + 配置管理 + K8s 发布工具
templates/:放原来的 YAML
values.yaml:放会变的参数
部署时:模板 + 变量 = 最终 K8s 资源
多环境配置隔离 :
一、核心思路:拆分 values 文件 + 部署时指定
Helm 允许你在部署时通过 --values/-f 参数指定多个 values 文件 ,优先级是:命令行 --set > 自定义 values 文件 > Chart 内置的 values.yaml。
所以我们可以给不同环境建独立的 values 文件,比如:
1 2 3 4 5 foo/ ├── Chart.yaml ├── values.yaml # 基础默认配置(所有环境共用) ├── values-test.yaml # 测试环境专属配置(覆盖默认) └── values-live.yaml # 生产环境专属配置(覆盖默认)
二、实操步骤(新手直接抄)
步骤 1:编写多环境 values 文件
values.yaml(基础配置):放所有环境共用的配置
1 2 3 4 5 6 7 8 app: name: my-service port: 8080 image: repository: my-app tag: latest replicaCount: 1
values-test.yaml(测试环境):只放测试环境的差异化配置(覆盖默认)
1 2 3 4 5 6 7 8 9 replicaCount: 1 image: tag: test-v1.0 env: - name: ENV_TYPE value: test - name: DEBUG value: "true"
values-live.yaml(生产环境):只放生产环境的差异化配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 replicaCount: 3 image: tag: live-v1.0 env: - name: ENV_TYPE value: live - name: DEBUG value: "false" resources: limits: cpu: 1000m memory: 1Gi requests: cpu: 500m memory: 512Mi
步骤 2:部署不同环境
1 2 3 4 5 6 7 8 # 部署到测试环境:先加载默认 values.yaml,再用 values-test.yaml 覆盖 helm install my-app-test ./foo -f values-test.yaml # 部署到生产环境:先加载默认 values.yaml,再用 values-live.yaml 覆盖 helm install my-app-live ./foo -f values-live.yaml # 进阶:叠加多个文件(比如通用配置 + 环境配置 + 临时参数) helm install my-app-live ./foo -f values-common.yaml -f values-live.yaml --set image.tag=live-v1.1
一、Helm 目录结构
首先创建标准的 Helm Chart 目录结构:
1 2 3 4 5 6 7 8 kubeblog/ ├── Chart.yaml # Chart 元信息 ├── templates/ │ ├── service.yaml # Service 模板 │ └── deployment.yaml # Deployment 模板 ├── values.yaml # 公共默认配置 ├── values-test.yaml # 测试环境配置 └── values-live.yaml # 生产环境配置
二、各文件内容实现
Chart.yaml(Chart 元信息)
1 2 3 4 5 6 apiVersion: v2 name: kubeblog description: A Helm chart for kubeblog application type: application version: 0.1 .0 appVersion: "1.1"
values.yaml(公共默认配置)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 app: name: kubeblog port: 5000 containerPort: 5000 nodePort: 30002 image: repository: art.local:8081/docker-local/kubeblog tag: 1.1 pullSecret: regcred-local resources: limits: memory: "512Mi" cpu: "500m" mysql: port: "6606" server: "" secret: name: "" key: ""
values-test.yaml(测试环境配置)
1 2 3 4 5 6 7 8 9 10 11 12 13 namespace: test app: nodePort: 30002 mysql: server: "gxtree.com" secret: name: mysql-password-test key: MYSQL_PASSWORD_TEST
values-live.yaml(生产环境配置)
1 2 3 4 5 6 7 8 9 10 11 12 13 namespace: live app: nodePort: 30003 mysql: server: "gxtree.com" secret: name: mysql-password-test key: MYSQL_PASSWORD_TEST
templates/service.yaml(Service 模板)
1 2 3 4 5 6 7 8 9 10 11 12 13 apiVersion: v1 kind: Service metadata: name: {{ .Values.app.name }} namespace: {{ .Values.namespace }}spec: selector: app: {{ .Values.app.name }} type: NodePort ports: - port: {{ .Values.app.port }} targetPort: {{ .Values.app.containerPort }} nodePort: {{ .Values.app.nodePort }}
templates/deployment.yaml(Deployment 模板)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Values.app.name }} namespace: {{ .Values.namespace }}spec: selector: matchLabels: app: {{ .Values.app.name }} template: metadata: labels: app: {{ .Values.app.name }} spec: containers: - name: {{ .Values.app.name }} image: {{ .Values.app.image.repository }}:{{ .Values.app.image.tag }} resources: limits: memory: {{ .Values.resources.limits.memory }} cpu: {{ .Values.resources.limits.cpu }} ports: - containerPort: {{ .Values.app.containerPort }} env: - name: MYSQL_PORT value: {{ .Values.mysql.port | quote }} - name: MYSQL_SERVER value: {{ .Values.mysql.server }} - name: MYSQL_PASSWORD_TEST valueFrom: secretKeyRef: name: {{ .Values.mysql.secret.name }} key: {{ .Values.mysql.secret.key }} imagePullSecrets: - name: {{ .Values.app.image.pullSecret }}
三、使用方法
部署测试环境 :
1 helm install kubeblog-test ./kubeblog -f ./kubeblog/values-test.yaml
部署生产环境 :
1 helm install kubeblog-live ./kubeblog -f ./kubeblog/values-live.yaml
更新部署 (如修改配置后):
1 2 3 4 5 # 测试环境 helm upgrade kubeblog-test ./kubeblog -f ./kubeblog/values-test.yaml # 生产环境 helm upgrade kubeblog-live ./kubeblog -f ./kubeblog/values-live.yaml
11.5 将 Helm Chart 上传到 JCR
添加仓库:之前添加过了,可以忽略
1 2 3 root@vmmaster:~/k8s/helm# helm repo ls NAME URL helm http://art.local:8081/artifactory/helm
打包:版本号在Chart.yaml中设置
上传:之前jcr中获取到的命令
1 curl -uadmin:AP3GjvK1YJ8h9XctaU8iZUjL28f -T <PATH_TO_FILE> "http://art.local:8081/artifactory/helm/<TARGET_FILE_PATH>"
1 curl -uadmin:AP3GjvK1YJ8h9XctaU8iZUjL28f -T foo-0.1.0.tgz "http://art.local:8081/artifactory/helm/foo-0.1.0.tgz"
更新:
search:
1 helm search repo helm/foo
直接install远程仓库的chart
1 2 # 安装远程仓库的 foo Chart(指定版本,避免装到最新版) helm install my-foo-app helm/foo --version 0.1.0
11.6 回滚
回滚到上一个版本(最常用)
1 2 # 语法:helm rollback <Release名称> helm rollback my-foo-app
指定版本回滚(精准控制,生产首选)
1、获取版本号
1 2 # 查看指定 Release 的历史版本 helm history my-foo-app
2、回滚
1 2 # 语法:helm rollback <Release名称> <历史版本号> helm rollback my-foo-app 1
第十二章 prometheus 和 grafana
12.1 prometheus 介绍
https://prometheus.ac.cn/docs/introduction/overview/
12.2 安装node exporter
同步系统时间 & 查看当前时间和同步状态
1 2 sudo timedatectl set-timezone Asia/Shanghai timedatectl
创建命名空间
1 kubectl create namespace monitoring
安装node export daemonset
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 apiVersion: v1 kind: Service metadata: annotations: prometheus.io/scrape: 'true' name: prometheus-node-exporter namespace: monitoring labels: app: prometheus component: node-exporter spec: clusterIP: None ports: - name: prometheus-node-exporter port: 9100 protocol: TCP selector: app: prometheus component: node-exporter type: ClusterIP --- apiVersion: apps/v1 kind: DaemonSet metadata: name: prometheus-node-exporter namespace: monitoring labels: app: prometheus component: node-exporter spec: selector: matchLabels: app: prometheus component: node-exporter template: metadata: name: prometheus-node-exporter labels: app: prometheus component: node-exporter spec: containers: - image: prom/node-exporter:v1.6.1 name: prometheus-node-exporter ports: - name: prom-node-exp containerPort: 9100 hostPort: 9100 hostNetwork: true hostPID: true
使用宿主机网络,开放宿主机端口 9100,可以直接通过 <node_ip>:9100 访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 gavin@vmworker01:~$ curl 192.168.56.201:9100/metrics | tail -n 10 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 81942 0 81942 0 0 6298k 0 --:--:-- --:--:-- --:--:-- 6668k promhttp_metric_handler_errors_total{cause="encoding"} 0 promhttp_metric_handler_errors_total{cause="gathering"} 0 # HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served. # TYPE promhttp_metric_handler_requests_in_flight gauge promhttp_metric_handler_requests_in_flight 1 # HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code. # TYPE promhttp_metric_handler_requests_total counter promhttp_metric_handler_requests_total{code="200"} 4 promhttp_metric_handler_requests_total{code="500"} 0 promhttp_metric_handler_requests_total{code="503"} 0
安装kube-state-metrics
kube-state-metrics 关注于获取 k8s 各种资源的最新状态,如 deployment 或者 daemonset,将 k8s 的运行状况在内存中做个快照,并且获取新的指标。
创建对应的 ServiceAccount
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 apiVersion: v1 kind: ServiceAccount metadata: name: kube-state-metrics namespace: monitoring --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: kube-state-metrics rules: - apiGroups: ["" ] resources: - configmaps - secrets - nodes - pods - services - resourcequotas - replicationcontrollers - limitranges - persistentvolumeclaims - persistentvolumes - namespaces - endpoints verbs: ["list" , "watch" ]- apiGroups: ["apps" ] resources: - daemonsets - deployments - replicasets - statefulsets verbs: ["list" , "watch" ]- apiGroups: ["batch" ] resources: - cronjobs - jobs verbs: ["list" , "watch" ]- apiGroups: ["autoscaling" ] resources: - horizontalpodautoscalers verbs: ["list" , "watch" ]- apiGroups: ["policy" ] resources: - poddisruptionbudgets verbs: ["list" , "watch" ]- apiGroups: ["certificates.k8s.io" ] resources: - certificatesigningrequests verbs: ["list" , "watch" ]- apiGroups: ["storage.k8s.io" ] resources: - storageclasses - csinodes verbs: ["list" , "watch" ]- apiGroups: ["networking.k8s.io" ] resources: - ingresses - networkpolicies verbs: ["list" , "watch" ]--- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: kube-state-metrics roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: kube-state-metrics subjects: - kind: ServiceAccount name: kube-state-metrics namespace: monitoring
创建kube-state-metrics deployment
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 apiVersion: apps/v1 kind: Deployment metadata: name: kube-state-metrics namespace: monitoring spec: replicas: 1 selector: matchLabels: app: kube-state-metrics template: metadata: labels: app: kube-state-metrics spec: serviceAccountName: kube-state-metrics containers: - name: kube-state-metrics image: k8s.gcr.io/kube-state-metrics/kube-state-metrics:v2.10.0 ports: - containerPort: 8080 name: http-metrics - containerPort: 8081 name: telemetry resources: limits: cpu: 500m memory: 256Mi requests: cpu: 100m memory: 64Mi livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 timeoutSeconds: 5 readinessProbe: httpGet: path: /readyz port: 8080 initialDelaySeconds: 5 timeoutSeconds: 5
创建kube-state-metrics service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 apiVersion: v1 kind: Service metadata: name: kube-state-metrics namespace: monitoring annotations: prometheus.io/scrape: "true" prometheus.io/port: "8080" spec: selector: app: kube-state-metrics ports: - name: http-metrics port: 8080 targetPort: 8080 - name: telemetry port: 8081 targetPort: 8081 type: ClusterIP
部署 node disk monitor
监视 Node 的磁盘占用情况
类型是 daemonset,每个 node 上都会部署一份
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 apiVersion: apps/v1 kind: DaemonSet metadata: name: node-disk-monitor namespace: monitoring labels: app: node-disk-monitor spec: selector: matchLabels: app: node-disk-monitor template: metadata: labels: app: node-disk-monitor spec: containers: - name: disk-check image: giantswarm/tiny-tools:latest command: - /bin/sh - -c - | while true; do # 检查根分区磁盘使用情况(可根据需要调整路径) DISK_USAGE=$(df -h / | grep -v Filesystem | awk '{print $5}' | sed 's/%//') TIMESTAMP=$(date +%s) # 输出 Prometheus 格式的指标 echo "node_disk_usage_percent{mountpoint=\"/\"} $DISK_USAGE $TIMESTAMP" # 每60秒检查一次 sleep 60 done volumeMounts: - name: rootfs mountPath: /host readOnly: true securityContext: privileged: true - name: caddy image: dockermuenster/caddy:0.9.3 ports: - containerPort: 9101 name: metrics command: - /bin/sh - -c - | echo ":9101 { root /srv browse }" > /etc/Caddyfile # 将 disk-check 容器的指标输出到 /srv/metrics mkfifo /srv/metrics while true; do cat /srv/metrics done & /usr/bin/caddy --conf /etc/Caddyfile volumeMounts: - name: metrics-fifo mountPath: /srv volumes: - name: rootfs hostPath: path: / - name: metrics-fifo emptyDir: {}--- apiVersion: v1 kind: Service metadata: name: node-disk-monitor namespace: monitoring annotations: prometheus.io/scrape: "true" prometheus.io/port: "9101" spec: selector: app: node-disk-monitor ports: - port: 9101 targetPort: 9101 clusterIP: None
12.3 部署prometheus
先扩容一下逻辑卷,不然不够用,后面pvc还需要申请
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 gavin@vmmaster:~$ sudo pvresize /dev/sda3 Physical volume "/dev/sda3" changed 1 physical volume(s) resized or updated / 0 physical volume(s) not resized gavin@vmmaster:~$ sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv Size of logical volume ubuntu-vg/ubuntu-lv changed from <16.50 GiB (4223 extents) to <23.00 GiB (5887 extents). Logical volume ubuntu-vg/ubuntu-lv successfully resized. gavin@vmmaster:~$ sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv resize2fs 1.47.0 (5-Feb-2023) Filesystem at /dev/mapper/ubuntu--vg-ubuntu--lv is mounted on /; on-line resizing required old_desc_blocks = 3, new_desc_blocks = 3 The filesystem on /dev/mapper/ubuntu--vg-ubuntu--lv is now 6028288 (4k) blocks long. gavin@vmmaster:~$ sudo fdisk -l Disk /dev/sda: 25 GiB, 26843545600 bytes, 52428800 sectors Disk model: VBOX HARDDISK Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: gpt Disk identifier: 743E14DE-DB3B-4A00-A70C-ED1173FE0B81 Device Start End Sectors Size Type /dev/sda1 2048 4095 2048 1M BIOS boot /dev/sda2 4096 4198399 4194304 2G Linux filesystem /dev/sda3 4198400 52426751 48228352 23G Linux filesystem Disk /dev/sdb: 40 GiB, 42949672960 bytes, 83886080 sectors Disk model: VBOX HARDDISK Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0x54c49b75 Device Boot Start End Sectors Size Id Type /dev/sdb1 2048 83886079 83884032 40G 83 Linux Disk /dev/mapper/ubuntu--vg-ubuntu--lv: 23 GiB, 24691867648 bytes, 48226304 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes gavin@vmmaster:~$
创建pvc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 kind: PersistentVolumeClaim apiVersion: v1 metadata: name: prometheus-storage-claim namespace: monitoring annotations: volume.beta.kubernetes.io/storage-class: "managed-nfs-storage" labels: app: prometheus component: storage spec: accessModes: - ReadWriteMany resources: requests: storage: 5Gi
部署prometheus
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 apiVersion: v1 kind: ServiceAccount metadata: name: prometheus namespace: monitoring --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: prometheus rules: - apiGroups: ["" ] resources: - nodes - nodes/metrics - services - endpoints - pods verbs: ["get" , "list" , "watch" ]- apiGroups: ["" ] resources: - configmaps verbs: ["get" ]- apiGroups: - networking.k8s.io resources: - ingresses verbs: ["get" , "list" , "watch" ]- nonResourceURLs: ["/metrics" , "/metrics/cadvisor" ] verbs: ["get" ]--- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: prometheus roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: prometheus subjects: - kind: ServiceAccount name: prometheus namespace: monitoring --- apiVersion: v1 kind: ConfigMap metadata: name: prometheus-config namespace: monitoring data: prometheus.yml: | global: scrape_interval: 15s # 全局采集间隔 evaluation_interval: 15s # 规则评估间隔 rule_files: scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090' ] - job_name: 'node-exporter' kubernetes_sd_configs: - role: endpoints namespaces: names: ['monitoring' ] relabel_configs: - source_labels: [__meta_kubernetes_service_label_app , __meta_kubernetes_service_label_component ] regex: prometheus;node-exporter action: keep - source_labels: [__meta_kubernetes_endpoint_port_name ] regex: prometheus-node-exporter action: keep - job_name: 'kube-state-metrics' kubernetes_sd_configs: - role: endpoints namespaces: names: ['monitoring' ] relabel_configs: - source_labels: [__meta_kubernetes_service_label_app ] regex: kube-state-metrics action: keep - source_labels: [__meta_kubernetes_endpoint_port_name ] regex: http-metrics action: keep - job_name: 'node-disk-monitor' kubernetes_sd_configs: - role: endpoints namespaces: names: ['monitoring' ] relabel_configs: - source_labels: [__meta_kubernetes_service_label_app ] regex: node-disk-monitor action: keep - source_labels: [__meta_kubernetes_endpoint_port_number ] regex: 9101 action: keep - job_name: 'kubernetes-cadvisor' kubernetes_sd_configs: - role: node relabel_configs: - action: labelmap regex: __meta_kubernetes_node_label_(.+) - target_label: __address__ replacement: kubernetes.default.svc:443 - source_labels: [__meta_kubernetes_node_name ] regex: (.+) target_label: __metrics_path__ replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor scheme: https tls_config: insecure_skip_verify: true authorization: credentials_file: /var/run/secrets/kubernetes.io/serviceaccount/token --- apiVersion: apps/v1 kind: StatefulSet metadata: name: prometheus namespace: monitoring labels: app: prometheus spec: serviceName: prometheus replicas: 1 selector: matchLabels: app: prometheus template: metadata: labels: app: prometheus spec: serviceAccountName: prometheus containers: - name: prometheus image: registry.aliyuncs.com/google_containers/prometheus:v2.53.1 args: - --config.file=/etc/prometheus/prometheus.yml - --storage.tsdb.path=/prometheus - --storage.tsdb.retention.time=15d - --web.console.libraries=/usr/share/prometheus/console_libraries - --web.console.templates=/usr/share/prometheus/consoles - --web.enable-lifecycle ports: - containerPort: 9090 name: web resources: limits: cpu: 1000m memory: 1Gi requests: cpu: 200m memory: 256Mi livenessProbe: httpGet: path: /-/healthy port: 9090 initialDelaySeconds: 30 timeoutSeconds: 5 readinessProbe: httpGet: path: /-/ready port: 9090 initialDelaySeconds: 5 timeoutSeconds: 5 volumeMounts: - name: prometheus-config mountPath: /etc/prometheus - name: prometheus-data mountPath: /prometheus securityContext: runAsUser: 0 runAsGroup: 0 volumes: - name: prometheus-config configMap: name: prometheus-config - name: prometheus-data persistentVolumeClaim: claimName: prometheus-storage-claim --- apiVersion: v1 kind: Service metadata: name: prometheus namespace: monitoring labels: app: prometheus spec: selector: app: prometheus ports: - name: web port: 9090 targetPort: 9090 type: ClusterIP --- apiVersion: v1 kind: Service metadata: name: prometheus-nodeport namespace: monitoring spec: selector: app: prometheus ports: - name: web port: 9090 targetPort: 9090 nodePort: 30090 type: NodePort
12.4 配置grafana
configmap: https://github.com/ggw2021/kubeblog/blob/main/docs/Chapter13/13-4-grafana-net-2-dashboard-configmap.yml
创建用户名和密码的secret
1 2 3 4 kubectl create secret generic grafana \ --namespace=monitoring \ --from-literal=admin-username=<你的用户名> \ --from-literal=admin-password=<你的密码>
deployment & service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 apiVersion: apps/v1 kind: Deployment metadata: name: grafana-core namespace: monitoring labels: app: grafana component: core spec: replicas: 1 selector: matchLabels: app: grafana component: core template: metadata: labels: app: grafana component: core spec: containers: - image: grafana/grafana:10.4.2 name: grafana-core imagePullPolicy: IfNotPresent resources: limits: cpu: 100m memory: 100Mi requests: cpu: 100m memory: 100Mi env: - name: GF_AUTH_BASIC_ENABLED value: "true" - name: GF_SECURITY_ADMIN_USER valueFrom: secretKeyRef: name: grafana key: admin-username - name: GF_SECURITY_ADMIN_PASSWORD valueFrom: secretKeyRef: name: grafana key: admin-password - name: GF_AUTH_ANONYMOUS_ENABLED value: "false" readinessProbe: httpGet: path: /login port: 3000 volumeMounts: - name: grafana-persistent-storage mountPath: /var/lib/grafana volumes: - name: grafana-persistent-storage emptyDir: {}--- apiVersion: v1 kind: Service metadata: name: grafana namespace: monitoring labels: app: grafana component: core spec: type: NodePort ports: - port: 3000 nodePort: 30091 selector: app: grafana component: core
完成后登录grafana,配置prometheus数据源(主要是填写地址http://192.168.56.202:30090/),然后旁边有dashboard,可以直接点导入。
12.5 配置监控kubeblog
配置采集:看最后一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 apiVersion: v1 kind: ConfigMap metadata: name: prometheus-config namespace: monitoring data: prometheus.yml: | global: scrape_interval: 15s # 全局采集间隔 evaluation_interval: 15s # 规则评估间隔 rule_files: scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090' ] - job_name: 'node-exporter' kubernetes_sd_configs: - role: endpoints namespaces: names: ['monitoring' ] relabel_configs: - source_labels: [__meta_kubernetes_service_label_app , __meta_kubernetes_service_label_component ] regex: prometheus;node-exporter action: keep - source_labels: [__meta_kubernetes_endpoint_port_name ] regex: prometheus-node-exporter action: keep - job_name: 'kube-state-metrics' kubernetes_sd_configs: - role: endpoints namespaces: names: ['monitoring' ] relabel_configs: - source_labels: [__meta_kubernetes_service_label_app ] regex: kube-state-metrics action: keep - source_labels: [__meta_kubernetes_endpoint_port_name ] regex: http-metrics action: keep - job_name: 'node-disk-monitor' kubernetes_sd_configs: - role: endpoints namespaces: names: ['monitoring' ] relabel_configs: - source_labels: [__meta_kubernetes_service_label_app ] regex: node-disk-monitor action: keep - source_labels: [__meta_kubernetes_endpoint_port_number ] regex: 9101 action: keep - job_name: 'kubernetes-cadvisor' kubernetes_sd_configs: - role: node relabel_configs: - action: labelmap regex: __meta_kubernetes_node_label_(.+) - target_label: __address__ replacement: kubernetes.default.svc:443 - source_labels: [__meta_kubernetes_node_name ] regex: (.+) target_label: __metrics_path__ replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor scheme: https tls_config: insecure_skip_verify: true authorization: credentials_file: /var/run/secrets/kubernetes.io/serviceaccount/token - job_name: 'spring-actuator' metrics_path: '/actuator/prometheus' scrape_interval: 5s static_configs: - targets: ['192.168.56.200:30002' ]
重启pod(直接删除),让采集生效
grafana 模版(官网模版中下载的):https://github.com/ggw2021/kubeblog/blob/main/docs/Chapter13/13-6-jvm-micrometer.json
在grafana的dashboard新建中选择导入,输入json,数据源选择prometheus即可
12.6 直接使用helm charts一键部署
先删除之前部署的内容,避免冲突
1 2 # 删除 monitoring 命名空间(会自动删除其中所有资源) kubectl delete namespace monitoring
添加 Prometheus 社区仓库
1 2 helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update
部署
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 helm install prometheus-stack prometheus-community/kube-prometheus-stack \ --namespace monitoring \ --create-namespace \ --set grafana.adminPassword=your_secure_password \ --set grafana.service.type=NodePort \ --set grafana.service.nodePort=32000 \ --set prometheus.service.type=NodePort \ --set prometheus.service.nodePort=32001 \ --set alertmanager.service.type=NodePort \ --set alertmanager.service.nodePort=32002
monitoring/prometheus-stack-kube-prom-prometheus:32001
配置采集规则
1 helm pull prometheus-community/kube-prometheus-stack --untar
1 vim ./helm/kube-prometheus-stack/prometheus-additional-scrape.yaml
1 2 3 4 5 6 7 8 9 10 prometheus: prometheusSpec: additionalScrapeConfigs: - job_name: 'kubeblog' metrics_path: '/actuator/prometheus' scrape_interval: 5s static_configs: - targets: ['192.168.56.200:30002' ]
使用指定的文件:
1 2 3 4 5 6 7 8 9 10 11 helm upgrade prometheus-stack ./helm/kube-prometheus-stack \ --namespace monitoring \ --create-namespace \ -f ./helm/kube-prometheus-stack/prometheus-additional-scrape.yaml \ --set grafana.adminPassword=2wsxXSW@.grafana \ --set grafana.service.type=NodePort \ --set grafana.service.nodePort=32000 \ --set prometheus.service.type=NodePort \ --set prometheus.service.nodePort=32001 \ --set alertmanager.service.type=NodePort \ --set alertmanager.service.nodePort=32002
持久化存储
1 vim ./helm/kube-prometheus-stack/prometheus-persistence-values.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 prometheus: prometheusSpec: storageSpec: volumeClaimTemplate: spec: storageClassName: managed-nfs-storage accessModes: ["ReadWriteOnce" ] resources: requests: storage: 5Gi grafana: persistence: enabled: true type: pvc storageClassName: managed-nfs-storage accessModes: - ReadWriteOnce size: 1Gi finalizers: - kubernetes.io/pvc-protection
1 2 3 4 5 6 7 8 9 10 11 12 helm upgrade prometheus-stack ./helm/kube-prometheus-stack \ --namespace monitoring \ --create-namespace \ -f ./helm/kube-prometheus-stack/prometheus-additional-scrape.yaml \ -f ./helm/kube-prometheus-stack/prometheus-persistence-values.yaml \ --set grafana.adminPassword=2wsxXSW@.grafana \ --set grafana.service.type=NodePort \ --set grafana.service.nodePort=32000 \ --set prometheus.service.type=NodePort \ --set prometheus.service.nodePort=32001 \ --set alertmanager.service.type=NodePort \ --set alertmanager.service.nodePort=32002
12.7 配置freelens指标
1 2 3 4 5 6 7 8 9 10 11 root@vmmaster:/# kubectl get svc -n monitoring NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE alertmanager-operated ClusterIP None <none> 9093/TCP,9094/TCP,9094/UDP 5h18m prometheus-operated ClusterIP None <none> 9090/TCP 5h18m prometheus-stack-grafana NodePort 10.1.76.201 <none> 80:32000/TCP 5h19m prometheus-stack-kube-prom-alertmanager NodePort 10.1.50.1 <none> 9093:32002/TCP,8080:30991/TCP 5h19m prometheus-stack-kube-prom-operator ClusterIP 10.1.67.245 <none> 443/TCP 5h19m prometheus-stack-kube-prom-prometheus NodePort 10.1.242.213 <none> 9090:32001/TCP,8080:30517/TCP 5h19m prometheus-stack-kube-state-metrics ClusterIP 10.1.119.57 <none> 8080/TCP 5h19m prometheus-stack-prometheus-node-exporter ClusterIP 10.1.124.100 <none> 9100/TCP 5h19m root@vmmaster:/#
第十三章 k8s扩展
13.1 Custom Resource 自定义资源
什么是 Custom Resource
自定义资源(Custom Resource)是对 Kubernetes API 的扩展。定制资源可以通过动态注册的方式在运行中的集群内或出现或消失,集群管理员可以独立于集群 更新定制资源。一旦某定制资源被安装,用户可以使用 kubectl 来创建和访问其中的对象,就像他们为 pods 这种内置资源所做的一样。
为什么需要自定义资源
Kubernetes 本身提供的资源类型并不足以满足每个用户的需求,例如,Kubernetes 提供的 Job 类型的资源来执行某个任务,但没有提供 CronJob 类型的资源执行定时任务,此时就可以通过定义 Custom Resource,从而实现自己需要的资源类型。
后来 CronJob 被 K8s 官方收编,成了内置资源,但它的前身就是一个典型的 Custom Resource。
什么时候应该使用自定义资源
你希望使用 Kubernetes 客户端库和 CLI 来创建和更改新的资源。
你希望 kubectl 能够直接支持你的资源;例如,kubectl get my-object object-name。
你希望构造新的自动化机制,监测新对象上的更新事件,并对其他对象执行 CRUD 操作,或者监测后者更新前者。
你希望编写自动化组件来处理对对象的更新。
你希望使用 Kubernetes API 对诸如 .spec、.status 和 .metadata 等字段的约定。
你希望对象是对一组受控资源的抽象,或者对其他资源的归纳提炼。
什么时候应该使用 configmap
存在一个已有的、文档完备的配置文件格式约定,例如 mysql.cnf 或 pom.xml。
你希望将整个配置文件放到某 configMap 中的一个主键下面。
配置文件的主要用途是针对运行在集群中 Pod 内的程序,供后者依据文件数据配置自身行为。
文件的使用者期望以 Pod 内文件或者 Pod 内环境变量的形式来使用文件数据,而不是通过 Kubernetes API。
你希望当文件被更新时通过类似 Deployment 之类的资源完成滚动更新操作。
13.2 实战扩展一个 CronTab 类型的资源
创建一个 CRD
当你创建新的 CustomResourceDefinition(CRD)时,Kubernetes API 服务器会为你所指定的每一个版本生成一个 RESTful 的资源路径。CRD 可以是名字空间作用域的,也可以是集群作用域的,取决于 CRD 的 scope 字段设置。和其他现有的内置对象一样,删除一个名字空间时,该名字空间下的所有定制对象也会被删除。CustomResourceDefinition 本身是不受名字空间限制的,对所有名字空间可用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: crontabs.stable.example.com spec: group: stable.example.com versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: cronSpec: type: string pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$' image: type: string replicas: type: integer minimum: 1 maximum: 10 scope: Namespaced names: plural: crontabs singular: crontab kind: CronTab shortNames: - ct
逐段详细解释
基础元信息(API 版本、资源类型、名称)
1 2 3 4 5 apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: crontabs.stable.example.com
关键:name 是 CRD 的 “身份证”,必须严格遵守 复数名.组名 的格式,否则创建会失败。
spec 核心配置(定义自定义资源的规则)
spec 是 CRD 的核心,所有关于 CronTab 资源的规则都在这里定义。
2.1 组和版本(API 分组、版本管理)
1 2 3 4 5 6 7 spec: group: stable.example.com versions: - name: v1 served: true storage: true
举例:后续创建 CronTab 资源时,开头要写 apiVersion: stable.example.com/v1,就是对应这里的 group + versions.name。
小技巧:served=true 表示用户可以用这个版本;storage=true 表示 K8s 会把所有 CronTab 数据都存为这个版本格式(避免多版本混乱)。
2.2 资源校验规则(schema):定义 CronTab 该有哪些字段
这部分是 “数据校验规则”,确保用户创建的 CronTab 符合规范(比如 replicas 不能小于 1,cronSpec 必须是合法的 cron 表达式)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 schema: openAPIV3Schema: type: object properties: spec: type: object properties: cronSpec: type: string pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$' image: type: string replicas: type: integer minimum: 1 maximum: 10
核心作用:防止用户写错配置 。比如如果有人把 replicas 设为 0,K8s 会直接拒绝创建,提示 “最小值是 1”;如果 cronSpec 写了 abc,会提示 “不符合 cron 表达式规则”。
2.3 作用域(scope):资源是集群级还是命名空间级
对比:内置资源中,Pod、Deployment 是 Namespaced,Node、CRD 是 Cluster。这里 CronTab 设为 Namespaced,和 Pod 一样,不同命名空间的 CronTab 相互隔离。
2.4 命名规则(names):资源的各种名称形式
1 2 3 4 5 6 names: plural: crontabs singular: crontab kind: CronTab shortNames: - ct
使用示例:创建一个符合规则的 CronTab 资源
定义完上面的 CRD 后,你可以创建一个具体的 CronTab 对象(比如 my-crontab.yaml):
1 2 3 4 5 6 7 8 9 apiVersion: stable.example.com/v1 kind: CronTab metadata: name: my-crontab namespace: default spec: cronSpec: "0 2 * * *" image: "busybox" replicas: 2
创建并查看:
1 2 3 4 5 6 # 创建 CRD kubectl apply -f your-crd-file.yaml # 创建 CronTab 资源 kubectl apply -f my-crontab.yaml # 查看 CronTab 资源 kubectl get ct # 等价于 kubectl get crontabs
第十四章 实践
注入NODE_IP到环境变量
K8s 不会主动给所有 Pod 注入 NODE_IP(或 hostIP)这类元数据,因为这是 “按需注入” 的设计:只有你明确在 Pod 配置里写了要读取 status.hostIP,K8s 才会把 Node IP 填充为环境变量,否则 Pod 里只会有 K8s 默认的环境变量(比如你看到的 KUBERNETES_SERVICE_HOST 这些)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 apiVersion: v1 kind: Pod metadata: name: static-web labels: role: myrole spec: containers: - name: web image: nginx ports: - name: web containerPort: 80 protocol: TCP env: - name: NODE_IP valueFrom: fieldRef: fieldPath: status.hostIP - name: CONSUL_HTTP_ADDR value: "http://$(NODE_IP):8500"
1 2 3 4 root@static-web:/# env | grep -E "NODE_IP|CONSUL_HTTP_ADDR" NODE_IP=192.168.56.201 CONSUL_HTTP_ADDR=http://192.168.56.201:8500 root@static-web:/#
日志采集
K8s 容器 stdout/stderr 日志不在容器内部 ,而是持久化在Pod 所在节点 的本地文件系统中,核心路径分两层:
路径类型
具体路径
作用说明
真实日志文件
/var/log/pods/<命名空间>_<Pod名>_<PodUID>/<容器名>/0.log
容器运行时(containerd/CRI-O)直接写入的真实日志文件,按 Pod / 容器隔离
软链快捷路径
/var/log/containers/<Pod名>_<命名空间>_<容器名>-<容器ID>.log
指向上面真实日志文件的软链,方便按 Pod / 容器名快速查找(新手优先看这个)
关键补充:
只有 Pod 运行在某个节点上,这个节点才会生成上述日志文件;在其他节点(比如 master)查不到对应文件;
0.log 是容器 stdout/stderr 的默认日志文件,日志轮转后会生成 0.log.1、0.log.2 等归档文件;
容器内 /var/log 为空是正常的 —— 日志不在容器 “内部空间”,而在节点的 “主机空间”。
一、节点级 Agent 采集(最常用)
实现方式 :在每个节点部署一个采集 Agent(如 Fluentd、Filebeat、Fluent Bit),从节点本地的容器日志文件(/var/log/containers、/var/log/pods)中读取日志,然后转发到远端存储(如 Elasticsearch)。
优点 :对应用和 Pod 无侵入性,只需在节点部署一个 Agent,资源消耗相对较低。
缺点 :要求应用必须将日志直接输出到容器的 stdout 和 stderr。
组件
核心作用
为什么必须要?
应用 stdout
日志输出统一出口
对应用无侵入,符合云原生最佳实践
containerd 轮转
限制节点日志文件大小 / 数量,避免磁盘打爆
基础保障,防止节点磁盘被日志占满
Filebeat
节点级采集,断点续传
单节点一个 Agent,运维成本极低
Kafka(可选,大日志量建议)
削峰填谷,解耦采集和存储
避免 ES/Loki 压力大时丢日志、阻塞采集
Logstash(可选)
日志清洗 / 格式化(可选)
统一日志格式,方便检索
ES/Loki
日志存储 + 检索
ES 适合全文检索,Loki 适合轻量 / 低成本
二、Sidecar 容器重定向采集(补充方案)
实现方式 :当应用只能将日志写入容器内文件时,通过一个 Sidecar 容器挂载该日志文件,将其内容重新输出到 Sidecar 的 stdout/stderr,再复用第一种方案进行采集。
适用场景 :应用无法修改为输出到标准输出的场景,是对第一种方式的补充。
三、Sidecar 容器直接采集(延伸方案)
实现方式 :在 Pod 中部署一个 Sidecar 容器(如 Fluentd),由它直接读取应用的日志文件,并将日志发送到远端存储(如 Elasticsearch)。
优点 :无需依赖节点级 Agent,可直接在 Pod 内完成采集。
缺点 :Sidecar 容器会消耗额外资源,当日志量较大时,需要为容器挂载额外存储来保存日志输出。
总结建议
日志量不大时,优先选择第一种方式 (节点级 Agent + 应用输出到 stdout/stderr),这是云原生的最佳实践。
日志量较大时,需要配合存储方案,避免节点磁盘被打满或日志丢失。(核心目标就是让日志 “不落地节点磁盘”,直接由采集组件(sidecar)推到 Kafka,再走后续流程)
第十五章 排查
pod启动失败
镜像拉取失败
先kubectl describe一下,看一下event,知道大概的原因,然后再单独拉取镜像,看看是什么问题。
摘抄自:慕课网 Kubernetes 入门到进阶实战,系统性掌握 K8s 生产实践