容器已经成为在云中分发和运行广泛的应用程序和服务的最简单方法之一。几年前,.NET运行时就针对容器进行了加固。现在,你只需用dotnet publish.NET Runtime就可以创建你的应用程序的容器化版本。容器图像现在是.NET SDK支持的一种输出类型。
TL;DR
在我们讨论这个工作的细节之前,我想展示一个从零到运行的容器化ASP.NET Core应用程序是什么样子。
# create a new project and move to its directory
dotnet new mvc -n my-awesome-container-app
cd my-awesome-container-app
# add a reference to a (temporary) package that creates the container
dotnet add package Microsoft.NET.Build.Containers
# publish your project for linux-x64
dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer
# run your app using the new container
docker run -it --rm -p 5010:80 my-awesome-container-app:1.0.0
现在你可以去http://localhost:5010 ,你应该看到MVC应用的样本,呈现出所有的光辉。
对于这个版本,你必须安装并运行Docker,才能使这个样本发挥作用。此外,只支持Linux-x64容器。
动机
容器是捆绑和运送应用程序的绝佳方式。构建容器镜像的一种流行方式是通过Dockerfile--一个描述如何创建和配置容器镜像的特殊文件。让我们来看看我们的一个ASP.NET Core应用程序的Dockerfile样本。
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /source
# copy csproj and restore as distinct layers
COPY aspnetapp/*.csproj .
RUN dotnet restore --use-current-runtime
# copy everything else and build app
COPY aspnetapp/. .
RUN dotnet publish -c Release -o /app --use-current-runtime --self-contained false --no-restore
# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "aspnetapp.dll"]
这个项目使用一个多阶段构建来构建和运行应用程序。它使用7.0 SDK镜像来恢复应用程序的依赖关系,将应用程序发布到一个文件夹,最后从该目录中创建最终的运行时镜像。
这个Docker文件运行得很好,但有一些不明显的注意事项,这些注意事项来自于Docker构建环境的概念。构建上下文是在Docker文件中可以访问的一组文件,通常(但不总是)与Docker文件是同一个目录。如果你有一个Docker文件位于你的项目文件旁边,但你的项目文件在解决方案根目录下,那么你的Docker构建上下文很容易不包括像Directory.Packages.props或NuGet.config这样的配置文件,这些文件将包含在一个普通的dotnet build 。任何分层的配置模型,如EditorConfig或版本库本地的git配置,都会出现这种情况。
明确定义的Docker构建上下文和.NET构建过程之间的不匹配是该功能的驱动因素之一。构建镜像所需的所有信息都存在于标准的dotnet build ,我们只需要找出正确的方法,以Docker等容器运行系统可以使用的方式来表示这些数据。
它是如何工作的
容器镜像由两个主要部分组成:一些JSON配置,包含关于如何运行镜像的元数据,以及代表文件系统的tarball档案的列表。在.NET 7中,我们为.NET运行时添加了几个API,用于处理TAR文件和流,这就为以编程方式操纵容器镜像打开了大门。
这种方法已经在Java生态系统中的Jib、Go的Ko,甚至是.NET中的konet等项目中得到了非常成功的证明。很明显,一个简单的工具驱动的生成容器镜像的方法正变得越来越流行。
我们建立了.NET SDK解决方案,目标如下。
- 提供与现有构建逻辑的无缝集成,以防止出现前面提到的那种上下文差距。
- 用C#实现,以利用我们自己的工具,并从.NET运行时的性能改进中受益
- 集成到.NET SDK中,这样,.NET团队就可以直接在现有的.NET SDK流程中改进和服务。
定制化
那些有编写Dockerfile经验的人可能会想,Dockerfile的所有复杂性都到哪里去了。FROM 基本镜像在哪里声明?使用了哪些标签?通过使用MSBuild属性和项目,生成的镜像的许多方面是可以定制的,但我们已经提供了一些默认值,以使入门更容易。
基本图像
基本镜像定义了你将拥有的功能以及你将使用的操作系统和版本。以下是自动选择的基本图像。
- 用于ASP.NET Core应用程序。
mcr.microsoft.com/dotnet/aspnet - 适用于独立的应用程序。
mcr.microsoft.com/dotnet/runtime-deps - 适用于所有其他应用程序。
mcr.microsoft.com/dotnet/runtime
在所有情况下,用于基础图像的标签是为应用程序选择的TargetFramework的版本部分--例如,TargetFramework为net7.0 ,将导致使用的标签为7.0 。这些简单的版本标签是基于Debian发行的Linux--如果你想使用一个不同的支持的发行版,如Alpine或Ubuntu,那么你需要手动指定该标签(见下面的ContainerBaseImage 的例子)。
我们认为选择默认基于Debian版本的运行时映像是最好的选择,可以广泛兼容大多数应用程序的需求。当然,你可以为你的容器自由选择任何基础镜像。只需将ContainerBaseImage 属性设置为任何镜像名称,剩下的就交给我们了。例如,也许你想在Alpine Linux上运行你的网络应用。这将会是这样的。
<ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:7.0-alpine</ContainerBaseImage>
镜像名称和版本
你的图像的名称将默认基于你的项目的AssemblyName 。如果这对你来说不合适,图像的名称可以通过使用ContainerImageName 属性来明确设置。
一个图像也需要一个标签,所以我们默认为Version 属性的值。这默认为1.0.0 ,但你可以自由地以任何方式定制它,我们会利用它。这特别适合像GitVersioning这样的自动版本管理方案,版本是由仓库中的一些配置数据导出的。这有助于确保你总是有一个独特的版本来运行你的应用程序。唯一的版本标签对于图像来说尤其重要,因为将具有相同标签的图像推送到注册表会覆盖之前作为该标签存储的图像。
其他一切
许多其他属性可以在图像上设置,如自定义入口点、环境变量,甚至任意标签(通常用于跟踪元数据,如SDK版本,或谁制作的图像)。此外,镜像经常通过指定不同的基础注册表被推送到目标注册表。在后续的.NET SDK版本中,我们计划以类似上述的方式增加对这些功能的支持--所有这些都是通过MSBuild项目属性控制的。
你应该在什么时候使用这个功能
本地开发
如果你需要一个用于本地开发的容器,现在你只需要一个命令就可以了。dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer ,将制作一个以你的项目命名的Debug-configuration容器镜像。一些用户把这些属性放到Directory.Build.props中,然后简单地运行dotnet publish ,这样就更简单了。
<Project>
<PropertyGroup>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<PublishProfile>DefaultContainer<PublishProfile>
</PropertyGroup>
</Project>
你也可以使用其他SDK和MSBuild功能,如响应文件或PublishProfiles来创建这些属性组以方便使用。
CI管线
总的来说,使用SDK构建容器镜像应该可以无缝地集成到你现有的构建过程中。一个最小的GitHub Actions工作流的样本,用于容器化一个应用程序,只有大约30行的配置。
name: Containerize ASP.NET Core application
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET SDK
uses: actions/setup-dotnet@v2
# login to our registry so we can push the image
- name: Docker Login
uses: actions-hub/docker/login@master
env:
DOCKER_REGISTRY_URL: sdkcontainerdemo.azurecr.io
DOCKER_USERNAME: ${{ secrets.ACR_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.ACR_PAT }}
# build the app for the linux-x64 runtime identifier
- name: Build
run: dotnet build --os linux --arch x64 --configuration Release
env:
GITHUB_USERNAME: baronfel
GITHUB_TOKEN: ${{ secrets.CONTAINER_PUSH_PAT }}
# Package the app into a linux-x64 container based on the dotnet/aspnet image
- name: Publish
run: dotnet publish --os linux --arch x64 --configuration Release -p:PublishProfile=DefaultContainer
# Because we don't yet support pushing to authenticated registries, we have to use docker to
# tag and push the image. In the future neither of these steps will be required!
- name: Tag built container with Azure Container Registry url
uses: actions-hub/docker/cli@master
with:
args: tag sdk-container-demo:1.0.0 sdkcontainerdemo.azurecr.io/baronfel/sdk-container-demo:latest
- name: Push built container to Azure Container Registry
uses: actions-hub/docker/cli@master
with:
args: push sdkcontainerdemo.azurecr.io/baronfel/sdk-container-demo:latest
这是一个名为baronfel/sdk-container-demo的例子 repo的一部分,用来演示这个端到端的流程。
不过,有一个主要的场景,我们根本无法用SDK构建的容器来处理。
RUN命令
在.NET SDK中没有办法执行RUN命令。这些命令通常被用来安装一些操作系统的包,或者创建一个新的操作系统用户,或者任何数量的任意事情。如果你想继续使用SDK的容器构建功能,你可以用这些变化来创建一个自定义的基础镜像,然后用这个基础镜像作为上述的ContainerBaseImage 。
举个例子,假设我在做一个库,需要在主机上安装libxml2库,以便做一些自定义的XML处理。我可以写一个像这样的Docker文件来捕获这个本地依赖关系。
FROM mcr.microsoft.com/dotnet/runtime:7.0
RUN apt-get update && \
apt-get install -y libxml2 && \
rm -rf /var/lib/apt/lists/*
现在,我可以构建、标记和推送这个镜像到我的注册中心,并将其作为我的应用程序的基础。
docker build -f Dockerfile . -t registry.contoso.com/my-base-image:1.0.0
docker push registry.contoso.com/my-base-image:1.0.0
在我的应用程序的项目文件中,我会将ContainerBaseImage 属性设置为这个新镜像。
<Project>
<PropertyGroup>
...
<ContainerBaseImage>registry.contoso.com/my-base-image:1.0.0</ContainerBaseImage>
...
</PropertyGroup>
</Project>
临时间隙
这是SDK构建的容器镜像的初始预览版,所以有一些功能我们还没来得及提供。
Windows镜像和非x64架构
在这个初始版本中,我们把重点放在了Linux-x64镜像的部署场景上。Windows镜像和其他架构是我们计划在完整版本中支持的关键场景,所以要注意这方面的新进展。
推送到远程注册中心
我们还没有实现对认证的支持,这对许多用户来说是至关重要的。这是我们最优先的项目之一。在这期间,我们建议推送到你的本地Docker守护进程,然后使用docker tag 和docker push 来推送生成的镜像到你的目的地。上面的GitHub动作示例展示了如何做到这一点。
一些图像定制
一些图像元数据的定制还没有实现。这包括自定义Entrypoints,环境变量,以及自定义用户/组信息。完整的列表可以在dotnet/sdk-container-builds上看到。
接下来的步骤
在.NET 7发布的候选阶段,我们将添加新的图像元数据,支持将图像推送到远程注册表,并支持Windows图像。你可以在我们为此创建的里程碑中跟踪这一进展。
我们还计划在整个发布过程中把这项工作直接整合到SDK中。到那时,我们将在NuGet上发布一个 "最终 "版本的软件包,它将警告你这个变化,并要求你从你的项目中完全删除该软件包。
如果你对该工具的前沿构建感兴趣,我们也有一个GitHub软件包,你可以选择尝试最新的工作。
结束语
我们很希望那些构建Linux容器的人能够尝试用.NET SDK构建它们。我个人在本地尝试了一下--我很开心地去了我的一些演示库,用一个命令就把它们容器化了,我希望你们都有同样的感觉
希望你们都能有同样的感觉!快乐的图像构建