K8S 免镜像部署:通过 API 上传文件动态创建服务(以 Ollama 为例)

1. 需求背景与挑战

在 Kubernetes (K8S) 环境中,为满足临时需求而新增一个服务,传统流程通常需要:

  1. 编写服务代码。
  2. 创建 Dockerfile。
  3. 构建并推送 Docker 镜像到仓库。
  4. 编写或修改 K8S 部署清单(Deployment/Service)。
  5. 应用清单,等待 Pod 启动。

这个过程耗时较长,尤其在快速验证、临时调试或部署一次性任务时显得笨重。用户希望找到一种更简便的方法:避开创建 Docker 镜像的步骤,直接通过一个 API 上传文件(如可执行文件、模型文件、配置文件),就能在 K8S 的某个容器(Container)上动态创建并运行服务,例如快速部署一个 Ollama 服务来加载上传的模型。

2. 核心思路:利用 Init Container 与共享 Volume

K8S 本身不直接支持"上传文件即服务",但我们可以通过组合现有资源来实现类似效果。核心思路如下:

  • 设计文件上传 API:该 API 运行在主容器或集群内另一个服务中,负责接收文件并存储到某个持久化存储(如 PVC、ConfigMap、或云存储)。
  • 主容器启动服务:主容器启动时,从共享 Volume 读取文件,并执行相应命令来启动目标服务(如 Ollama)。

这样,更新服务只需通过 API 上传新文件,然后重启服务,而无需重建 Docker 镜像。

3. 架构设计与组件

下图展示了该方案的组件交互流程:

cs 复制代码
flowchart TD
    A[用户/客户端] -->|1. 上传文件| B[文件上传 API]
    B -->|2. 存储文件| C[(持久化存储
如 PVC/MinIO)]
    D -->|3. 读取文件| E
    F -->|4. 启动服务| G[服务端点]
    A -->|5. 访问服务| G

关键组件说明:

  • 文件上传 API :一个简单的 Web 服务,提供 POST /upload 接口,将接收到的文件写入后端存储。可以用 Python Flask、Go Gin 或 Node.js Express 快速实现。
  • 持久化存储:选择取决于文件大小和访问模式。对于模型等大文件,推荐使用与云平台集成的对象存储(如 AWS S3、MinIO)或 ReadWriteMany 类型的 PVC。
  • 主容器:基础镜像包含目标服务的运行时环境(如ASP.NET 镜像)。启动脚本文件并启动服务。

4. 实战示例:动态部署 Ollama 服务

假设我们已有 K8S 集群,并希望动态部署 Ollama 来加载用户上传的 GGUF 模型文件。

4.1 准备文件上传 API(ASP.NET 示例)

Program.cs

cs 复制代码
using Microsoft.AspNetCore.Http.Features;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.Limits.MaxRequestBodySize = null;
});

builder.Services.Configure<FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = long.MaxValue;
    options.ValueLengthLimit = int.MaxValue;
    options.MultipartHeadersLengthLimit = int.MaxValue;
});

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "LargeFileUploadApi",
        Version = "v1"
    });
});

var app = builder.Build();

Directory.CreateDirectory(Path.Combine(app.Environment.ContentRootPath, "uploads"));

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "uploads")),
    RequestPath = "/uploads"
});

app.MapControllers();

app.MapGet("/", () => Results.Ok(new
{
    service = "LargeFileUploadApi",
    swagger = "/swagger",
    upload = "/api/files/upload",
    health = "/health"
}));

app.Run();

FilesController.cs

cs 复制代码
using Microsoft.AspNetCore.Mvc;

namespace LargeFileUploadApi.Controllers;

[ApiController]
[Route("api/files")]
public sealed class FilesController : ControllerBase
{
    private readonly IWebHostEnvironment _environment;

    public FilesController(IWebHostEnvironment environment)
    {
        _environment = environment;
    }

    [HttpPost("upload")]
    public async Task<ActionResult<UploadResult>> Upload(IFormFile file, CancellationToken cancellationToken)
    {
        if (file is null || file.Length == 0)
        {
            return BadRequest(new { error = "No file was provided." });
        }

        var uploadRoot = Path.Combine(_environment.ContentRootPath, "uploads");
        Directory.CreateDirectory(uploadRoot);

        var fileName = Path.GetFileName(file.FileName);
        var filePath = Path.Combine(uploadRoot, fileName);

        await using var targetStream = System.IO.File.Create(filePath);
        await file.CopyToAsync(targetStream, cancellationToken);

        var fileInfo = new FileInfo(filePath);
        return Ok(new UploadResult(
            fileName,
            fileInfo.Length,
            $"/uploads/{Uri.EscapeDataString(fileName)}"));
    }

    [HttpGet("health")]
    public IActionResult Health()
    {
        return Ok(new { status = "ok" });
    }
}

public sealed record UploadResult(string FileName, long SizeInBytes, string DownloadUrl);

将此 API 部署为 K8S Service。

4.2 打包 Docker Image

比如我在本地执行:

4.2.1 发布API,并复制发布文件到Dockerfile同目录的publish中

cs 复制代码
dotnet publish -c Release -o publish -r linux-x64

4.2.2 编写Dockerfile

Dockerfile

cs 复制代码
FROM harbor.xxx.com/base_image/aspnet:10.0

ENV ASPNETCORE_ENVIRONMENT=Development
ENV TZ=Asia/Shanghai
ENV LANG=en_US.UTF-8

# 1. 创建用户
RUN adduser --disabled-password myuser

# 2. 创建应用目录 (此时还是 root 权限)
RUN mkdir -p /app

# 3. 将目录所有权赋予 myuser
RUN chown -R myuser:myuser /app

# 4. 设置工作目录
WORKDIR /app

# 5. 复制文件 (此时还是 root 权限,可以写入 /app)
COPY publish/ .

# 6. 再次确保所有复制进来的文件归属权正确 (可选,但推荐)
RUN chown -R myuser:myuser /app

# 7. 切换到非 root 用户
# USER myuser

# 8. 启动命令
EXPOSE 80 11434 5000 5001
ENV ASPNETCORE_URLS http://0.0.0.0:80
ENTRYPOINT ["dotnet", "LargeFileUploadApi.dll"]

4.2.3 docker build

docker_build.sh

cs 复制代码
export HARBOR_URL=harbor.xxx.com
export HARBOR_PROJECT=k8sdev
export HARBOR_USER=robot\$k8sdev+test
export HARBOR_PASSWORD=**************
export DOCKER_IMAGE=dev/docker-debug:1.0.0

# delete docker image
docker rmi "$HARBOR_URL/$HARBOR_PROJECT/$DOCKER_IMAGE"

# docker build
docker build --no-cache -t "$HARBOR_URL/$HARBOR_PROJECT/$DOCKER_IMAGE" .

# run docker image
# docker run -it --rm --name docker-debug -p 801:80 -p 802:11434 -p 803:5000 -p 804:5001  "$HARBOR_URL/$HARBOR_PROJECT/$DOCKER_IMAGE"
# docker exec -it docker-debug /bin/bash 

#psuh docker image to Harbor
#docker tag mcr.microsoft.com/dotnet/sdk:9.0 "$HARBOR_URL/$HARBOR_PROJECT/dotnetsdk:9.0.202"
##docker login -u $HARBOR_USER -p $HARBOR_PASSWORD $HARBOR_URL
##docker push "$HARBOR_URL/$HARBOR_PROJECT/$DOCKER_IMAGE"
##docker logout "$HARBOR_URL"

4.2 创建 K8S Deployment 清单

cs 复制代码
略...

4.3 操作流程

  1. 部署基础设施:部署文件上传 API。

  2. 编写Modelfile :

    cs 复制代码
    # 指向容器内模型文件的路径
    FROM /root/.ollama/models/Qwen3.5-4B-Q4_K_M.gguf
    
    # 设置模型参数(可选)
    PARAMETER temperature 0.7
    PARAMETER top_p 0.9
    PARAMETER num_ctx 4096
    
    # 设置系统提示词(可选)
    SYSTEM "You are a helpful assistant."
  3. 编写 entrypoint.sh : 启动 ollama。

    cs 复制代码
    #!/bin/bash
    set -e
    
    # 1. 设置环境变量,允许所有网络接口访问
    export OLLAMA_HOST=0.0.0.0:11434
    
    # 定义模型名称
    MODEL_NAME="Qwen3.5-4B-Q4_K_M"
    MODELFILE_PATH="/root/.ollama/models/Modelfile"
    
    # 1. 启动 Ollama 后台服务
    # 使用 & 让 serve 在后台运行,并将输出重定向到日志文件以便调试
    echo ">>> 正在启动 Ollama 后台服务..."
    nohup ollama serve > /tmp/ollama_server.log 2>&1 &
    SERVE_PID=$!
    disown $SERVE_PID  # <--- 关键添加:将进程从当前 shell 会话中分离
    
    # 2. 等待服务完全启动并监听端口
    # Ollama 启动通常需要几秒钟,这里通过循环检查端口或健康接口来确保服务可用
    echo ">>> 等待 Ollama 服务就绪..."
    MAX_RETRIES=30
    RETRY_COUNT=0
    while ! curl -s http://localhost:11434/ > /dev/null 2>&1; do
        RETRY_COUNT=$((RETRY_COUNT + 1))
        if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
            echo "!!! 错误: Ollama 服务启动超时,请检查日志 /tmp/ollama_server.log"
            kill $SERVE_PID
            exit 1
        fi
        sleep 1
    done
    echo ">>> Ollama 服务已就绪。"
    
    # 3. 检查模型是否存在
    # 此时服务已启动,ollama list 可以正常工作
    if ! ollama list | grep -q "$MODEL_NAME"; then
        echo ">>> 模型未找到,正在注册模型: $MODEL_NAME ..."
        
        # 检查 Modelfile 是否存在
        if [ ! -f "$MODELFILE_PATH" ]; then
            echo "!!! 错误: 找不到 Modelfile: $MODELFILE_PATH"
            kill $SERVE_PID
            exit 1
        fi
    
        # 创建模型
        if ollama create "$MODEL_NAME" -f "$MODELFILE_PATH"; then
            echo ">>> 模型注册成功!"
        else
            echo "!!! 错误: 模型注册失败"
            kill $SERVE_PID
            exit 1
        fi
    else
        echo ">>> 模型 $MODEL_NAME 已存在,跳过注册步骤。"
    fi
    
    echo ">>> 初始化完成。Ollama 服务正在后台运行 (PID: $SERVE_PID)"
    echo ">>> 提示: 当前脚本即将结束,如果需要保持服务长期运行,请不要关闭此终端,或使用 nohup/systemd 管理。"
    
    # 4. 保持脚本运行,防止后台服务随脚本退出而被杀死(可选)
    # 如果这是启动脚本的最后一步,通常我们希望服务继续运行。
    # wait $SERVE_PID 会让脚本挂起,直到 serve 进程结束。
    wait $SERVE_PID
  4. 上传文件 :用户通过 API 上传 Ollama package, Model, Modelfile, entrypoint 文件。

    cs 复制代码
    # 1.上传文件 on your PC (Ollama package, Model, Modelfile, entrypoint)
    cd /Docker/ollama/ && \
    
    zstd -d /model/ollama-linux-amd64.tar.zst -o ./ollama-linux-amd64.tar && \
    
    curl -X POST -F "file=@./ollama-linux-amd64.tar" http://localhost:801/api/files/upload && \
    rm ./ollama-linux-amd64.tar && \
    
    curl -X POST -F "file=@/models/Qwen3.5-4B-Q4_K_M.gguf" http://localhost:801/api/files/upload && \
    
    curl -X POST -F "file=@/Docker/ollama/Modelfile" http://localhost:801/api/files/upload && \
    
    curl -X POST -F "file=@/Docker/ollama/entrypoint.sh" http://localhost:801/api/files/upload
  5. 安装并启动 Ollama :Ollama manual install: https://docs.ollama.com/linux#manual-install

    cs 复制代码
    # 2.安裝Ollama on Docker-debug container
    curl -fsSL http://localhost/uploads/ollama-linux-amd64.tar \
        | tar x -C /usr
    
    mkdir -p /root/.ollama/models && \
    mv /app/uploads/Qwen3.5-4B-Q4_K_M.gguf /root/.ollama/models/Qwen3.5-4B-Q4_K_M.gguf && \
    mv /app/uploads/Modelfile /root/.ollama/models/Modelfile
    
    chmod +x /app/uploads/entrypoint.sh && \
    /app/uploads/entrypoint.sh
  6. Ollama 加载模型:主容器启动,检测到文件后启动 Ollama 并加载模型。

  7. 访问服务 :通过 Service ollama-service:11434 访问 Ollama API。

5. 方案优缺点与注意事项

优点

  • 快速迭代:无需每次修改都构建镜像,只需上传文件就可启动新增的服务。
  • 镜像通用 :基础容器镜像(如 aspnet)可复用,通过外部文件改变其行为。
  • 适合临时需求:完美应对临时调试、一次性任务或快速原型验证。

缺点与注意事项

  • 文件管理:需要自行管理存储中的文件版本、清理策略。
  • 安全性:上传 API 需做好认证、授权和文件类型校验,防止恶意文件上传。

7. 总结

通过"文件上传 API"的模式,我们可以在 K8S 上实现一种近似"免镜像"的动态服务部署。虽然仍需基础容器镜像,但将易变的业务文件(模型、脚本、配置)外置,大大提升了临时需求的处理效率。此方案特别适合 AI 模型部署、临时数据处理任务等场景。在实际应用中,请根据安全、性能和运维复杂度权衡,选择最合适的存储方案和文件同步机制。

相关推荐
AI导出鸭PC端1 小时前
文心怎样生成word文档?一键智能排版,AI导出鸭解决格式错乱痛点
人工智能·ai·word·豆包·deepseek·ai导出鸭
深小乐1 小时前
DeepSeek Harness 发布一周,登顶 Top10 的插件,暴露了哪些真实需求?
人工智能
小小克1 小时前
k8s集群部署的方法
云原生·容器·kubernetes
kobeyyl1 小时前
K8s集群部署核心原理与主流部署方法全解析
云原生·容器·kubernetes
奈斯先生Vector1 小时前
OpenScience 安装失败怎么办?Windows 环境、API 配置与本地服务排错指南
人工智能·windows·架构·开源·aigc·midjourney
武子康1 小时前
Email Thread 不是 Agent Session:生产级异步通信网关的状态、幂等与审批合同
人工智能·llm·agent
AIDANHANG1 小时前
放开长期挂着开关前先核到期日默认安全值与残留分支
人工智能
一次旅行2 小时前
2026‑08‑22 AI产业深度解读|Anthropic自研芯片布局、SGLang权重缓存守护进程、Agent任务作弊审计、AI原生SDLC
人工智能·缓存·sglang
Dawson Zhu2 小时前
【AI架构前沿】MEMO:解耦推理与记忆,破解大模型“知识更新“与“灾难性遗忘“的两难困境
人工智能·架构·aigc·agi