构建多平台的 AOT 容器镜像

B站影视 2024-12-04 08:31 2

摘要:最近把 dotnet-httpie 做了一些升级改造,移除了 dotnet 6.0/7.0 的支持,只保留 8.0 和 9.0 的支持,于是可以更好地去做 AOT 的支持并且将容器镜像也基于 AOT 来打包,进一步减小了 docker 镜像的大小

Intro

最近把 dotnet-httpie 做了一些升级改造,移除了 dotnet 6.0/7.0 的支持,只保留 8.0 和 9.0 的支持,于是可以更好地去做 AOT 的支持并且将容器镜像也基于 AOT 来打包,进一步减小了 docker 镜像的大小

Code Changes

因为项目没有那么复杂,代码上的变化比较简单

先来看下项目文件的变化

移除了 net6.0/7.0 之后就可以直接使用PublishAot了,另外发现 nuget 包里的内容会有很多其他语言的语言包,所以配置了一下en除了PublishAot之外还额外配置了一些 AOT publish 时候的一些配置来进一步减少 publish 之后的文件大小,具体可以参考

Size

false
false
false
false
true
false
false
false
true
true
false

false

true
true

再来看几个具体的代码变化

有一些使用了条件编译的代码可能可以去掉了

condition compilation

Json 序列化需要使用 Source Generator 的模式来代替原来的写法

http-jsonjson generatorDynamicallyAccessedMembersattribute 来告知编译器需要保留的一些动态依赖除了上述变更之外还有一个小的改动,这里的改动是使用 primary constructor 的特性,移除了私有字段,直接使用 primary constructor 上的字段,由于在 dotnet 8 中 logging source generator 还不支持引用 primary constructor 的字段,所以这里有一个NET8_0的条件编译, dotnet 8 的时候声明一个私有字段以支持 logging source generatorContainer Image Changes

看完代码变化我们再来看看 docker 镜像相关的一些变化

原来的 Dockerfile 如下,原来的 docker image 会做一个 self-contained 的 单文件的 publish 并且会做单文件的压缩

FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine AS base
LABEL Maintainer="WeihanLi"

FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build-env

WORKDIR /app
COPY ./src/ ./src/
copy ./build/ ./build/
COPY ./Directory.Build.props ./
COPY ./Directory.Build.targets ./
COPY ./Directory.Packages.props ./
WORKDIR /app/src/HTTPie/
RUN dotnet publish -f net8.0 -c Release --self-contained --use-current-runtime -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true -p:AssemblyName=http -p:TargetFrameworks=net8.0 -o /app/artifacts

FROM base AS final
COPY --from=build-env /app/artifacts/http /root/.dotnet/tools/http
RUN ln -s /root/.dotnet/tools/http /root/.dotnet/tools/dotnet-http
ENV PATH="/root/.dotnet/tools:${PATH}"
ENTRYPOINT ["http"]
CMS ["--help"]

原来的 docker 镜像大小大概是 36+ MB

更新一个版本之后的 docker image

FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build-env

# Configure NativeAOT Build Prerequisites
# https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=linux-alpine,net8
# for alpine
RUN apk update && apk add clang build-base zlib-dev

WORKDIR /app
COPY ./src/ ./src/
COPY ./build/ ./build/
COPY ./Directory.Build.props ./
COPY ./Directory.Build.targets ./
COPY ./Directory.Packages.props ./
WORKDIR /app/src/HTTPie/
RUN dotnet publish -f net9.0 --use-current-runtime -p:AssemblyName=http -p:TargetFrameworks=net9.0 -o /app/artifacts

FROM alpine
COPY --from=build-env /app/artifacts/http /root/.dotnet/tools/http
RUN ln -s /root/.dotnet/tools/http /root/.dotnet/tools/dotnet-http
ENV PATH="/root/.dotnet/tools:${PATH}"
ENTRYPOINT ["http"]
CMS ["--help"]

更新之后,我们的 docker image 的可执行文件已经是 AOT publish 的产物了,所以 runtime 的镜像可以直接使用 alpine 而无需安装其他的 dotnet runtime 依赖

上面的 dockerfile 是单个 platform 的镜像,如果要在不同的 platform 上使用,比如在苹果的 ARM 电脑上是不能运行linux/amd64的,所以接着尝试增加多个平台的支持,目前支持 linux/amd64, linux/arm64 两种架构

要支持交叉编译需要配置 docker driver,要配置 QEMU 模拟多个架构,对于 dockerfile 也需要一些改动,和之前分享的多平台容器镜像相比会更加复杂一些,因为 AOT 发布需要使用到一些平台相关的依赖,微软提供了一些可以帮助交叉编译的一些容器镜像可以简化这一过程,感谢大佬的帮助,感兴趣的朋友也可以查看这个问题和大佬的改造 https://github.com/dotnet/runtime/discussions/110288 因为开始的时候遇到一些错误,错误的以为 alpine 不能 build arm 的支持,中间改成了基于 debian 的 image,所以大佬的改造也是基于 debian image 的,后面经过一些摸索改成了基于 alpine 的版本,因为 alpine 的镜像会更小一些

最终版本的 dockerfile 如下:

FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-net9.0-cross-arm64-musl AS cross-build-env

FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build-env

COPY --from=cross-build-env /crossrootfs /crossrootfs

ARG TARGETARCH
ARG BUILDARCH

# Configure NativeAOT Build Prerequisites
# https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=linux-alpine,net8
# for alpine
RUN apk update && apk add clang build-base zlib-dev

WORKDIR /app

COPY ./src/ ./src/
COPY ./build/ ./build/
COPY ./Directory.Build.props ./
COPY ./Directory.Build.targets ./
COPY ./Directory.Packages.props ./
COPY ./.editorconfig ./

WORKDIR /app/src/HTTPie/

RUN if [ "${TARGETARCH}" = "${BUILDARCH}" ]; then \
dotnet publish -f net9.0 --use-current-runtime -p:AssemblyName=http -p:TargetFrameworks=net9.0 -o /app/artifacts; \
else \
apk add binutils-aarch64 --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community; \
dotnet publish -f net9.0 -r linux-musl-arm64 -p:AssemblyName=http -p:TargetFrameworks=net9.0 -p:SysRoot=/crossrootfs/arm64 -p:ObjCopyName=aarch64-alpine-linux-musl-objcopy -o /app/artifacts; \
fi

FROM alpine

# https://github.com/opencontainers/image-spec/blob/main/annotations.md
LABEL org.opencontainers.image.authors="WeihanLi"
LABEL org.opencontainers.image.source="https://github.com/WeihanLi/dotnet-httpie"

COPY --from=build-env /app/artifacts/http /usr/bin/http
RUN chmod +x /usr/bin/http
ENTRYPOINT ["/usr/bin/http"]
CMD ["--help"]

最终 build 出来的镜像大概 12 MB+

第一行代码从微软的交叉编译帮助镜像中 copy 其他架构编译可能用到的文件,并针对 arm 架构安装编译必要的文件

apk add binutils-aarch64 --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community

由于这个 package 还没合并到 main repo 里,所以这里手动指定了 repository 地址

之后我们需要稍微调整下 publish 的命令,需要指定SysRoot和ObjCopyName用来帮助找到正确的系统依赖,另外需要注意下基于 alpine 和 debian 的 rid 不同,alpine arm 需要使用linux-musl-arm64dotnet publish -f net9.0 -r linux-musl-arm64 -p:AssemblyName=http -p:TargetFrameworks=net9.0 -p:SysRoot=/crossrootfs/arm64 -p:ObjCopyName=aarch64-alpine-linux-musl-objcopy -o /app/artifacts

dockerfile 搞定之后需要配置 QEMU 和 docker driver,之前的介绍是通过 Github Actions 的,这次基于 Azure DevOps 直接使用命令脚本来配置了,可以通过下面的命令来配置

docker run --privileged --rm multiarch/qemu-user-static --reset -p yes
docker buildx create --name container-builder --driver docker-container --driver-opt default-load=true --bootstrap --use
之后 build 并 push 镜像,需要使用docker buildx build --push命令并通过--platform指定要构建的 platform,这里我们用到的是linux/amd64/linux/arm64docker buildx build --push -f Dockerfile --platform="linux/amd64,linux/arm64" --output="type=image" -t weihanli/dotnet-httpie:latest .

完整 build yaml 可以参考:https://github.com/WeihanLi/dotnet-httpie/blob/dev/.azure/pipelines/docker.yml

More

项目比较简单所以改造比较简单,大部分时间花在了研究 AOT 的多平台容器镜像的构建推送上了,希望对构建基于 AOT 的多平台容器镜像有所帮助

AOT 之后 docker 镜像的大小减少了差不多 2/3

Dockerfile: https://github.com/WeihanLi/dotnet-httpie/blob/dev/Dockerfile

build pipeline yaml: https://github.com/WeihanLi/dotnet-httpie/blob/dev/.azure/pipelines/docker.yml

微软 dotnet buildtools docker image:https://github.com/dotnet/dotnet-buildtools-prereqs-docker

References

https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?WT.mc_id=DT-MVP-5004222

https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/optimizing?WT.mc_id=DT-MVP-5004222

https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options?WT.mc_id=DT-MVP-5004222#trimming-framework-library-features

来源:opendotnet

相关推荐