はじめに
タイトルに書いた通りの記事です。パブリックなNuGetフィードだけを使った場合は特に気にすることもなくさっくりDockerイメージを作れるのですが、プライベートfeedを使おうとした途端に厄介なことがでてきます。あまりそんなことが必要な人はいないかも知れませんが、自分への備忘録として残しておきます。
TL; DR
主なハマりポイントと解決策を先に列挙しておきます。
ハマりポイント
- Dockerfile内の
dotnet restore
にプライベートフィード(Azure DevOpsなどに置いてある)へのクレデンシャル情報を渡す必要がある - クレデンシャルを平文でDockerfileに直接書くのはセキュリティ的によろしくない
-
--build-args
でクレデンシャルを引数として渡すのもダメ(理由は後述)
解決策
- パーソナルアクセストークン (PAT) を発行してクレデンシャルとして利用する。手動のサインインが不要になる
-
VSS_NUGET_EXTERNAL_FEED_ENDPOINTS
環境変数を使ってクレデンシャルをdotnet restore
に渡す - Dockerのsecret volume機能を使う
具体的な手順
前提
Azure DevOpsでプライベートfeedを使う(公式ドキュメント)ことを想定して記事を書いてます。
dotnet restore
でデフォルト以外のNuGetフィードを使う
このStackOverflow記事が参考になります。平たく言うとVSS_NUGET_EXTERNAL_FEED_ENDPOINTS
という環境変数にJSONの形でフィードのURLとクレデンシャルを渡してやることで、dotnet restore
がデフォルト以外のフィード (-s
オプションで指定) からパッケージを持ってきてくれます。こんな感じです。
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
ENV ASPNETCORE_URLS="https://+;http://+"
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
# Auth with private feed
WORKDIR /tool
RUN wget https://raw.githubusercontent.com/Microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh \
&& chmod +x installcredprovider.sh \
&& ./installcredprovider.sh
WORKDIR /src
COPY ["./MyProject.csproj", "MyProject/"]
ARG FEED=https://pkgs.dev.azure.com/...../nuget/v3/index.json
RUN VSS_NUGET_EXTERNAL_FEED_ENDPOINTS='{"endpointCredentials":[{"endpoint":"'${FEED}'","username":"name","password":"パスワード"}]}' dotnet restore -s ${FEED} "MyProject/MyProject.csproj"
10行目で credential provider をインストールしたあと、最後の行でdotnet restore
を呼び出しています。
しかし、このままだとログイン名とパスワードをべたにDockerfile
中に埋め込むことになってしまいます。それは避けなければならないですね。かといってdocker build
の最中に手動でログインすることは不可能です。
Azure DevOpsでパーソナルアクセストークン (PAT) を発行する
というわけでパーソナルアクセストークン(PAT)を取得しましょう。GitHubにも同様の機能があるので、ご存じの方はすぐ理解してもらえると思います。公式ドキュメントはここです。公式ドキュメントはPATの操作について一通り網羅しているので長いですが、序盤の"Create a PAT"だけ読めばPATを作れます。作ったら忘れずコピーしておきましょう。
おさらいですが、PATが必要な理由は以下の2点です。
- Azure DevOpsのプライベート NuGet feed からパッケージをダウンロードするためにはアクセス権が必要
- Dockerfile内で
RUN dotnet restore
したいので、手動でログインすることはできない
ただし公式ドキュメントにもあるようにPATはパスワードとほぼ同等なので、これも安易にそのままDockerfileに埋め込んでgit commitするわけにはいきません。このため必然的にdocker build
実行時に外からPATを渡すことになります。
PATをdocker build
に渡す
docker build
に実行時パラメータを渡すための一般的な方法は--build-args
を使うことですが、パスワードを渡すのに使うと危険です。どうしてかというと、docker history
コマンドで丸わかりだからです。ここのブログでくわしく説明されています。
(余談ですが、go build
でパッケージをgitリポジトリから持ってくる場合も同様の問題が起こります。このブログで説明されています。)
secret volume機能を使う
解決策は上記のブログに書いてある通り、Dockerのsecret volume機能を使うことです。
- PATを適当なローカルのファイルに保存する (下記のMYPERSONALSECURITYTOKENを、実際のPAT文字列に置き換えてください)。
cat MYPERSONALSECURITYTOKEN > ~/.nuget/tokens/mytoken
- 上記の
Dockerfile
の最終行を以下のように変更する
RUN --mount=type=secret,id=mytoken VSS_NUGET_EXTERNAL_FEED_ENDPOINTS='{"endpointCredentials":[{"endpoint":"'${FEED}'","username":"name","password":"'$(cat /run/secrets/mytoken)'"}]}' dotnet restore -s ${FEED} "MyProject/MyProject.csproj"
RUN
コマンドに--mount=type=secret
を指定するのがポイントです (イコールが2個使われてて変な感じがしますが)。PATを使ってNuGetフィードから読む場合はusername
は空文字列以外なら何でもいいので、そのままname
としてあります。
-
docker build
を--secret
オプションをつけて実行する
DOCKER_BUILDKIT=1 docker build --secret id=mytoken,src=$HOME/.nuget/tokens/mytoken -t myproj:1.0.0 .
これで/run/secrets/mytoken
がローカル側の~/.nuget/tokens/mytoken
に置き換えられます。docker build
の実行履歴にもクレデンシャルが表示されないこともポイントです。src=
には最初~/.nuget/tokens/mytoken
と指定したのですが~
をホームディレクトリに置き換えてくれずエラーになったので$HOME
としてます。うーむ。
DOCKER_BUILDKIT=1
は--secret
を使うために必要な環境変数です。あらかじめexport DOCKER_BUILDKIT=1
しておけば不要です。まあお好みで。
最終形
というわけで私のDockerfile
はこんな感じになりました (フィードのURL等は隠してます)。
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
ENV ASPNETCORE_URLS="https://+;http://+"
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
# Auth with private feed
WORKDIR /tool
RUN wget https://raw.githubusercontent.com/Microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh \
&& chmod +x installcredprovider.sh \
&& ./installcredprovider.sh
WORKDIR /src
COPY ["./MyProject.csproj", "MyProject/"]
ARG FEED=https://pkgs.dev.azure.com/...../nuget/v3/index.json
RUN --mount=type=secret,id=mytoken VSS_NUGET_EXTERNAL_FEED_ENDPOINTS='{"endpointCredentials":[{"endpoint":"'${FEED}'","username":"name","password":"'$(cat /run/secrets/mytoken)'"}]}' dotnet restore -s ${FEED} "MyProject/MyProject.csproj"
COPY . MyProject
WORKDIR "/src/MyProject"
RUN dotnet build "MyProject.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "MyProject.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyProject.dll"]
ビルドコマンド
DOCKER_BUILDKIT=1 docker build --secret id=mytoken,src=$HOME/.nuget/tokens/mytoken -t myproj:1.0.0 .
まとめ
基本的には参考にしたドキュメント群にすべて書かれていたのですが、「NuGetのプライベートfeedをDockerfileでセキュアに読む」というニッチな問題そのものズバリのドキュメントがなかったので自分用のメモとして書きました。他の方の参考にもなれば幸いです。
参考文献
- https://docs.microsoft.com/en-us/azure/devops/artifacts/get-started-nuget?view=azure-devops&tabs=windows
- https://stackoverflow.com/questions/57362453/artifacts-credprovider-and-vss-nuget-external-feed-endpoints-with-docker
- https://medium.com/marionete/pass-secure-information-for-building-docker-images-8adeafe08355
- https://www.dudley.codes/posts/2020.12.28-dockerfile-build-args-exposed/