任务目标
搭建一套 GitOps 持续交付流水线,实现代码提交即自动部署。
具体目标如下:
搭建私有镜像仓库 Harbor:存储所有容器镜像,为离线部署提供镜像源
搭建 Git 代码仓库 Gitea:存放 Kubernetes 部署清单(yaml 文件)
搭建 K3s 集群:包含一个 Master 节点和一个 Worker 节点,作为容器应用的运行环境
部署 Argo CD:作为 GitOps 调度引擎,自动监听 Git 仓库变更并同步到 K3s 集群
验证完整流程:修改 Gitea 仓库中的 YAML 文件(如调整副本数),Argo CD 自动检测变更并将新状态同步到 K3s 集群,最终业务应用无中断更新
任务环境
三个节点各司其职
VM1:192.168.106.102,承载 Harbor,存储所有容器镜像
VM2:192.168.106.103,承载 Gitea(代码仓库)+ ArgoCD+ K3s Server
VM3:192.168.106.104,承载 K3s Agent,运行业务应用容器
网络拓扑图参考如下:

为什么 Gitea、ArgoCD、K3s Server 最好部署在同一台机器上?
1. 减少网络延迟,提升响应速度
GitOps 的核心流程是:开发者提交代码到 Gitea → ArgoCD 检测到变更 → ArgoCD 通知 K3s Server 更新部署。
如果这三个组件分别部署在不同机器上,每次变更都需要跨节点网络通信。部署在同一台机器上时,这些通信都走本地回环(localhost),几乎没有网络延迟,同步速度极快。
2. 简化认证和权限管理
ArgoCD 需要访问 K3s 集群的 API Server,如果部署在同一台机器上,可以直接使用本地的 kubeconfig 文件(默认路径 /etc/rancher/k3s/k3s.yaml),无需额外配置 RBAC 或证书。
第一部分:部署harbor(VM1)
第一步:配置网络
首先需要配置服务器的网络参数,因为后续 Harbor 私有仓库和 Kubernetes 集群都需要稳定的网络环境。
sudo nano /etc/netplan/50-cloud-init.yaml在文件中根据实际网络环境设置静态 IP 或保持 DHCP,确保服务器的 IP 地址固定,后续所有服务都依赖这个地址。

sudo netplan try #临时应用
sudo netplan apply #使网络配置正式生效第二步:配置 Docker 允许访问 HTTP 私有仓库
Docker 默认只允许与 HTTPS 协议的镜像仓库通信,但我们即将部署的 Harbor 私有仓库将使用 HTTP 协议(因为没有配置 SSL 证书),所以需要修改 Docker 的配置来允许不安全的 HTTP 连接。
sudo nano /etc/docker/daemon.json{
"insecure-registries": ["http://192.168.106.102"]
}这个配置告诉 Docker 守护进程:对于地址为 http://192.168.106.102 的镜像仓库,不需要强制使用 HTTPS 连接,可以接受 HTTP 协议。
sudo systemctl daemon-reload # 重新读取这个配置文件
sudo systemctl restart docker #重启 Docker 服务第三步:安装Harbor
Harbor 是一个企业级的 Docker 镜像仓库,我们使用离线安装包进行部署,因为在线安装可能受网络环境影响。
tar -xvf harbor-offline-installer-v2.14.3.tgz #里面包含了 Harbor 的所有安装文件、Docker 镜像包和安装脚本
cp harbor.yml.tmpl harbor.yml
sudo nano harbor.yml #使用配置模板编辑使用配置模板主要修改以下几项:
hostname:设置为 192.168.106.102,这是 Harbor 服务对外提供访问的地址
http.port:默认为 80 端口,保持不变
https:由于我们没有 SSL 证书,需要将整个 HTTPS 部分注释掉(在每一行前加 #),因为 Harbor 默认会尝试启用 HTTPS
harbor_admin_password:设置管理员密码,例如设置为 Harbor12345,这个密码用于登录 Harbor Web 界面
data_volume:数据存储路径,保持默认的 /data 即可
sudo bash ./install.sh #执行 Harbor 安装脚本
安装成功后,可以通过浏览器访问 http://192.168.106.102 来打开 Harbor 的 Web 界面。
第四步:上传所需镜像
登录harbor

sudo docker load < core-images.tar #将 core-images.tar 这个打包好的镜像文件加载到本地 Docker 的镜像仓库中
sudo docker images #命令查看当前 Docker 中的所有镜像列表
Docker 镜像的完整名称格式是 仓库地址/项目名/镜像名:版本号。
现在我们需要将本地镜像打上 Harbor 仓库的标签,这样推送时 Docker 就知道要传到哪个仓库的哪个项目下。
sudo docker tag gitea/gitea:latest 192.168.106.102/library/gitea:latest
sudo docker tag nginx:alpine 192.168.106.102/library/nginx:alpine
sudo docker tag redis:7-alpine 192.168.106.102/library/redis:7-alpine
sudo docker tag alpine:latest 192.168.106.102/library/alpine:latest
sudo docker tag quay.io/argoproj/argocd:v2.13.3 192.168.106.102/library/argocd:v2.13.3
sudo docker tag ghcr.io/dexidp/dex:v2.41.1 192.168.106.102/library/dex:v2.41.1将打好标签的镜像推送到 Harbor 仓库中。
sudo docker push 192.168.106.102/library/gitea:latest
sudo docker push 192.168.106.102/library/nginx:alpine
sudo docker push 192.168.106.102/library/redis:7-alpine
sudo docker push 192.168.106.102/library/alpine:latest
sudo docker push 192.168.106.102/library/argocd:v2.13.3
sudo docker push 192.168.106.102/library/dex:v2.41.1常用的基础镜像都已上传到 library 项目中。后续部署 Gitea、ArgoCD 等组件时,Kubernetes 集群就可以从 192.168.106.102/library/ 这个私有仓库中拉取镜像,而无需访问外网或使用公共镜像仓库。
第二部分:部署Gitea+k3s-master+Argo CD(VM2)
第一步:配置网络和不安全仓库
设置VM2的内网IP为192.168.106.103,步骤参考VM1。
第二步:部署Gitea
编写yaml文件。
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
restart: always
environment:
- USER_UID=1000
- USER_GID=1000
volumes:
- ./data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:22"从刚刚搭建好的 Harbor 私有仓库中拉取 Gitea 镜像。
sudo docker pull 192.168.106.102/library/gitea:latest
sudo docker tag 192.168.106.102/library/gitea:latest gitea/gitea:latest #创建一个新的标签访问http://192.168106.103:3000,进入Gitea初始化界面。
在初始化页面中,需要注意设置以下内容:
数据库设置:使用默认的 SQLite
仓库根路径:保持默认
HTTP 端口:默认 3000 端口
基础 URL:填写内网地址,例如 http://192.168.106.103:3000
完成所有配置后,进行初始化。初始化完成后,可以使用设置的管理员用户名和密码登录 Gitea。
第三步:部署k3s-master
在能够连接的外网的机器上执行以下命令,拉取所需要的镜像、文件等。
#拉取k3s二进制执行文件集成了 API Server、调度器等所有功能
wget https://github.com/k3s-io/k3s/releases/download/v1.31.1%2Bk3s1/k3s
#Airgap离线镜像包,内置k3s所运行所需的所有镜像
wget https://github.com/k3s-io/k3s/releases/download/v1.31.1%2Bk3s1/k3s-airgap-images-amd64.tar.gz
#官方安装包脚本自动配置系统服务,识别本地的二进制文件
curl -sfL https://get.k3s.io -o install.sh
#下载官方Argo CD官方清单
curl -L https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml -o argocd-install.yaml使用 scp 命令将下载好的文件传输到目标服务器
scp install.sh yxwa@172.16.11.108:~/k3s/
scp argocd-install.yaml yxwa@172.16.11.108:~/k3s/
scp k3s yxwa@172.16.11.108:~/k3s/
scp k3s-airgap-images-amd64.tar.gz yxwa@172.16.11.108:~/k3s/返回到VM2。

chmod +x ~/k3s/k3s #添加可执行权限将 k3s 二进制文件复制到 /usr/local/bin 目录下,这个目录在系统的 PATH 环境变量中,这样在任何位置都可以直接执行 k3s 命令。
sudo cp ~/k3s/k3s /usr/local/bin/k3s
sudo mkdir -p /var/lib/rancher/k3s/agent/images/将离线镜像包复制到刚刚创建的目录中,这样 k3s 安装时会自动从这个目录读取镜像,而无需从网络下载。
sudo cp ~/k3s/k3s-airgap-images-amd64.tar.gz /var/lib/rancher/k3s/agent/images/执行 install.sh 脚本
sudo INSTALL_K3S_SKIP_DOWNLOAD=true sh ~/k3s/install.sh \
--node-ip=192.168.106.103 \ #指定 k3s 节点使用的 IP 地址为 192.168.106.103
--flannel-iface=enp6s19 \ #指定 Flannel 网络插件使用的网络接口为 enp6s19
--disable traefik \ #禁用默认安装的 Traefik 反向代理,因为后续会使用 Argo CD 自己的 NodePort
--write-kubeconfig-mode 644 #设置 kubeconfig 文件的权限为 644,允许普通用户读取,方便后续 kubectl 命令无需 sudo
kubectl get nodes
如果看到节点状态为 Ready,说明 k3s 集群安装成功并正常运行
第四步:部署Argo CD
为了让 k3s 从我们的 Harbor 私有仓库拉取镜像,需要配置 containerd(k3s 默认使用的容器运行时)的镜像仓库镜像。
sudo mkdir -p /etc/rancher/k3s创建镜像仓库镜像配置文件。
mirrors: # 镜像配置节
"192.168.106.102": # 目标仓库地址(镜像名称中的仓库部分)
endpoint: # 实际拉取时访问的端点
- "http://192.168.106.102" # 使用 HTTP 协议访问 Harborcontainerd 默认要求镜像仓库使用 HTTPS 协议。由于我们的 Harbor 使用 HTTP,需要明确告诉 containerd 允许与该仓库建立不安全的 HTTP 连接。
配置 containerd 对 HTTP 仓库的信任。
sudo mkdir -p /etc/containerd/certs.d/192.168.106.102/ #为特定仓库创建子目录
sudo tee /etc/containerd/certs.d/192.168.106.102/hosts.toml <<EOF
server = "http://192.168.106.102" # 仓库服务器地址
[host."http://192.168.106.102"] # 定义 HTTP 主机配置
capabilities = ["pull", "resolve"] # 允许拉取和解析镜像
EOF
sudo systemctl restart k3s #重启 K3s 服务因为需要从 Harbor 私有仓库拉取 Argo CD 相关的镜像,而不是从公共仓库拉取,所以需要修改安装清单中的镜像地址。
sed -i 's|quay.io/argoproj|192.168.106.102/library|g' argocd-install.yaml
sed -i 's|:v[0-9].*|:v2.13.3|g' argocd-install.yaml
sed -i 's|ghcr.io/dexidp/dex:.*|192.168.106.102/library/dex:v2.41.1|g' argocd-install.yaml
sed -i 's|public.ecr.aws/docker/library/redis:.*|192.168.106.102/library/redis:7-alpine|g' argocd-install.yaml检查是否修改成功,最终结果如下:

创建一个命名空间,所有 Argo CD 的资源都将部署在这个命名空间中。
kubectl create namespace argocd部署 Argo CD。
kubectl apply --server-side -n argocd -f argocd-install.yaml查看 Pod 状态,等待所有 Pod 进入 Running 状态。
kubectl get pods -n argocd
Argo CD Server 服务默认类型是 ClusterIP,只能在集群内部访问。为了能够从外部浏览器访问 Argo CD 界面,需要将其修改为 NodePort 类型。
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "NodePort", "ports": [{"port": 443, "targetPort": 8080, "nodePort": 30000}]}}' # 修改 ArgoCD Server 服务类型为 NodePort,并固定外部访问端口为 30000获取初始管理员密码并解码
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d如果部署过程中出现问题需要重新安装,可以执行以下命令清理。
kubectl delete -n argocd -f argocd-install.yaml #删除安装清单中定义的所有资源
kubectl delete namespace argocd --force --grace-period=0 #强制删除 argocd 命名空间
kubectl get all -n argocd #确认命名空间中的所有资源都已清理干净应该返回 "No resources found" 或提示命名空间不存在
第三部分:部署k3s-worker节点
第一步:配置网络和不安全仓库
设置VM3的内网IP为192.168.106.104,步骤参考VM1。
第二步:添加worker节点
在新的 Worker 节点服务器上创建安装目录
mkdir -p k3s给 k3s 二进制文件添加可执行权限
chmod +x ~/k3s/k3s
sudo cp ~/k3s/k3s /usr/local/bin/k3s给安装脚本添加可执行权限
chmod +x ~/k3s/install.sh
sudo mkdir -p /var/lib/rancher/k3s/agent/images/
sudo cp ~/k3s/k3s-airgap-images-amd64.tar.gz /var/lib/rancher/k3s/agent/images/将VM2和VM3进行对接。
#在VM2上获取token
sudo cat /var/lib/rancher/k3s/server/node-token
#执行安装命令,将当前节点以 Worker 角色加入到已有的 k3s 集群中
sudo K3S_URL=https://192.168.106.103:6443 \ #指定 k3s 集群的 API Server 地址
K3S_TOKEN=K10133e4f8fb654c9b5909e688e81287fbd6fd20450894b7ab3385f41818e0ebb2c::server:3b350fbf70edc89a6a975ddb114b1457 \ #指定加入集群所需的认证令牌,这个令牌需要从 Master 节点获取
INSTALL_K3S_SKIP_DOWNLOAD=true \ #跳过下载步骤,因为我们已经在本地准备好了 k3s 二进制文件和离线镜像包
/home/yxwa/k3s/install.sh \ #指定安装脚本的完整路径
--node-ip=192.168.106.104 \ #指定当前 Worker 节点的 IP 地址,这样集群中的网络通信就可以使用这个地址
--flannel-iface=enp6s19 \
--node-name worker-01 \ #给当前节点指定一个易于识别的名称,在 kubectl get nodes 命令中会显示这个名字在VM2上执行:
sudo kubectl get nodes
如果看到 worker-01 节点的状态为 Ready,说明 Worker 节点已经成功加入集群。
第三步:降级步骤
该部分可参考VM2。
sudo mkdir -p /etc/rancher/k3s
sudo tee /etc/rancher/k3s/registries.yaml <<EOF
mirrors:
"192.168.106.102":
endpoint:
- "http://192.168.106.102"
configs:
"192.168.106.102":
tls:
insecure: true
EOF
sudo mkdir -p /etc/containerd/certs.d/192.168.106.102/
sudo tee /etc/containerd/certs.d/192.168.106.102/hosts.toml <<EOF
server = "http://192.168.106.102"
[host."http://192.168.106.102"]
capabilities = ["pull", "resolve"]
EOF
sudo systemctl restart k3s-agent #重启worker节点第四部分:检验
Gitea创建一个新公开仓库,在仓库根目录下新建一个yaml文件。


apiVersion: apps/v1
kind: Deployment
metadata:
name: my-web
namespace: my-web
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: 192.168.106.102/library/nginx:alpine
---
apiVersion: v1
kind: Service
metadata:
name: my-web-svc
namespace: my-web
spec:
type: NodePort
selector:
app: web
ports:
- port: 80
targetPort: 80
nodePort: 30080这个文件定义了一个名为 my-web 的 Deployment(初始副本数为 2)和一个 NodePort 类型的 Service(端口 30080),提交该文件到 main 分支。

访问https://192.168.106.102:30000,进入 Argo CD 界面,点击NEW APP创建一个新的应用。

在通用信息部分,将应用命名为 ops,项目选择默认的 default,并将同步策略设置为 Automatic,这样后续 Git 仓库有任何变更,Argo CD 就能自动同步到 Kubernetes 集群。

在源码配置部分,仓库 URL 填写 Gitea 仓库的地址,Revision 保持 HEAD 以跟踪主分支最新提交,Path 填 . 表示 YAML 文件就在根目录。
在目标部署部分,集群 URL 选择 https://kubernetes.default.svc(指向当前 Kubernetes 集群),命名空间填写 my-web,与 YAML 文件中定义的命名空间保持一致。最后点击 CREATE 完成创建。

创建成功后,Argo CD 会自动检测到 Git 仓库中的 YAML 文件,并将 Deployment 和 Service 部署到 Kubernetes 集群的 my-web 命名空间中。此时在应用详情页面可以看到同步状态为 Synced,健康状态为 Healthy,并且能看到两个 Pod 正常运行。

为了验证自动更新能力,回到 Gitea 再次编辑 test.yaml 文件,将 replicas: 2 修改为 replicas: 4,然后提交变更。

回到 Argo CD 界面,点击刷新按钮,应用状态先是短暂变为 OutOfSync(表示集群实际状态与 Git 仓库期望状态不一致),随后由于我们开启了自动同步策略,状态会自动变回 Synced。

此时查看 Kubernetes 集群中的 Pod 数量,会发现已经从 2 个增加到了 4个,说明 Argo CD 成功自动拉取了 Git 仓库的最新配置并完成了部署。

如果在某个环节出现 OutOfSync 状态长时间没有自动恢复,可以手动干预:点击应用进入详情页,再点击SYNC,点击SYNCHRONIZE即可手动完成同步,让集群状态与 Git 仓库重新保持一致。

打开浏览器访问http://172.16.11.104:30080

为什么业务容器的访问地址是 172.16.11.104?
因为 test.yaml 中定义的 Service 类型是 NodePort,并且指定了 nodePort: 30080。
当你在浏览器中访问 http://172.16.11.104:30080 时:
请求直接到达 VM3 工作节点 的 30080 端口
工作节点上的 kube-proxy 会将流量转发到对应的 Pod
理论上,集群中任意节点的 IP + NodePort 都可以访问,因为 kube-proxy 会在每个节点上监听 NodePort。
但前提是:
对应节点的防火墙放行了 30080 端口
对应节点上 kube-proxy 正常运行
通常建议直接访问 Worker 节点的 IP,因为业务流量不应该经过控制节点,这样符合控制平面与数据平面分离的架构原则。
