Kubernetes技术入门与实践(三):构建高效中间件服务

从零构建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 libcBusyBox,专为容器环境优化。相比 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:alpinemy-mariadb:alpinemy-dns:alpinemy-redis:alpine。所有构建在开发机(可联网)上完成,然后导出为 tar 包分发至各 Worker 节点。

3.1 Nginx 镜像

Nginx 作为本次实验的 Web 服务器,需要提供一个默认页面来验证服务是否正常启动。然而,Alpine 官方维护的 Nginx 包非常精简,存在两个需要解决的问题:

  1. 静态资源目录不匹配 :Alpine 默认将网页根目录设定在 /var/www/localhost/htdocs,这与我们日常使用的 /var/www/html 习惯不同。更重要的是,该目录默认为空,没有 index.html,直接访问会触发 404 错误。

  2. 非 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 特权端口。因此,需要通过 setcapnamed 二进制程序赋予 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

使用 scpdocker 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 bindapk add redis 等命令会自动创建同名用户,导致后续 adduser 失败。 在 Dockerfile 中使用 `deluser <用户名> 2>/dev/null

八、总结与展望

本次实验完整演示了如何从零开始,在 openEuler 操作系统上基于 Alpine Linux 手工制作 Nginx、MariaDB、DNS、Redis 四个中间件的容器镜像,并成功在 Kubernetes 集群中以单 Pod 模式部署和验证其功能。全过程严格遵循"不使用官方仓库现成镜像"的原则,从选择最精简的基础系统、手动处理包依赖、到解决每一个目录权限和端口绑定的细节,深刻实践了生产环境中自建镜像的安全与可控性理念。

通过对这些典型排错案例的解决,我们不仅掌握了 Alpine 的 apk 包管理特点、BIND 的配置细节,还深入理解了 Kubernetes Pod 的调度策略、hostNetwork 与特权端口的处理方式。这套方法可作为教学实验或中小企业私有化交付的可靠模板,为更复杂的云原生实践奠定坚实的基础。

相关推荐
lichenyang4534 小时前
Docker 学习笔记(四):Dockerfile,把项目打成自己的镜像
docker·容器
lichenyang4534 小时前
Docker 学习笔记(三):Docker 网络、bridge、子网和容器互通
docker·容器
lichenyang4534 小时前
Docker 学习笔记(二):docker run 的参数到底在控制什么?
docker·容器
运维开发故事3 天前
基于 Arthas 的多集群在线诊断系统设计与实现
kubernetes
Patrick_Wilson5 天前
从「改个端口」到 502:Next.js on k8s 的容器端口、Service 映射与 env 覆盖
docker·kubernetes·next.js
探索云原生5 天前
K8s 1.36 这个 GA 特性,把 initContainer 拉模型的 hack 干掉了
ai·云原生·kubernetes
云恒要逆袭5 天前
运行你的第一个Docker容器
后端·docker·容器
Java之美6 天前
一次k8s升级引发的DevicePlugin注册失败
云原生·kubernetes
程序员老赵7 天前
10 分钟部署 OpenCode:Docker 一键安装,浏览器打开就能用 AI 写代码(附完整命令与排错)
docker·容器·ai编程