[.NET 11] Native AOT 기본 활성화와 컨테이너 이미지 초경량화 세팅

.NET 11에서 SDK의 Native AOT CLI 호스트가 기본 활성화됐습니다. 애플리케이션 쪽에서는 여전히 PublishAot를 명시해야 하며, 컨테이너 발행에는 재현 가능한 이미지 다이제스트와 레이어 재사용이 추가됐습니다. 이 글은 ASP.NET Core 앱을 AOT로 게시하고 컨테이너 이미지를 최소 구성으로 만드는 절차입니다.
절차
- .NET 11에서 기본이 된 것과 아닌 것 구분
- AOT 게시 설정
- 컨테이너 발행 설정
- 이미지 크기 축소 옵션
- 재현 가능한 빌드와 다이제스트 고정
- 실행 검증
- 실패하는 지점과 대응
1. 기본이 된 것과 아닌 것
dotnetCLI 호스트가 Native AOT로 빌드되어 기본 실행됩니다.dotnet --version,dotnet --info, 각 명령의--help,dotnet --cli-schema,dotnet sln list|migrate|remove는 관리형 런타임 없이 동작합니다.- 전역 도구·외부 명령 실행도 AOT 경로에서 프로세스를 띄우므로,
dotnet ef같은 명령에서 관리형 CLI 기동 시간(600~700ms)을 건너뜁니다. - MSBuild 서버가 기본으로 켜져 CLI 호출 사이에 상주합니다.
- 애플리케이션 AOT 게시는 기본이 아닙니다.
PublishAot=true를 프로젝트에 지정해야 합니다.
기본값을 끄는 환경 변수는 다음과 같습니다. CI에서 메모리·동시성이 문제가 될 때만 개별로 끕니다.
export DOTNET_CLI_ENABLEAOT=false # AOT CLI 호스트 비활성
export DOTNET_CLI_USE_MSBUILD_SERVER=false # MSBuild 서버 상주 비활성
2. AOT 게시 설정
웹 API 최소 구성으로 시작합니다. PublishAot와 런타임 식별자를 지정하는 것이 전부입니다.
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<PublishAot>true</PublishAot>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<InvariantGlobalization>true</InvariantGlobalization>
<StripSymbols>true</StripSymbols>
</PropertyGroup>
dotnet publish -c Release
게시 결과는 bin/Release/net11.0/linux-x64/publish 아래의 단일 네이티브 실행 파일입니다. 관리형 DLL이 없으므로 실행에는 .NET 런타임이 필요하지 않습니다.
.NET 11 SDK에서는 AOT 게시 시 전체 빌드를 생략하는 동작 변경이 있습니다. 게시 전에 별도로 dotnet build를 돌려 경고를 먼저 확인하는 편이 안전합니다.
dotnet build -c Release # 트림·AOT 경고(IL2026, IL3050) 확인
dotnet publish -c Release
3. 컨테이너 발행 설정
SDK의 컨테이너 발행을 쓰면 Dockerfile 없이 이미지를 만듭니다.
<PropertyGroup>
<ContainerRepository>myregistry/myapi</ContainerRepository>
<ContainerFamily>noble-chiseled</ContainerFamily>
<ContainerBaseImage>mcr.microsoft.com/dotnet/runtime-deps:11.0-noble-chiseled</ContainerBaseImage>
</PropertyGroup>
dotnet publish -c Release /t:PublishContainer
ContainerBaseImage를 지정하지 않으면 SDK가 런타임에 맞는 기본 이미지를 고릅니다. AOT 게시에서는 런타임 DLL이 필요 없으므로 runtime-deps 계열이 기준입니다. chiseled 이미지는 셸과 패키지 관리자가 없어 공격면이 작습니다.
레지스트리에 바로 올리려면 저장소를 지정하고 인증을 붙입니다.
docker login myregistry.example.com
dotnet publish -c Release /t:PublishContainer \
-p:ContainerRegistry=myregistry.example.com \
-p:ContainerImageTag=1.0.0
4. 이미지 크기 축소 옵션
InvariantGlobalization=true: ICU 데이터를 제거합니다. 문화권별 정렬·형식 지정이 필요한 서비스에는 쓰지 않습니다.UseSystemResourceKeys=true: 프레임워크 예외 메시지를 리소스에서 빼고 짧은 키로 대체합니다. 로그 가독성이 떨어집니다.StripSymbols=true: 디버그 심볼을 산출물에서 제거합니다.IlcOptimizationPreference=Size: 속도 대신 크기를 우선합니다.EventSourceSupport=false: 진단 이벤트를 제거합니다. 관측 도구가 EventSource를 쓰면 끄지 않습니다.
크기 비교는 태그를 고정해 두고 측정합니다.
docker images myregistry/myapi --format '{{.Tag}}\t{{.Size}}'
5. 재현 가능한 빌드와 다이제스트 고정
.NET 11 컨테이너 발행은 SOURCE_DATE_EPOCH를 기준으로 이미지 메타데이터의 타임스탬프를 고정해, 같은 입력이면 같은 다이제스트가 나오도록 지원합니다. 커밋 시각을 값으로 넘깁니다.
export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)
dotnet publish -c Release /t:PublishContainer -p:ContainerImageTag=1.0.0
docker inspect --format '{{index .RepoDigests 0}}' myregistry/myapi:1.0.0
대상 매니페스트가 이미 있으면 중복 레이어 업로드를 건너뛰므로, 태그만 바꿔 다시 올리는 파이프라인의 푸시 시간이 줄어듭니다.
6. 실행 검증
- 이미지가 실행되는지 확인합니다.
- 셸이 없는 이미지이므로
docker exec로 내부를 뒤지는 방식은 쓰지 않습니다. - 네이티브 바이너리의 의존 라이브러리는 호스트에서 확인합니다.
docker run --rm -p 8080:8080 myregistry/myapi:1.0.0
curl -i http://localhost:8080/health
# 네이티브 실행 파일의 링크 상태 확인
ldd bin/Release/net11.0/linux-x64/publish/MyApi
ldd 결과에 not found가 있으면 컨테이너 베이스 이미지에 해당 라이브러리가 없다는 뜻입니다. runtime-deps 계열을 쓰고 있는지, 배포 대상 배포판과 태그가 맞는지 확인합니다.
7. 실패하는 지점과 대응
- 리플렉션 기반 직렬화, 동적 프록시, 런타임 코드 생성은 AOT에서 동작하지 않습니다. System.Text.Json은 source generator를 사용합니다.
- 오래된 라이브러리의
RequiresUnreferencedCode,RequiresDynamicCode경고는 게시 전에 정리합니다. - MVC 컨트롤러·Razor 기반 앱은 AOT 게시 대상이 아닙니다. Minimal API나 gRPC처럼 요청 파이프라인이 단순한 구성이 기준입니다.
- 크로스 컴파일은 플랫폼 링커가 필요합니다. Windows에서 linux-x64를 만들려면 크로스 툴체인이 준비돼야 하며, 여의치 않으면 CI 컨테이너에서 빌드합니다.
- 시작 시간은 줄지만 빌드 시간은 늘어납니다. 캐시가 없는 CI에서는 게시 단계가 병목이 되므로, AOT 산출물 캐시를 잡아 둡니다.
# CI에서 AOT 빌드할 때의 최소 예시
FROM mcr.microsoft.com/dotnet/sdk:11.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -r linux-x64 --self-contained \
-p:PublishAot=true -o /out
FROM mcr.microsoft.com/dotnet/runtime-deps:11.0-noble-chiseled
WORKDIR /app
COPY --from=build /out/MyApi /app/MyApi
EXPOSE 8080
ENTRYPOINT ["/app/MyApi"]
정리
.NET 11에서 기본이 된 것은 dotnet CLI 호스트의 AOT 실행과 MSBuild 서버 상주이고, 애플리케이션 AOT는 PublishAot를 지정해야 합니다. 컨테이너는 runtime-deps chiseled 베이스에 SDK 컨테이너 발행을 붙이고, InvariantGlobalization·UseSystemResourceKeys·StripSymbols로 크기를 줄인 뒤 SOURCE_DATE_EPOCH로 다이제스트를 고정하면 됩니다.