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 与特权端口的处理方式。这套方法可作为教学实验或中小企业私有化交付的可靠模板,为更复杂的云原生实践奠定坚实的基础。

相关推荐
杨云龙UP2 小时前
Docker MySQL 5.7 全库备份到异地服务器实践记录_20260427
linux·运维·服务器·数据库·mysql·docker·容器
运维全栈笔记2 小时前
K8S部署MySQL主从复制实现高可用数据库
mysql·adb·云原生·容器·系统架构·kubernetes·kubelet
海兰5 小时前
Elastic 基于 Agentic 架构与 MCP 的 Kubernetes 智能可观测性深度解析
elasticsearch·容器·架构·kubernetes
雨奔11 小时前
Kubernetes DNS 完全指南:服务发现核心机制与实践
java·kubernetes·服务发现
米高梅狮子12 小时前
05.Kubernetes Volume和Kubernetes ConfigMap
云原生·容器·kubernetes
MAVER1CK18 小时前
Install VNC in Docker container
运维·docker·容器
眷蓝天21 小时前
kubectl 管理工具
kubernetes·kubectl
亚空间仓鼠21 小时前
Kubernetes技术入门与实践(二):常用命令
云原生·容器·kubernetes
LSL666_21 小时前
3 安装docker
运维·docker·容器