笔记
0工具
0此页用于记录用户反馈问题后的每一次改进
关于
“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
不想公开、只想临时给人看:点“分享”生成一条带密码和有效期的链接,到期自动失效,你也能随时撤销。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
趋势
第1部分 — 容器化应用并在本地Kubernetes中运行
嘿,各位!今天咱们来搞点实际的东西。这篇笔记是我“DevOps 101”系列的第一篇,目标是带你从零开始,把一个空文件夹变成能在本地Kubernetes集群里跑起来的应用。整个过程大概40分钟,跟着我一步步走,保证你学完能自己动手。
先说说你能学到啥:
- 用Node.js + Express写一个简单的Todo API
- 把它打成一个Docker镜像(容器化)
- 用kind在本地建一个Kubernetes集群
- 手写YAML文件部署应用(不用那些偷懒的快捷命令),真正理解K8s怎么干活
- 从你本机访问集群里跑着的应用
这个系列叫“DevOps 101 — 从零学K8s、Helm、ArgoCD”,所有部分都用同一个叫todo-ops的应用。后面我们会慢慢加数据库、配置、Ingress、Helm,最后用ArgoCD做GitOps。今天这第一部分就是地基。
准备工作
先把工具装好。你需要这些:
- Docker(或者Docker Desktop / OrbStack)—— 确保它在运行
- kind —— 在Docker里创建K8s集群的工具
- kubectl —— 和Kubernetes对话的命令行工具
- Node.js 20+ 和 npm —— 本地跑应用用的
- git —— 版本管理,后面GitOps部分很关键
安装步骤
macOS用户(用Homebrew,没有的话先装一个):
# 装Homebrew(跳过如果你已经有了)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Docker Desktop(或者用OrbStack:brew install --cask orbstack)
brew install --cask docker
# 剩下的CLI工具
brew install kind kubectl node git
装完后打开Docker Desktop(或OrbStack),等它显示“Running”再继续。
Linux用户(Ubuntu/Debian):
# Docker Engine
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker "$USER" # 让你不用sudo就能跑docker,记得登出再登入生效
# kubectl
curl -LO "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -m 0755 kubectl /usr/local/bin/kubectl && rm kubectl
# kind
curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64
sudo install -m 0755 kind /usr/local/bin/kind && rm kind
# Node.js 20(通过NodeSource) + git
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs git
💡 如果你用的是Apple Silicon(M1/M2/M3),把上面Linux命令里的amd64改成arm64。macOS用brew装就不用操心这个。
装完后跑一下这些命令,每个都应该输出版本号,别报“command not found”:
docker --version
kind --version
kubectl version --client
node --version
git --version
为什么要容器化?
这问题其实挺经典的:你写的代码在你自己机器上跑得好好的,但到别人机器上或者服务器上就各种报错。原因无非是Node.js版本不对、缺了个库、环境变量没设对……烦死个人。
容器化就是解决这个痛点的。它把你的应用和它需要的所有东西(运行时、依赖、配置)打包成一个标准化的“盒子”。这个盒子在任何有Docker的地方跑起来都一样。Docker镜像就是那个静态的打包文件(有点像安装包),而容器就是运行起来的镜像(就像进程)。
第一步:创建项目
先建个目录,初始化git仓库:
mkdir todo-ops && cd todo-ops && git init
mkdir todo-ops:创建项目文件夹cd todo-ops:进去,之后所有命令都在这个目录里跑git init:把这个目录变成git仓库,方便追踪改动。后面Part 6做GitOps会用到
你应该看到输出:
Initialized empty Git repository in /你的路径/todo-ops/.git/
检查点1:跑pwd,路径应该以/todo-ops结尾。跑ls -a,应该能看到.git目录。如果搞错了(比如在父目录跑了git init),用rm -rf .git删掉那个错误的.git,然后cd到正确目录重来。
第二步:写Node.js应用
我们的Todo API很简单:一个HTTP服务,能查看和添加待办事项。这期先不接数据库,数据存在内存里——重启就没了,故意的,下期我们会换成Postgres。
需要两个文件:package.json(项目配置)和server.js(代码)。
2a. package.json
创建package.json:
{
"name": "todo-api",
"version": "1.0.0",
"description": "Todo API - DevOps 101",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.19.2"
}
}
解释一下各个字段:
name、version、description:项目基本信息main:入口文件scripts.start:定义了npm start等于跑node server.js。这样启动应用就不用记文件名了dependencies.express:声明我们需要Express框架。^4.19.2意思是“4.19.2及以上,但保持在4.x版本内”
2b. server.js
创建server.js:
const express = require("express");
const app = express();
const PORT = 3000;
// 允许Express解析请求体中的JSON(POST请求需要)
app.use(express.json());
// 临时“数据库”:一个内存数组
// 重启应用就丢了——下期我们会换成Postgres
let todos = [
{ id: 1, title: "读DevOps 101第1部分", done: false },
];
let nextId = 2;
// GET /todos → 返回所有待办事项(JSON格式)
app.get("/todos", (req, res) => {
res.json(todos);
});
// POST /todos → 添加一个新待办。请求体需要包含{"title": "..."}
app.post("/todos", (req, res) => {
const title = req.body && req.body.title;
if (!title) {
return res.status(400).json({ error: "title is required" });
}
const todo = { id: nextId++, title, done: false };
todos.push(todo);
res.status(201).json(todo);
});
// GET /healthz → 健康检查探针。只要应用活着就返回200
app.get("/healthz", (req, res) => {
res.status(200).send("OK");
});
// 监听端口3000,绑定所有网络接口(0.0.0.0)——这在容器里很重要
app.listen(PORT, () => {
console.log(`Todo API listening on port ${PORT}`);
});
几个关键点:
express.json()是一个中间件,它在每个请求处理前运行,把JSON格式的请求体解析成req.body。没有这行,POST请求就读不到titletodos和nextId是内存状态,简单粗暴,方便学习。数据不持久- 三个路由对应三个端点:
GET /todos、POST /todos、GET /healthz /healthz返回200 OK——后面Part 4会用这个端点做Kubernetes的健康检查。提前写好,保持一致app.listen(PORT)打开端口等待请求。console.log让我们知道应用已经启动
检查点2:跑ls,应该看到package.json和server.js。跑node -c server.js检查语法——没有输出就是对的。如果报错,看看是哪行语法有问题(漏括号、漏逗号之类的),修好再试。
第三步:本地跑起来看看
在容器化之前,先确认应用在裸机(你本机)上能跑。这是个好习惯:出问题好定位——如果这步就挂了,那问题不在Docker或K8s,在代码本身。
npm install
这行命令会读package.json,下载Express库到node_modules文件夹,并生成package-lock.json锁定版本。输出大概是这样的:
added 63 packages, and audited 64 packages in 2s
然后启动应用:
npm start
你应该看到:
> todo-api@1.0.0 start
> node server.js
Todo API listening on port 3000
好,应用在跑了!现在打开另一个终端窗口,测试一下:
# 获取所有待办事项
curl http://localhost:3000/todos
你应该得到:
[{"id":1,"title":"读DevOps 101第1部分","done":false}]
再试试添加一个:
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"title": "学Kubernetes"}'
返回:
{"id":2,"title":"学Kubernetes","done":false}
再查一次GET /todos,应该能看到两条记录了。最后测健康检查:
curl http://localhost:3000/healthz
输出应该是OK。
搞定了就按Ctrl+C停掉应用。
检查点3:跑完npm install后,node_modules目录和package-lock.json文件应该存在。三个curl命令都应该返回正确的响应。如果有问题,检查是不是端口被占了(试试换端口),或者代码有没有拼写错误。
第四步:写Dockerfile
现在要把应用打包成Docker镜像。Dockerfile就是一个配方文件,告诉Docker怎么构建镜像。
创建Dockerfile(注意没有扩展名):
# 第一阶段:依赖安装
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
# 第二阶段:运行
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY server.js ./
EXPOSE 3000
CMD ["node", "server.js"]
解释一下这个Dockerfile在干嘛:
第一阶段(deps):
FROM node:20-alpine AS deps:基于Node.js 20的Alpine Linux镜像(超轻量,才几十MB),并给这个阶段起名叫depsWORKDIR /app:设置工作目录为/appCOPY package.json package-lock.json ./:只复制依赖配置文件,不复制代码。这样可以利用Docker的缓存机制——只要依赖没变,这层就不用重跑RUN npm ci --only=production:npm ci和npm install类似,但它严格按package-lock.json装依赖,更快更可靠。--only=production意思是只装生产依赖(不装devDependencies),减小镜像体积
第二阶段(runner):
FROM node:20-alpine AS runner:又是一个干净的Node.js Alpine镜像WORKDIR /app:同样设置工作目录COPY --from=deps /app/node_modules ./node_modules:从第一阶段复制装好的node_modules过来。这样最终镜像里只有依赖,没有构建工具COPY server.js ./:复制应用代码EXPOSE 3000:告诉Docker这个容器会监听3000端口(这只是文档说明,不实际打开端口)CMD ["node", "server.js"]:容器启动时执行的命令
这种多阶段构建的好处:最终镜像只包含运行需要的东西,不包含npm、构建工具等,体积小、安全。你可以对比一下——如果只用单阶段,镜像可能200MB+,现在只有120MB左右。
第五步:构建并测试Docker镜像
5a. 构建镜像
docker build -t todo-ops:latest .
docker build:构建命令-t todo-ops:latest:给镜像打标签,名字叫todo-ops,版本latest.:构建上下文是当前目录(Dockerfile所在位置)
运行后你会看到类似这样的输出:
[+] Building 15.2s (10/10) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 308B
=> [internal] load .dockerignore
=> => transferring context: 2B
=> [internal] load metadata for docker.io/library/node:20-alpine
=> [deps 1/3] FROM docker.io/library/node:20-alpine
=> [internal] load build context
=> => transferring context: 586B
=> [deps 2/3] COPY package.json package-lock.json ./
=> [deps 3/3] RUN npm ci --only=production
=> [runner 1/3] WORKDIR /app
=> [runner 2/3] COPY --from=deps /app/node_modules ./node_modules
=> [runner 3/3] COPY server.js ./
=> exporting to image
=> => naming to docker.io/library/todo-ops:latest
看到FINISHED就说明成功了。跑一下确认:
docker images
应该能看到todo-ops,大小大概120MB。
5b. 测试容器
先跑起来:
docker run -d --name todo-container -p 3000:3000 todo-ops:latest
-d:后台运行(detached模式)--name todo-container:给容器起个名字-p 3000:3000:把主机的3000端口映射到容器的3000端口。左边的3000是主机端口,右边是容器端口todo-ops:latest:要运行的镜像
跑完后,用docker ps确认容器在运行。然后测试:
curl http://localhost:3000/todos
应该和之前一样返回数据。测试完停掉并删除容器:
docker stop todo-container && docker rm todo-container
检查点4:docker images应该显示todo-ops镜像。docker run之后,docker ps应该显示容器在运行。curl请求应该返回正确结果。如果容器起不来,用docker logs todo-container看日志。
第六步:创建本地Kubernetes集群
终于到K8s了!我们用kind(Kubernetes in Docker)在本地创建集群。kind实际上是在Docker容器里跑K8s节点,所以你的Docker Desktop得开着。
kind create cluster --name todo-cluster
kind create cluster:创建集群--name todo-cluster:给集群起个名字,方便后面管理
输出应该是:
Creating cluster "todo-cluster" ...
✓ Ensuring node image (kindest/node:v1.31.0) 🖼
✓ Preparing nodes 📦
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
Set kubectl context to "kind-todo-cluster"
You can now use your cluster with:
kubectl cluster-info --context kind-todo-cluster
检查一下集群状态:
kubectl cluster-info --context kind-todo-cluster
应该看到类似:
Kubernetes control plane is running at https://127.0.0.1:xxxxx
CoreDNS is running at https://127.0.0.1:xxxxx/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
再看看节点:
kubectl get nodes
输出:
NAME STATUS ROLES AGE VERSION
todo-cluster-control-plane Ready control-plane 2m v1.31.0
很好!一个单节点集群已经跑起来了。这个节点其实是一个Docker容器,你可以用docker ps看到它。
检查点5:kubectl cluster-info不报错。kubectl get nodes显示一个Ready状态的节点。如果kubectl连不上,检查一下context对不对:kubectl config current-context应该显示kind-todo-cluster。
第七步:把镜像加载到集群
这里有个坑:我们刚才构建的镜像在本地Docker里,但kind集群跑在Docker容器里——它们是隔离的。所以我们需要把镜像加载到集群节点里。
kind load docker-image todo-ops:latest --name todo-cluster
kind load docker-image:加载镜像到集群节点todo-ops:latest:要加载的镜像--name todo-cluster:指定集群名字
输出:
Image: "todo-ops:latest" with ID "sha256:xxxxx" not yet present on node "todo-cluster-control-plane"
Loading image "todo-ops:latest" ...
搞定。现在集群节点里有了这个镜像。
检查点6:命令不报错。你可以用docker exec进到集群节点里确认:docker exec -it todo-cluster-control-plane crictl images | grep todo-ops,应该能看到镜像。
第八步:部署应用到Kubernetes
现在要写真正的Kubernetes部署文件了。我们会创建两个资源:
- Deployment:告诉K8s“我要跑一个应用,保持它一直运行”
- Service:让集群内外的其他东西能访问这个应用
8a. 创建命名空间
先创建一个专用命名空间,把资源组织起来:
kubectl create namespace todo-ops
输出:namespace/todo-ops created
8b. Deployment YAML
创建k8s/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: todo-api
namespace: todo-ops
labels:
app: todo-api
spec:
replicas: 2
selector:
matchLabels:
app: todo-api
template:
metadata:
labels:
app: todo-api
spec:
containers:
- name: todo-api
image: todo-ops:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
逐行解释:
apiVersion: apps/v1:Deployment属于apps/v1API组,这是稳定版kind: Deployment:资源类型metadata.name:Deployment的名字metadata.namespace:所属命名空间metadata.labels:Deployment自己的标签spec.replicas: 2:要跑2个Pod副本(高可用)spec.selector.matchLabels:Deployment通过这个标签选择它管理的Podspec.template:Pod模板,定义Pod长什么样metadata.labels:Pod的标签,必须匹配上面的matchLabelsspec.containers:容器定义name:容器名字image:要跑的镜像imagePullPolicy: IfNotPresent:如果镜像本地没有才去拉。因为我们是用kind load加载的,所以设为这个值ports.containerPort:容器监听的端口resources:资源限制requests:K8s保证至少给这么多资源limits:最多能用这么多- 内存单位:
Mi=兆字节,Gi=吉字节 - CPU单位:
100m=0.1个CPU核心,200m=0.2个核心
8c. Service YAML
创建k8s/service.yaml:
apiVersion: v1
kind: Service
metadata:
name: todo-api-service
namespace: todo-ops
spec:
type: NodePort
selector:
app: todo-api
ports:
- protocol: TCP
port: 3000
targetPort: 3000
nodePort: 30080
解释:
apiVersion: v1:Service是核心API,属于v1kind: Service:资源类型spec.type: NodePort:服务类型。NodePort会在每个集群节点上开一个端口(30080),把外部流量转发到Podspec.selector:通过标签选择后端Pod(必须匹配Deployment里Pod的标签)spec.ports:端口映射port:Service自己的端口(集群内部访问用)targetPort:转发到Pod的哪个端口nodePort:节点上开的端口(范围30000-32767)。不指定的话K8s会随机分配
8d. 应用YAML
先创建k8s目录:
mkdir k8s
然后把两个YAML文件放进去。现在部署:
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
输出:
deployment.apps/todo-api created
service/todo-api-service created
检查状态:
kubectl get pods -n todo-ops
应该看到两个Pod都在Running状态:
NAME READY STATUS RESTARTS AGE
todo-api-xxxxx-xxxxx 1/1 Running 0 30s
todo-api-xxxxx-xxxxx 1/1 Running 0 30s
再看看Service:
kubectl get svc -n todo-ops
输出:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
todo-api-service NodePort 10.96.xxx.xxx <none> 3000:30080/TCP 30s
检查点7:两个Pod都是Running状态。Service显示NodePort类型,端口映射是3000:30080。如果Pod一直Pending或CrashLoopBackOff,用kubectl describe pod <pod名> -n todo-ops和kubectl logs <pod名> -n todo-ops排查。
第九步:从本机访问Kubernetes里的应用
现在应用跑在集群里了,怎么访问它?因为我们用的是NodePort类型Service,可以通过集群节点的IP加NodePort来访问。
先获取集群节点的IP:
kubectl get nodes -o wide
输出:
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP
todo-cluster-control-plane Ready control-plane 5m v1.31.0 172.18.0.2 <none>
内部IP是172.18.0.2,但这是Docker网络内部的地址,从你的宿主机不一定能直接访问。别急,kind有办法——它会把节点端口映射到宿主机。
实际上,kind创建集群时,默认会把控制平面节点的某些端口映射出来。但更靠谱的方式是用kubectl port-forward:
kubectl port-forward service/todo-api-service 8080:3000 -n todo-ops
这个命令会在本地开一个端口8080,把流量转发到集群里的Service的3000端口。输出:
Forwarding from 127.0.0.1:8080 -> 3000
Forwarding from [::1]:8080 -> 3000
现在打开另一个终端窗口,测试:
curl http://localhost:8080/todos
应该返回:
[{"id":1,"title":"读DevOps 101第1部分","done":false}]
再加一个:
curl -X POST http://localhost:8080/todos \
-H "Content-Type: application/json" \
-d '{"title": "成功部署到K8s"}'
返回:
{"id":2,"title":"成功部署到K8s","done":false}
再查健康检查:
curl http://localhost:8080/healthz
输出:OK
牛逼!你的应用成功跑在Kubernetes集群里了!而且有两个副本,如果一个挂了,另一个还能服务。
检查点8:kubectl port-forward命令在后台运行,不报错。三个curl请求都返回正确结果。如果连不上,检查port-forward是不是还在跑,或者试试curl -v看详细错误。
清理
测试完了,关掉port-forward(按Ctrl+C)。然后可以删掉集群:
kind delete cluster --name todo-cluster
输出:Deleted clusters: ["todo-cluster"]
这会删掉整个集群,包括所有Pod、Service等资源。本地镜像还在,下次可以直接用。
小结
今天你做了这些事:
- 写了一个Node.js Todo API应用
- 用Dockerfile容器化了它
- 在本地用kind创建了Kubernetes集群
- 手写YAML文件部署了Deployment和Service
- 通过port-forward从本机访问了集群里的应用
这些是K8s最基础但最核心的概念。下一期我们会加入Postgres数据库,用ConfigMap和Secret管理配置,让应用真正有状态。
有什么问题随时留言讨论!