Visual Studio 2022 + WSL2 + Docker で ASP.NET Core Web アプリのリモート デバッグ環境構築

# はじめに
WSL(Windows Subsystem for Linux) では、Windows 上でネイティブの Linux を起動できます。WSL で、Docker を実行することもでき、Linux コンテナーの開発環境を構築することもできます。
Visual Studio 2022(Visual Studio Code ではなく) では、Docker がサポートされていますが、Docker Desktop の利用が必須となっています。ただし、Docker Desktop を使用せず、Visual Studio 2022 で開発したアプリを Linux コンテナーで実行させて、デバッグしたい場面もあります。
ここでは、Visual Studio 2022 で開発した ASP.NET Core Web アプリ (.NET 6.0) を WSL2 上の Docker コンテナーへ配置し、Visual Studio 2022 からリモートデバッグする方法を説明します。

# 前提条件
- Windows 11
    - もしくは、Windows 10 バージョン 2004(20H1) Build 19041 以降
- WSL2(ディストリビューション: Ubuntu)  に Docker CE をインストール済
    - セットアップは、[WSL2 に Docker をインストールする](https://qiita.com/hiromasa-masuda/items/4902686b404682376c29)を参照ください。
- Visual Studio 2022
    - [ASP.NET と Web 開発] のワークロードが必要です。

# WSL2 への SSH サーバー インストール
Visual Studio 2022　からのリモートデバッグには、WSL に SSH サーバーが必要です。
WSL 側で、パッケージ リポジトリを更新し、openssh-server をインストールします。

    $ sudo apt update
    $ sudo apt install openssh-server

ssh サーバーが起動していることを確認します。

    $ systemctl status ssh
    ● ssh.service - OpenBSD Secure Shell server
        Loaded: loaded (/lib/systemd/system/ssh.service; enabled; vendor preset: enabled)
        Active: active (running) since Fri 2022-12-30 17:59:11 JST; 6min ago
        ...
ここで、Active: active (running) となっていれば、ssh サーバーが起動しています。

Windows 側で、ssh クライアントから、WSL 側の ssh サーバーへ接続します。
WSL へは、localhost で接続できます。

    > ssh <WSL ユーザー名>@localhost
    <WSL ユーザー名>@localhost's password: <- WSL ユーザー名 のパスワードを入力
    Welcome to Ubuntu 22.04.1 LTS (GNU/Linux 5.15.79.1-microsoft-standard-WSL2 x86_64)
    ...
問題なく接続できたら、`exit` で接続を終了してください。

# ASP.NET Core Web アプリ プロジェクトの作成

サンプルとして、Visual Studio 2022 で、ASP.NET Core Web アプリを作成します。
Visual Studio 2022 を起動し、[新しいプロジェクトの作成] - [ASP.NET Core Web アプリ] - [次へ] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/47227d71-d9ae-316b-3b47-5e236db3361b.png)

[プロジェクト名] に任意のプロジェクト名、[場所] に任意の場所を設定し、[次へ] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/f359319c-48ac-930a-bfbc-28bd0ae9ec29.png)

[フレームワーク] は、.NET 6.0 を選択します。[Docker を有効にする] には、チェックしないで、[作成] を選択します。
[Docker を有効にする] にチェックした場合は、Docker Desktop が必要となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/4fa607eb-41b6-b0f7-ee4b-4519aab94beb.png)

どの環境で動作しているかを確認するためのコードを追加していきます。
[ソリューション エクスプローラー] から、プロジェクト以下の [Pages] - [Index.cshtml] をダブルクリックして、コードを編集します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/822c2ee1-bce8-5e21-0bc1-c07d41a54e78.png)

Welcome と Learn about の間に、`<p>@ViewData["OSDescription"]</p>` を入れて、アプリが起動している OS の情報を出力するようにします。
```html:Index.cshtml
@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>@ViewData["OSDescription"]</p>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

```
続いて、[ソリューション エクスプローラー] から、プロジェクト以下の [Pages] - [Index.cshtml] - [Index.cshtml.cs] をダブルクリックして、コードを編集します。
OnGet メソッド内で、ViewData["OSDescription"] に `System.Runtime.InteropServices.RuntimeInformation.OSDescription` を設定します。ページ要求時に、アプリが起動している OS の情報を取得し、ViewData に設定しています。

```csharp:Index.cshtml.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace NetCoreWebAppOnWslDocker001.Pages
{
    public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;

        public IndexModel(ILogger<IndexModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {
            ViewData["OSDescription"] = System.Runtime.InteropServices.RuntimeInformation.OSDescription;
        }
    }
}
```

プロジェクトを実行して、OS の情報が出力されていることを確認します。
Visual Studio 2022 から実行しているので、アプリが、Windows 上で実行されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/b8cd5f1e-c5c1-29fe-e47b-39e08eddce7c.png)

# プロジェクトへの Dockerfile の追加
コンテナーイメージをビルドするために、Dockerfile を作成していきます。
[ソリューション エクスプローラー] から、プロジェクトを選択し、右クリックメニューから、[追加] - [新しい項目] を選択します。[テキスト ファイル] を選択し、[追加] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/0776a444-d2a9-4188-5d74-1869b3915c60.png)

プロジェクト直下に、Dockerfile.txt ファイルが追加されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/ce8b6b78-13ed-e76c-f481-cd0377b04d34.png)

.txt を削除して、Dockerfile にファイル名を変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/92fc2039-b859-eb28-edcc-9151d891164c.png)

Dockerfile をダブルクリックして、ファイルを編集します。
以下は、プロジェクト名: NetCoreWebAppOnWslDocker001 の時の Dockerfile の例です。
NetCoreWebAppOnWslDocker001.csproj、NetCoreWebAppOnWslDocker001.dll を自身で作成したプロジェクトの名前に置き換えて編集してください。編集が終わったら、Dockerfile を保存してください。

```dockerfile:Dockerfile
# ASP.NET Core Runtime イメージの取得
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app/
# ポート 80, 443 でリッスン
EXPOSE 80
EXPOSE 443

# ソースコードのビルド用に .NET SDK イメージの取得
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src/
COPY "NetCoreWebAppOnWslDocker001.csproj" "."
# プロジェクトの依存関係の復元
RUN dotnet restore "NetCoreWebAppOnWslDocker001.csproj"
COPY . .
# ソースコードをビルド 構成:Release, 出力先: /app/build
RUN dotnet build "NetCoreWebAppOnWslDocker001.csproj" -c Release -o /app/build
# ビルドしたモジュールを発行 構成:Release, 出力先: /app/publish, プロパティ: UseAppHost=false
RUN dotnet publish "NetCoreWebAppOnWslDocker001.csproj" -c Release -o /app/publish -p:UseAppHost=false

FROM base
WORKDIR /app/
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "NetCoreWebAppOnWslDocker001.dll"]
```
マルチステージ ビルドを使って、ソースコードビルド用のイメージでコードをビルドしてモジュールを出力、出力したモジュールを公開用イメージに展開するようにしています。

# コンテナー イメージのビルド
WSL 側から、Dockerfile を参照し、コンテナー イメージをビルドします。
Windows の C ドライブは、WSL の /mnt/c/ にマウントされています。WSL 側から、ASP.NET Core Web アプリ プロジェクトを配置したディレクトリへ移動し、`docker build` でイメージをビルドします。

    $ cd /mnt/c/Source/Vs/NetCoreWebAppOnWslDocker001
    $ docker build -t hiromasa/netcorewebapp001 .
    ...
    Successfully built xxxxxxxxxxxx
    Successfully tagged hiromasa/netcorewebapp001:latest

`docker images` で、ビルドしたイメージが一覧にあることを確認します。ビルドに失敗する場合は、Dockerfile の内容を確認してください。

    $ docker images
    REPOSITORY                        TAG       IMAGE ID       CREATED              SIZE
    hiromasa/netcorewebapp001         latest    xxxxxxxxxxxx   About a minute ago   217MB

作成したイメージから、コンテナーを起動します。
`-d`: デーモンで起動するオプション、`-p`: コンテナのポート 80 を 8080 で公開するオプションを指定しています。

    $ docker run -d --name netcorewebapp001 -p 8080:80 hiromasa/netcorewebapp001

Windows 側のブラウザーで、`http://localhost:8080` へアクセスします。
アプリが、WSL(コンテナー) 上で実行されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/c9bac7d0-2e74-cca7-3022-4cfc25926b3e.png)

# Visual Studio 2022 リモート デバッグ
Visual Studio 2022 で、WSL 側のコンテナーのプロセスへアタッチしてリモート デバッグを行っていきます。
Visual Studio 2022 で、先ほどのプロジェクトを開いた状態で、[デバッグ] - [プロセスにアタッチ...] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/b3fdae12-74c2-77d8-b482-b2c2995bb014.png)

[プロセスにアタッチ] ダイアログの [接続の種類] で、`Docker (Linux コンテナー)` を選択し、[検索...] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/8f568759-6cfa-754a-bcaf-0b641798520c.png)

[Docker　コンテナーの選択] ダイアログで、[追加] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/579b3ddf-b506-b022-1bb1-5b3b9c21aae5.png)

[リモート システムへの接続] ダイアログで、[ホスト名] に `localhost`、[ポート] に WSL SSH サーバーのポート番号(デフォルト: 22)、[ユーザー名] に WSL のユーザー名を設定します。[認証の種類] - [パスワード] を選択、[パスワード] に WSL ユーザーのパスワードを入力し、[接続] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/2b6414a7-83e3-1869-922b-b502532a9f93.png)

[ホストキーを受け入れる] ダイアログで、[はい] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/53e9c594-621d-209f-ebad-4fb792b24eb8.png)

WSL 側で起動したコンテナーが表示されるので、選択して、[OK] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/50d33ef5-1685-51cc-2c82-4f856d11b1f1.png)

[プロセスにアタッチ] ダイアログで、[アタッチ先] - [選択...] を選択します。[次のコードの種類をデバッグする] - [マネージド (Unix 用 .NET Core)] にチェックを入れ、[OK] を選択します。次に、[使用可能なプロセス] から、[タイトル] が、`dotnet <エントリポイントの DLL>` である行を選択し、[アタッチ] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/2415df15-612a-50c8-9f61-da9af4b33520.png)

Visual Studio 2022 がデバッグモードになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/7be26084-1728-0c53-95a2-2c1e11b93c8d.png)

[ソリューション エクスプローラー] から、[Index.cshtml.cs] を選択し、17 行目に、ブレイク ポイントを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/d692e200-620f-c196-25f9-ded8897c78f9.png)

`ブレークポイントは、現在の設定ではヒットしません。このドキュメントのシンボルが読み込まれていません。`と表示されます。この状態では、ブレイクポイントにヒットさせることができません。
これは、Dockerfile の `dotnet build -c Release` で、構成をリリースに指定してコードをビルドしたことで、モジュールが最適化されたためです。.NET デバッガーは、最適化されたモジュールを非ユーザーコードとして分類します。非ユーザーコードは、デフォルトの設定では、ブレイクポイント等のデバッグ機能を利用できません。

:::note info
`dotnet build -c Debug` 等の構成でビルドすることで、モジュールの最適化を行わない場合は、以降の設定は必要ありません。これらの設定を行わなくてもブレイクポイントが機能するはずです。
:::

[デバッグ] - [デバッグの停止] でデバッグを停止します。[デバッグ] - [オプション] で [オプション] ダイアログを起動し、[デバッグ] - [全般] - [マイ コードのみを有効にする] のチェックをオフにし、[OK] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/3a710c23-a217-28bc-4173-192fd6765813.png)

[デバッグ] - [プロセスにアタッチ...] - [アタッチ] を選択します。もしくは、[デバッグ] - [プロセスに再アタッチする] を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/acdd981e-015b-dfd1-b17e-4931752382a7.png)

先ほど、警告が出ていたブレークポイントが、今回は、正しく表示されていることを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/bdb37712-3353-a06c-c9c0-d366e9f1c3da.png)

Windows 側のブラウザーで、`http://localhost:8080` へアクセスします。
ブレイクポイントにヒットさせることができることを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172205/9eb523fb-7ab8-0037-0815-6967739415ca.png)

# まとめ
Visual Studio 2022 ローカルで Web アプリをデバッグして開発。開発した Web アプリを Linux のコンテナーで起動して動作確認。この時、Visual Studio 2022 で Linux コンテナーへ接続し、リモートデバッグすることで問題解決が容易になります。
また、WSL は、Windows のファイルシステムにアクセスできるので、Windows 上で開発した Visual Studio プロジェクトを WSL から直接参照し、ソースコードからイメージのビルド、コンテナー起動も容易にできます。
Windows 上でのコンテナー開発における一つの選択肢として、Docker Desktop を使用しない方法を説明しました。
# 参考
- [Dockerfile リファレンス](https://docs.docker.jp/engine/reference/builder.html)
- [ASP.NET Core Runtime | Microsoft Artifact Registry](https://mcr.microsoft.com/en-us/product/dotnet/aspnet/about)
- [.NET SDK | Microsoft Artifact Registry](https://mcr.microsoft.com/product/dotnet/sdk/about)
- [マルチステージビルドの利用](https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/multistage-build/)
- [UseAppHost | MSBuild reference for .NET SDK projects
](https://learn.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props#useapphost)
- ["マイ コードのみ" を使用してユーザー コードのみをデバッグする](https://learn.microsoft.com/ja-jp/visualstudio/debugger/just-my-code?view=vs-2022)
- [シンボル ファイルと Visual Studio のシンボル設定を理解する](https://jpdscore.github.io/blog/debugging/understanding-symbol-files-and-visual-studios-symbol-settings/)
- [Debugging External Sources with Visual Studio](https://devblogs.microsoft.com/visualstudio/debugging-external-sources-with-visual-studio/)