从零构建Alpine中间件镜像并
部署至openEuler单Master K8s集群的完整实践
引言
在云原生时代,Kubernetes 已成为容器编排的事实标准。容器镜像作为应用的最终交付载体,其安全性与可控性至关重要。越来越多的企业出于安全合规、离线部署以及深度定制的需求,要求不使用公共镜像仓库的现成镜像 ,而是从零开始,手动构建所有中间件。本实验正是基于这一思想,在 openEuler 24.03 SP3 系统 的单 Master 节点 Kubernetes 集群上,以 Alpine Linux 为基础,手工制作 Nginx、MariaDB、DNS (BIND)、Redis 四种中间件镜像,通过 K8s 实现单 Pod 部署,并最终验证所有服务的可行性。整个过程采用 docker + cri-dockerd 的容器运行时,完全离线可控,体现了"自建镜像、完全掌控"的工程实践。
一、理论基础与功能说明
1.1 为什么选择 Alpine Linux?
Alpine Linux 是一个面向安全的轻量级 Linux 发行版,其基础镜像大小仅约 3 MB。它使用 musl libc 和 BusyBox,专为容器环境优化。相比 Ubuntu、openEuler 等动辄百兆的基础镜像,Alpine 能够显著减小最终镜像的体积,加快分发和启动速度。其包管理器 apk 简洁高效,拥有丰富的软件包支持,非常适合制作精简的中间件镜像。
1.2 中间件功能简介
-
Nginx:高性能的 HTTP 和反向代理服务器,常用于 Web 服务、负载均衡与静态资源托管。
-
MariaDB:MySQL 数据库的经典分支,完全兼容 MySQL 协议与语法,是广泛使用的关系型数据库。
-
DNS (BIND):互联网上应用最广泛的域名解析服务软件,支持正向(域名到IP)和反向(IP到域名)解析。
-
Redis:基于内存的高性能键值存储系统,常用于缓存、会话管理、消息队列等场景。
1.3 Kubernetes Pod 与单Pod部署
Pod 是 Kubernetes 最小的调度单元,可以包含一个或多个容器。本实验采用"单 Pod 部署"策略,为每个中间件创建一个独立的 Pod,非常符合作业的直观要求。相比 Deployment 主要管理多副本和滚动更新,直接使用 Pod 对象在实验场景下更简洁明确。
1.4 自建镜像 vs 官方镜像
拉取官方镜像虽然方便,但可能包含不必要的组件、未被审计的漏洞,且版本受制于维护者。自建镜像让我们能够:
-
掌控每一层内容,确保安全与合规;
-
深度定制配置文件(如 nginx.conf、my.cnf);
-
实现完全离线交付,无需依赖外部仓库。
本次实验完全贯彻这一原则:从 Alpine 裸系统开始,手动安装软件、写入配置、设置权限,最终生成可直接使用的镜像。
二、实验环境与架构
2.1 集群节点与网络规划
实验集群由三台运行 openEuler 24.03 SP3 的虚拟机构成,相关环境信息如下表所示。
| 节点角色 | 主机名 | IP 地址 | 操作系统 |
|---|---|---|---|
| Master (兼 Node) | k8s-master | 192.168.64.128 | openEuler 24.03 SP3 |
| Worker Node 1 | k8s-node1 | 192.168.64.129 | openEuler 24.03 SP3 |
| Worker Node 2 | k8s-node2 | 192.168.64.130 | openEuler 24.03 SP3 |
核心软件与运行时:
-
容器运行时 :Docker + cri-dockerd(因 Kubernetes 1.24 版本起移除了内置的
dockershim,必须额外安装cri-dockerd作为适配器,桥接 kubelet 的 CRI 接口与 Docker 的 API)。 -
软件版本:Kubernetes 1.28+ 系列,Docker 20.10+,以及最新稳定版的 cri-dockerd。
2.2 DNS 解析规划
我们规划了 hzx.com 作为内部域名,配置了以下解析记录,用于验证 DNS 服务的功能。
| 记录类型 | 域名或 IP | 指向 | 说明 |
|---|---|---|---|
| A (正向) | ns.hzx.com |
192.168.64.128 | 名称服务器记录 |
| A (正向) | www.hzx.com |
192.168.64.128 | Web 服务测试记录 |
| A (正向) | k8s-master.hzx.com |
192.168.64.128 | Master 节点记录 |
| A (正向) | k8s-node1.hzx.com |
192.168.64.129 | Worker 1 节点记录 |
| A (正向) | k8s-node2.hzx.com |
192.168.64.130 | Worker 2 节点记录 |
| PTR (反向) | 192.168.64.128 | k8s-master.hzx.com |
反向解析示例 |
三、镜像制作详细过程
制作目标是四个镜像:my-nginx:alpine、my-mariadb:alpine、my-dns:alpine、my-redis:alpine。所有构建在开发机(可联网)上完成,然后导出为 tar 包分发至各 Worker 节点。
3.1 Nginx 镜像
Nginx 作为本次实验的 Web 服务器,需要提供一个默认页面来验证服务是否正常启动。然而,Alpine 官方维护的 Nginx 包非常精简,存在两个需要解决的问题:
-
静态资源目录不匹配 :Alpine 默认将网页根目录设定在
/var/www/localhost/htdocs,这与我们日常使用的/var/www/html习惯不同。更重要的是,该目录默认为空,没有index.html,直接访问会触发 404 错误。 -
非 root 用户权限限制:Linux 系统严格限制非 root 用户绑定 1024 以下的特权端口,而 HTTP 的标准服务端口正是 80。
为了构建一个既简单又安全的镜像,我们进行了专门的设计:
-
统一目录并预置内容 :在 Dockerfile 中,我们主动创建约定俗成的
/var/www/html目录,并写入一句测试信息<h1>nginx ok</h1>作为默认首页。 -
主进程降权运行 :我们不设置
USER nginx,让容器以 root 身份启动,这样 Nginx 主进程就能顺利绑定 80 端口。同时,在 Nginx 的主配置文件/etc/nginx/nginx.conf中,我们设置了user nginx;这一指令,它会强制所有处理用户请求的 worker 进程切换到低权限的nginx用户,从而兼顾了服务功能与系统安全。
Dockerfile 及配置:
bash
FROM alpine:3.19
RUN apk add --no-cache nginx && \
mkdir -p /run/nginx /var/www/html && \
echo "nginx ok" > /var/www/html/index.html && \
chown -R nginx:nginx /run/nginx /var/www/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx
user nginx;
worker_processes 1;
events { worker_connections 1024; }
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 0.0.0.0:80; # 强制绑定IPv4,解决Alpine默认监听IPv6的问题
server_name localhost;
root /var/www/html;
index index.html index.htm;
}
}
关键点解释 :必须使用 listen 0.0.0.0:80; 明确绑定到 IPv4 地址。在某些环境下,简单的 listen 80; 可能默认绑定到 ::1:80,导致使用 IPv4 的 wget 连接被拒。实际测试中,wget http://127.0.0.1 能成功,而 wget http://localhost 则失败,原因正是 localhost 解析为了 ::1。
3.2 MariaDB 镜像
鉴于 Alpine 官方源不包含真正的 MySQL,而提供完全兼容且更轻量的 MariaDB,我们选择后者。MariaDB 默认仅允许 root 用户通过 Unix Socket 无密码登录,这会导致后续的 TCP 测试(mysql -u root)失败。构建过程中,我们通过先启动一个临时实例来修改 root 的认证方式。
Dockerfile 及配置:
bash
FROM alpine:3.19
RUN apk add --no-cache mariadb mariadb-client
COPY my.cnf /etc/mysql/my.cnf
RUN mysql_install_db --user=mysql --datadir=/var/lib/mysql && \
chown -R mysql:mysql /var/lib/mysql
RUN mkdir -p /run/mysqld && chmod 755 /run/mysqld && chown mysql:mysql /run/mysqld
# 后台启动,修改root认证方式,允许TCP无密码登录
RUN mysqld --user=mysql --datadir=/var/lib/mysql --skip-name-resolve & \
sleep 3; \
mysql -u root -S /run/mysqld/mysqld.sock -e \
"ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password; FLUSH PRIVILEGES;"; \
killall mysqld; sleep 2; \
rm -f /run/mysqld/mysqld.sock
VOLUME /var/lib/mysql
EXPOSE 3306
USER mysql
CMD ["mysqld", "--user=mysql", "--skip-name-resolve"]
ini
[mysqld]
datadir=/var/lib/mysql
socket=/run/mysqld/mysqld.sock
port=3306
skip-name-resolve
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
关键点解释 :提前创建 /run/mysqld 目录并设定权限,防止服务启动时因找不到 socket 目录而崩溃。通过 ALTER USER ... IDENTIFIED VIA mysql_native_password 将认证方式改为兼容模式,使得 mysql -u root 可以通过 TCP 连接。
3.3 DNS (BIND) 镜像
BIND 需要提供正向和反向区域文件。其默认以非 root 用户 named 运行,无法直接绑定 53 特权端口。因此,需要通过 setcap 为 named 二进制程序赋予 cap_net_bind_service 的能力。
Dockerfile 及配置:
bash
FROM alpine:3.19
RUN apk add --no-cache bind libcap
COPY named.conf /etc/bind/named.conf
COPY hzx.com.zone /var/bind/hzx.com.zone
COPY 64.168.192.zone /var/bind/64.168.192.zone
RUN deluser named 2>/dev/null || true && \
adduser -D -H named && \
mkdir -p /var/bind /var/log/named && \
chown -R named:named /var/bind /var/log/named /etc/bind \
/var/bind/hzx.com.zone /var/bind/64.168.192.zone && \
setcap cap_net_bind_service=+ep /usr/sbin/named
EXPOSE 53/tcp 53/udp
USER named
CMD ["sh", "-c", "named-checkconf /etc/bind/named.conf && exec named -g -c /etc/bind/named.conf -u named"]
bash
# named.conf 关键部分
options {
directory "/var/bind";
listen-on { any; };
listen-on-v6 { none; }; # 禁用IPv6监听,避免干扰
allow-query { any; };
...
};
zone "hzx.com" IN {
type master;
file "hzx.com.zone";
};
zone "64.168.192.in-addr.arpa" IN { # 反向区名必须符合规范
type master;
file "64.168.192.zone";
};
关键点解释:
-
反向区域名称 :必须是
64.168.192.in-addr.arpa,不能误写为文件名64.168.192.zone。否则 BIND 会将文件名当作区域名,导致反向解析失败(日志显示zone 64.168.192.zone/IN loaded)。 -
禁用 IPv6 :通过
listen-on-v6 { none; };避免 IPv6 地址干扰,是实践中一个重要的排错手段。 -
能力设置 :
setcap cap_net_bind_service=+ep让非 root 的named用户也能绑定 53 端口。
3.4 Redis 镜像
Redis 配置相对简单,需要确保绑定所有 IP 并为其持久化文件创建 /data 目录。
Dockerfile 及配置:
bash
FROM alpine:3.19
RUN apk add --no-cache redis
COPY redis.conf /etc/redis.conf
RUN deluser redis 2>/dev/null || true && \
adduser -D redis && \
mkdir -p /data && \
chown redis:redis /etc/redis.conf /data
USER redis
EXPOSE 6379
CMD ["redis-server", "/etc/redis.conf"]
bash
# redis.conf 关键部分
bind 0.0.0.0
port 6379
daemonize no
dir /data
关键点解释 :daemonize no 配置保证 Redis 在前台运行,避免容器启动后立刻退出。dir /data 必须指向一个存在且有写入权限的目录,否则 Redis 进程会报错崩溃。
四、构建与分发
在包含所有 Dockerfile 的开发机上执行以下命令进行构建,然后导出为一个 tar 包。
bash
cd /media/kubernetes1/middleware
# 依次构建四个镜像
cd nginx && docker build -t my-nginx:alpine .
cd ../mariadb && docker build -t my-mariadb:alpine .
cd ../dns && docker build -t my-dns:alpine .
cd ../redis && docker build -t my-redis:alpine .
# 导出所有镜像
docker save -o final-middleware.tar \
my-nginx:alpine \
my-mariadb:alpine \
my-dns:alpine \
my-redis:alpine
使用 scp 和 docker load 将镜像分发到集群中的 每一个 Worker 节点。
bash
for ip in 129 130; do
scp final-middleware.tar root@192.168.64.$ip:/tmp/
ssh root@192.168.64.$ip docker load -i /tmp/final-middleware.tar
done
五、Kubernetes 部署
所有 Pod 的 YAML 文件都必须显式设置 imagePullPolicy: Never,强制 Kubernetes 使用已经分发到节点上的本地镜像。
首先,创建一个专门的命名空间 2516:
bash
kubectl create namespace 2516
将四个 Pod 的定义合并到一个 all-pods.yaml 文件中,方便统一管理。DNS Pod 必须使用 hostNetwork: true 来共享宿主机网络,以便外部客户端能够直接查询其监听的 53 端口。
bash
# all-pods.yaml
apiVersion: v1
kind: Pod
metadata:
name: my-nginx
namespace: "2516"
labels:
app: nginx
spec:
containers:
- name: nginx
image: my-nginx:alpine
imagePullPolicy: Never
ports:
- containerPort: 80
---
apiVersion: v1
kind: Pod
metadata:
name: my-mariadb
namespace: "2516"
labels:
app: mariadb
spec:
containers:
- name: mariadb
image: my-mariadb:alpine
imagePullPolicy: Never
ports:
- containerPort: 3306
---
apiVersion: v1
kind: Pod
metadata:
name: my-dns
namespace: "2516"
labels:
app: dns
spec:
hostNetwork: true
containers:
- name: bind
image: my-dns:alpine
imagePullPolicy: Never
ports:
- containerPort: 53
protocol: UDP
- containerPort: 53
protocol: TCP
---
apiVersion: v1
kind: Pod
metadata:
name: my-redis
namespace: "2516"
labels:
app: redis
spec:
containers:
- name: redis
image: my-redis:alpine
imagePullPolicy: Never
ports:
- containerPort: 6379
执行部署命令并检查 Pod 的运行状态:
bash
kubectl apply -f /root/k8s-yaml/all-pods.yaml
kubectl get pods -n 2516 -o wide
(输出示例:四个 Pod 的 STATUS 均为 Running,并分布在 k8s-node1 和 k8s-node2 上。)
六、测试与验证
在所有 Pod 成功运行后,我们逐一验证其功能。
Nginx 测试
bash
kubectl exec -n 2516 my-nginx -- wget -qO- http://127.0.0.1
# 预期输出: nginx ok
测试结果表明 Nginx 能够成功提供 Web 服务。使用 IP 127.0.0.1 而非 localhost,可有效规避容器内 DNS 解析为 IPv6 而 Nginx 仅监听 IPv4 的连接问题。
MariaDB 测试
bash
kubectl exec -n 2516 -it my-mariadb -- mysql -u root -e "SELECT VERSION();"
# 预期输出: 10.11.14-MariaDB
DNS 测试 (假设 pod 调度在 k8s-node2 / 192.168.64.130 上)
bash
# 正向解析 www.hzx.com
nslookup www.hzx.com 192.168.64.130
# 预期输出: Address: 192.168.64.128
# 反向解析 192.168.64.128
nslookup 192.168.64.128 192.168.64.130
# 预期输出: name = k8s-master.hzx.com
测试结果证明 DNS 服务配置的正向和反向区域都已生效。
Redis 测试
bash
kubectl exec -n 2516 my-redis -- redis-cli ping
# 预期输出: PONG
七、排错实录:常见问题与解决方案
在整个自建镜像和部署过程中,我们遇到了许多典型的技术陷阱。下面将这六个最具代表性的问题进行梳理,以供参考。
| 问题现象 | 深层原因 | 解决方案 |
|---|---|---|
| 容器启动即退出 (Error/CrashLoopBackOff) | MariaDB、Redis、Nginx 启动所需的目录(如 /run/mysqld、/data)在镜像中缺失,进程报错退出。 |
在 Dockerfile 中使用 RUN mkdir -p 预先创建所有必要目录,并赋予正确的用户和权限。 |
| MariaDB 登录被拒 | 默认仅允许 root 通过 Unix Socket 无密码认证,TCP 连接被拒绝。 | 在构建时启动临时 mysqld 实例,执行 ALTER USER 将认证方式改为 mysql_native_password,并清空密码。 |
| Nginx 服务"连接被拒绝" | wget http://localhost 时,localhost 被解析为 IPv6 地址 ::1,而 Nginx 的 listen 80; 在某些环境下可能只绑定了 IPv6 或绑定不明确。 |
在 nginx.conf 中明确指定 listen 0.0.0.0:80;,强制监听所有 IPv4 地址;测试时也直接使用 127.0.0.1。 |
| DNS 正向解析 NXDOMAIN | 正向区域文件(如 hzx.com.zone)中缺少目标域名(如 www)的 A 记录。 |
在正向区域文件中添加缺失的 A 记录(如 www IN A 192.168.64.128),更新序列号并重建镜像。 |
| DNS 反向解析失败 (区域名错误) | named.conf 中反向区的名称误写为文件名 "64.168.192.zone",导致 BIND 不将其视为标准反向区域。 |
将区名修正为符合规范的 "64.168.192.in-addr.arpa",文件名保持不变。 |
构建时报错 adduser: user in use |
apk add bind 或 apk add redis 等命令会自动创建同名用户,导致后续 adduser 失败。 |
在 Dockerfile 中使用 `deluser <用户名> 2>/dev/null |
八、总结与展望
本次实验完整演示了如何从零开始,在 openEuler 操作系统上基于 Alpine Linux 手工制作 Nginx、MariaDB、DNS、Redis 四个中间件的容器镜像,并成功在 Kubernetes 集群中以单 Pod 模式部署和验证其功能。全过程严格遵循"不使用官方仓库现成镜像"的原则,从选择最精简的基础系统、手动处理包依赖、到解决每一个目录权限和端口绑定的细节,深刻实践了生产环境中自建镜像的安全与可控性理念。
通过对这些典型排错案例的解决,我们不仅掌握了 Alpine 的 apk 包管理特点、BIND 的配置细节,还深入理解了 Kubernetes Pod 的调度策略、hostNetwork 与特权端口的处理方式。这套方法可作为教学实验或中小企业私有化交付的可靠模板,为更复杂的云原生实践奠定坚实的基础。