tech.guitarrapc.cóm

Technical updates

Youtubeアクセスをブロックする

YouTubeへのアクセスをブロックするにはいくつかの方法があります。例えばプロキシサーバーを利用したり、ファイアウォールで制御するといった方法が古くから使われています。今回は、UniFi OSを使える環境においてYouTubeへのアクセスをブロックする方法のメモです。

はじめに

本来どこかのサイトへのアクセスをブロックするのはやりたくないものです。ただ、いろいろな理由でネットワーク配下の端末から特定サイトへのアクセスを制限したいこともあるでしょう。 現在のネットワークにおいては、ゲートウェイであるルーターでアクセス制御する以外にも、エンドポイントである端末側で制御する方法もありますし、あるいはネットワーク自体をゼロトラスト化してそのポリシーで制御する方法もあります。そういう意味では、プロキシやファイアウォールを直接扱うことは少なくなってきており、個人的にも避けたいものがあります。

YouTubeへのアクセスをブロックするといっても、なるべく省力化しつつ効果的に行いたいものです。今回はUniFi OSのUIでできることに限定し、SSHでUniFi OSにログインしていじることは除外します。

ブロック方法一覧

コンテンツフィルターとポリシーテーブルでは、アクセスをブロックする対象や粒度が異なります。以下にそれぞれの特徴をまとめます。

方法 アクセス制御方法 影響単位 メリット デメリット
コンテンツフィルター ドメインベース ネットワーク単位 細かくドメインを選んで制御できる YouTube公式ではドメインリスト的な案内はない、端末単位の制御/スケジュール制御はできない
ポリシーテーブル DNS(CNAME)ベース ネットワーク単位 YouTubeが一覧を提供しており4レコードで止まる 端末単位の制御/スケジュール制御はできない
トラフィックルール ドメインベース ネットワーク単位、個別デバイス単位 細かくドメインを選んで制御できる、端末単位の制御/スケジュール制御が可能 YouTube公式ではドメインリスト的な案内はない

コンテンツフィルター

コンテンツフィルターでYouTubeアクセスをブロックしてみましょう。UniFi OSの管理画面から、Settings > CyberSecureを開き、Content Filterタブでネットワークを選択してBlocklistにYouTubeのドメインを追加します。

alt text

追加するドメインは以下の通りです。StackoverflowやRedditなどで紹介されているものを参考にしています。

youtube.com
www.youtube.com
m.youtube.com
ytimg.com
s.ytimg.com
ytimg.l.google.com
youtube.l.google.com
i.google.com
googlevideo.com
youtu.be
yt.be

Add Multipleから追加しても良いですし、1つずつ追加しても構いません。追加後は保存しましょう。

Add Multiple Domains

alt text

alt text

これで、YouTubeへのアクセスがWeb/スマホともにブロックされます。

alt text

ポリシーテーブル

ポリシーテーブルでYouTubeアクセスをブロックしてみましょう。YouTubeが公式に提供しているControl YouTube content available to usersに沿って、DNSベースでCNAMEを調整してアクセスをブロックする方法です。

YouTubeが提供しているCNAMEリストは以下の通りです。

www.youtube.com
m.youtube.com
youtubei.googleapis.com
youtube.googleapis.com
www.youtube-nocookie.com

これらを、制限アクセスの度合に応じて以下のいずれかにCNAMEを向けます。

# 厳しい制限付きモード
restrict.youtube.com
# 中程度の制限付きモード
restrictmoderate.youtube.com

UniFi OSの管理画面から、Settings > Policy Engineの一番左にあるアイコンPolicy Tableを開き、Create New Policyからドメインレコードを追加します。Alias Recordを選択し、上記のCNAMEリストを1つずつ追加していきます。Target Aliasにはrestrict.youtube.comを指定してアクセスブロックとしましょう。

alt text

alt text

一通り追加したら完了です。

alt text

digコマンドで確認すると、以下のようにCNAMEが向いていることがわかります。

$ dig -t CNAME www.youtube-nocookie.com
www.youtube-nocookie.com. 5     IN      CNAME   restrict.youtube.com.

これで、YouTubeへのアクセスがWeb/スマホともにブロックされます。

alt text

トラフィックルール

トラフィックルールでYouTubeアクセスをブロックしてみましょう。 UniFi OSの管理画面から、Settings > Policy Engineの一番左にあるアイコンTraffic & Firewall Rulesを開き、Create entryからドメインレコードを追加します。

alt text

あるいは、デバイス一覧からデバイスを選択してTraffic Rules > Create Ruleでを、ここに飛びます。

alt text

追加するドメインはコンテンツフィルターで紹介したものと同じです。ActionでBlockや速度調整などが選べますが、今回はBlockを選択します。Sourceはネットワーク全体にするか特定デバイスにするか選べます、今回はLoLというPCを選択しています。

alt text

他の止め方と違うのが、Sourceで端末を指定できること、Scheduleで時間帯を指定できることです。例えば1回だけ試しに止めたり、毎週やカスタム曜日+時間帯で止めたりといったことが可能です。

alt text

alt text

alt text

これで、YouTubeへのアクセスがWeb/スマホともにブロックされます。

alt text

まとめ

今回は、YouTubeへのアクセスをUniFi OSでブロックする方法を3つ紹介しました。YouTubeに限らず、特定のサイトへのアクセスを制限したい場合に応用できるでしょう。トラフィックルールは端末単位やスケジュール単位で制御できるのが便利ですが、ドメインが多いのはちょっと厳しいです。うっかりするとWebは止められてもアプリは止まらないということもあります。その点、ポリシーテーブルはCNAMEベースで4レコードだけなので楽ですが、制御の粒度が粗いです。コンテンツフィルターは設定箇所がネットワーク単位でわかりやすく、AllowListも使えて調整しやすい部分があります。

しかし、YouTubeの場合は、特定のチャンネルとかキーワードを止める需要の方が高い気もしなくはないです。その場合はUniFiだと難しい感じがしますがどうなんですかね?

参考

C# NativeAOTをDockerでビルドする

NativeAOTは、.NETアプリケーションをネイティブコードにコンパイルする技術です。 NativeAOTはJITではなくAOTでコンパイルすることで、いくつかのメリットがあります。スタートアップが速くなり、メモリ使用量も少なくなります。また、動作環境にランタイムが不要なため、JITが使えない環境でも動作しますし、デプロイも簡素化されます。

この記事では、C#のNativeAOTアプリケーションをDockerコンテナ内でビルドする方法について解説します。 ここ数年はコンテナもARM64環境で動作して当然となってきているため、.NETのNativeAOTサーバーもARM64対応しましょう。

ゴール

この記事のゴールは、マルチプラットフォーム対応のコンテナビルドである次のコマンドを実行できるようにすることです。

$ docker buildx build --platform linux/amd64,linux/arm64 -f Dockerfile .

また、イメージサイズもできるだけ小さくしてみましょう。

NativeAOTのプロジェクトを用意する

ASP.NET Core Web APIのNativeAOTプロジェクトを用意しましょう。以下のコマンドでプロジェクトを作成するとNativeAOT対応のWeb APIプロジェクトが作成されます。

$ mkdir test
$ dotnet new webapiaot

生成されたものを確認しましょう。

$ ls -l
total 32
-rw-rw-r--    1 guitarrapc guitarrapc      1404 Nov 23 02:59 Program.cs
drwxrwxr-x    2 guitarrapc guitarrapc         0 Nov 23 01:21 Properties
-rw-rw-r--    1 guitarrapc guitarrapc       127 Nov 23 01:21 appsettings.Development.json
-rw-rw-r--    1 guitarrapc guitarrapc       151 Nov 23 01:21 appsettings.json
-rw-rw-r--    1 guitarrapc guitarrapc       642 Nov 23 01:23 test.csproj
-rw-rw-r--    1 guitarrapc guitarrapc       255 Nov 23 01:23 test.csproj.user
-rw-rw-r--    1 guitarrapc guitarrapc       180 Nov 23 01:21 test.http
-rw-rw-r--    1 guitarrapc guitarrapc      1119 Nov 23 01:22 test.sln

test.csprojは次のようになっています。NativeAOT対応のために<PublishAot>true</PublishAot>が指定されていることがポイントです。また、Unicodeやタイムゾーンといったグローバリゼーション不要であることが<InvariantGlobalization>true</InvariantGlobalization>で指定されています。ここは扱うデータによって必要かどうかが変わります。

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <InvariantGlobalization>true</InvariantGlobalization>
    <PublishAot>true</PublishAot>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
    <DockerfileContext>.</DockerfileContext>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
  </ItemGroup>

</Project>

Program.csは次のようになっています。System.Text.Jsonがデフォルトのリフレクションベースではなく、ソースジェネレーターベースになっていることがポイントです。

using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

Todo[] sampleTodos =
[
    new(1, "Walk the dog"),
    new(2, "Do the dishes", DateOnly.FromDateTime(DateTime.Now)),
    new(3, "Do the laundry", DateOnly.FromDateTime(DateTime.Now.AddDays(1))),
    new(4, "Clean the bathroom"),
    new(5, "Clean the car", DateOnly.FromDateTime(DateTime.Now.AddDays(2)))
];

var todosApi = app.MapGroup("/todos");
todosApi.MapGet("/", () => sampleTodos)
        .WithName("GetTodos");

todosApi.MapGet("/{id}", Results<Ok<Todo>, NotFound> (int id) =>
    sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo
        ? TypedResults.Ok(todo)
        : TypedResults.NotFound())
    .WithName("GetTodoById");

app.Run();

public record Todo(int Id, string? Title, DateOnly? DueBy = null, bool IsComplete = false);

[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{

}

Dockerfileを用意する

コマンドからプロジェクトを作るとDockerfileは生成されません1。このためVisual Studioを使って、csprojを右クリック > Add > Container SupportでDockerfileを生成します。

alt text

次のようなDockerfileが生成されます。

# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.

# These ARGs allow for swapping out the base used to make the final image when debugging from VS
ARG LAUNCHING_FROM_VS
# This sets the base image for final, but only if LAUNCHING_FROM_VS has been defined
ARG FINAL_BASE_IMAGE=${LAUNCHING_FROM_VS:+aotdebug}

# This stage is used when running from VS in fast mode (Default for Debug configuration)
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080


# This stage is used to build the service project
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
# Install clang/zlib1g-dev dependencies for publishing to native
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    clang zlib1g-dev
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["test.csproj", "."]
RUN dotnet restore "./test.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "./test.csproj" -c $BUILD_CONFIGURATION -o /app/build

# This stage is used to publish the service project to be copied to the final stage
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./test.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=true

# This stage is used as the base for the final stage when launching from VS to support debugging in regular mode (Default when not using the Debug configuration)
FROM base AS aotdebug
USER root
# Install GDB to support native debugging
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    gdb
USER app

# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
FROM ${FINAL_BASE_IMAGE:-mcr.microsoft.com/dotnet/runtime-deps:10.0} AS final
WORKDIR /app
EXPOSE 8080
COPY --from=publish /app/publish .
ENTRYPOINT ["./test"]

このDockerfileはx64環境でのビルドである次のコマンドでのビルドに対応しています。

# amd64環境でプラットフォームを省略するとlinux/amd64と同義
$ docker buildx build -f Dockerfile .
# 直接指定してもいい
$ docker buildx build --platform linux/amd64 -t test -f Dockerfile .

この状態で、Visual StudioからDockerコンテナでデバッグ実行すると、NativeAOTアプリケーションがビルドされ、コンテナ内で実行されます。

alt text

http://localhost:8080/todosにアクセスすると、次のようなJSONレスポンスが返ってきます。

[{"id":1,"title":"Walk the dog","dueBy":null,"isComplete":false},{"id":2,"title":"Do the dishes","dueBy":"2025-11-22","isComplete":false},{"id":3,"title":"Do the laundry","dueBy":"2025-11-23","isComplete":false},{"id":4,"title":"Clean the bathroom","dueBy":null,"isComplete":false},{"id":5,"title":"Clean the car","dueBy":"2025-11-24","isComplete":false}]

これで下準備は完了です。

NativeAOTアプリケーションをARM64でビルドする

先ほどのDockerfileはARM64プラットフォームではビルドできません。

$ docker buildx build --platform linux/arm64 -t test -f Dockerfile .
[+] Building 30.3s (8/17)                                                                          docker:desktop-linux
 => [internal] load build definition from Dockerfile                                                               0.0s
 => => transferring dockerfile: 2.00kB                                                                             0.0s
 => [internal] load metadata for mcr.microsoft.com/dotnet/runtime-deps:10.0                                        0.2s
 => [internal] load metadata for mcr.microsoft.com/dotnet/sdk:10.0                                                 0.5s
 => [internal] load .dockerignore                                                                                  0.0s
 => => transferring context: 464B                                                                                  0.0s
 => [build 1/8] FROM mcr.microsoft.com/dotnet/sdk:10.0@sha256:c7445f141c04f1a6b454181bd098dcfa606c61ba0bd213d0a7  29.2s
 => => resolve mcr.microsoft.com/dotnet/sdk:10.0@sha256:c7445f141c04f1a6b454181bd098dcfa606c61ba0bd213d0a702489e5  0.0s
 => => sha256:22dab8ebc6e89e49835b31267f75a6fb7424b29e43bf96a5804d83290faf88e7 17.67MB / 17.67MB                   3.8s
 => => sha256:85d854027678bf2e564d8c6af8751002a6ab17752fc9d3059d17bfda6b786a47 519B / 519B                         0.4s
 => => sha256:06863216bce418eaebfa898e891da0ef31f981eaec54d8209a88e736d75b8231 183.98MB / 183.98MB                26.7s
 => => sha256:ddd0e3c00fb65d4dcf97613490f621434369e3fc91ce61251e8af5e3045195cb 23.91MB / 23.91MB                   4.2s
 => => sha256:724261c1460e0b2b2cd34e459af32d077d3a217b3314377de3818e4b0c1ce79a 12.25MB / 12.25MB                   2.7s
 => => sha256:d95936f442c561a911722daba76c39964bc169290e61b297c0ddac14ec04baa5 154B / 154B                         0.4s
 => => sha256:7cce16645c484da5db798ea1c93ef06576e4c7a0cd534a13079d30a883e35cb1 34.61MB / 34.61MB                   6.0s
 => => extracting sha256:7cce16645c484da5db798ea1c93ef06576e4c7a0cd534a13079d30a883e35cb1                          0.3s
 => => extracting sha256:d95936f442c561a911722daba76c39964bc169290e61b297c0ddac14ec04baa5                          0.0s
 => => extracting sha256:724261c1460e0b2b2cd34e459af32d077d3a217b3314377de3818e4b0c1ce79a                          0.1s
 => => extracting sha256:ddd0e3c00fb65d4dcf97613490f621434369e3fc91ce61251e8af5e3045195cb                          0.0s
 => => extracting sha256:06863216bce418eaebfa898e891da0ef31f981eaec54d8209a88e736d75b8231                          2.1s
 => => extracting sha256:85d854027678bf2e564d8c6af8751002a6ab17752fc9d3059d17bfda6b786a47                          0.0s
 => => extracting sha256:22dab8ebc6e89e49835b31267f75a6fb7424b29e43bf96a5804d83290faf88e7                          0.2s
 => [internal] load build context                                                                                  0.0s
 => => transferring context: 1.69kB                                                                                0.0s
 => [final 1/3] FROM mcr.microsoft.com/dotnet/runtime-deps:10.0@sha256:69ee6d1a1b7a92cc82a71001342554d4611a509d88  0.0s
 => => resolve mcr.microsoft.com/dotnet/runtime-deps:10.0@sha256:69ee6d1a1b7a92cc82a71001342554d4611a509d880114b2  0.0s
 => ERROR [build 2/8] RUN apt-get update     && apt-get install -y --no-install-recommends     clang zlib1g-dev    0.6s
------
 > [build 2/8] RUN apt-get update     && apt-get install -y --no-install-recommends     clang zlib1g-dev:
0.177 exec /bin/sh: exec format error
------
Dockerfile:18
--------------------
  17 |     # Install clang/zlib1g-dev dependencies for publishing to native
  18 | >>> RUN apt-get update \
  19 | >>>     && apt-get install -y --no-install-recommends \
  20 | >>>     clang zlib1g-dev
  21 |     ARG BUILD_CONFIGURATION=Release
--------------------
ERROR: failed to build: failed to solve: process "/bin/sh -c apt-get update     && apt-get install -y --no-install-recommends     clang zlib1g-dev" did not complete successfully: exit code: 255

ARM64環境でNativeAOTアプリケーションをビルドできるようにDockerfileを修正します。修正にあたり、NativeAOTアプリケーションをARM64でコンテナビルドするために必要なことを整理しておきましょう。

  • クロスコンパイル用のツールチェインをインストールする gcc-aarch64-linux-gnu libc6-dev-arm64-cross binutils-aarch64-linux-gnu
  • objcopyコマンドをクロスコンパイル用のものに置き換える aarch64-linux-gnu-objcopy
  • dotnet builddotnet publishのビルドターゲットに-a arm64を指定する
  • Dockerfileのビルドステージでプラットフォームを受け取るため、ビルドステージに--platform=$BUILDPLATFORMを指定、ビルド引数にARG TARGETARCHを追加

これらを踏まえて、Dockerfileを修正します。

# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.

# These ARGs allow for swapping out the base used to make the final image when debugging from VS
ARG LAUNCHING_FROM_VS
# This sets the base image for final, but only if LAUNCHING_FROM_VS has been defined
ARG FINAL_BASE_IMAGE=${LAUNCHING_FROM_VS:+aotdebug}

# This stage is used when running from VS in fast mode (Default for Debug configuration)
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080


# This stage is used to build the service project
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
ARG TARGETARCH
WORKDIR /src
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    clang zlib1g-dev \
    && if [ "$TARGETARCH" = "arm64" ]; then apt-get install -y --no-install-recommends gcc-aarch64-linux-gnu libc6-dev-arm64-cross binutils-aarch64-linux-gnu; fi \
    && rm -rf /var/lib/apt/lists/*
COPY ["test.csproj", "."]
RUN dotnet restore "./test.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "./test.csproj" -c $BUILD_CONFIGURATION -a $TARGETARCH -o /app/build

# This stage is used to publish the service project to be copied to the final stage
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
# Setup objcopy for ARM64 cross-compilation
RUN if [ "$TARGETARCH" = "arm64" ]; then ln -sf /usr/bin/aarch64-linux-gnu-objcopy /usr/local/bin/objcopy; fi
RUN dotnet publish "./test.csproj" -c $BUILD_CONFIGURATION -a $TARGETARCH -o /app/publish /p:UseAppHost=true

# This stage is used as the base for the final stage when launching from VS to support debugging in regular mode (Default when not using the Debug configuration)
FROM base AS aotdebug
USER root
# Install GDB to support native debugging
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    gdb
USER app

# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
FROM ${FINAL_BASE_IMAGE:-mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled-extra} AS final
WORKDIR /app
EXPOSE 8080
COPY --from=publish /app/publish .
ENTRYPOINT ["./test"]

以下はARM64対応したDockerfileの差分です。 主要なコマンドはいじらずに対応できているのが分かります。注意点として、ARG TARGETARCHはビルドステージの頭で追加してください。AMD64/ARM64プラットフォーム別にコマンドを変える条件分離を入れるためです。

- FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+ FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
+ ARG TARGETARCH
WORKDIR /src
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
    clang zlib1g-dev \
+     && if [ "$TARGETARCH" = "arm64" ]; then apt-get install -y --no-install-recommends gcc-aarch64-linux-gnu libc6-dev-arm64-cross binutils-aarch64-linux-gnu; fi \
+     && rm -rf /var/lib/apt/lists/*
COPY ["test.csproj", "."]
RUN dotnet restore "./test.csproj"
COPY . .
WORKDIR "/src/."
- RUN dotnet build "./test.csproj" -c $BUILD_CONFIGURATION -o /app/build
+ RUN dotnet build "./test.csproj" -c $BUILD_CONFIGURATION -a $TARGETARCH -o /app/build

# This stage is used to publish the service project to be copied to the final stage
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
+ # Setup objcopy for ARM64 cross-compilation
+ RUN if [ "$TARGETARCH" = "arm64" ]; then ln -sf /usr/bin/aarch64-linux-gnu-objcopy /usr/local/bin/objcopy; fi
- RUN dotnet publish "./test.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=true
+ RUN dotnet publish "./test.csproj" -c $BUILD_CONFIGURATION -a $TARGETARCH -o /app/publish /p:UseAppHost=true

それでは、修正したDockerfileでマルチプラットフォームビルドを実行しましょう。

$ docker buildx build --platform linux/amd64,linux/arm64 -t test -f Dockerfile .
[+] Building 265.8s (31/31) FINISHED                                                               docker:desktop-linux
 => [internal] load build definition from Dockerfile                                                               0.0s
 => => transferring dockerfile: 2.37kB                                                                             0.0s
 => [linux/amd64 internal] load metadata for mcr.microsoft.com/dotnet/runtime-deps:10.0                            1.2s
 => [linux/amd64 internal] load metadata for mcr.microsoft.com/dotnet/sdk:10.0                                     1.2s
 => [linux/arm64 internal] load metadata for mcr.microsoft.com/dotnet/runtime-deps:10.0                            1.5s
 => [internal] load .dockerignore                                                                                  0.0s
 => => transferring context: 464B                                                                                  0.0s
 => [internal] load build context                                                                                  0.0s
 => => transferring context: 4.67kB                                                                                0.0s
 => [linux/arm64 final 1/3] FROM mcr.microsoft.com/dotnet/runtime-deps:10.0@sha256:69ee6d1a1b7a92cc82a71001342554  3.7s
 => => resolve mcr.microsoft.com/dotnet/runtime-deps:10.0@sha256:69ee6d1a1b7a92cc82a71001342554d4611a509d880114b2  0.0s
 => => sha256:7d9e1832d26ac2daa9e6af016a0da63d925a22c72c7673707338d057ccc3a068 3.56kB / 3.56kB                     0.4s
 => => sha256:97dd3f0ce510a30a2868ff104e9ff286ffc0ef01284aebe383ea81e85e26a415 28.86MB / 28.86MB                   2.0s
 => => sha256:5c57a8c859e2e325b62a2c81fc3da56db2274f2084df97f9fa118f751c1f0d76 16.79MB / 16.79MB                   3.2s
 => => extracting sha256:97dd3f0ce510a30a2868ff104e9ff286ffc0ef01284aebe383ea81e85e26a415                          0.9s
 => => extracting sha256:5c57a8c859e2e325b62a2c81fc3da56db2274f2084df97f9fa118f751c1f0d76                          0.4s
 => => extracting sha256:7d9e1832d26ac2daa9e6af016a0da63d925a22c72c7673707338d057ccc3a068                          0.0s
 => [linux/amd64 build 1/8] FROM mcr.microsoft.com/dotnet/sdk:10.0@sha256:c7445f141c04f1a6b454181bd098dcfa606c61  19.8s
 => => resolve mcr.microsoft.com/dotnet/sdk:10.0@sha256:c7445f141c04f1a6b454181bd098dcfa606c61ba0bd213d0a702489e5  0.0s
 => => sha256:60be9a169ff87b6d4f370e679741c0c2715471260372e4468e5cf5b1ed5e66bd 17.60MB / 17.60MB                   1.3s
 => => sha256:e1a43a11c338e681e5189550383f50b55854c0ca90d6a1b29b544eed52260c1e 492B / 492B                         0.1s
 => => sha256:cdb9f2f2f20d8b619a2ec71ca268aeb85d54781135d4ce6fe53b7f814b525e21 188.45MB / 188.45MB                14.0s
 => => sha256:82c9b8487b966e2a36d3591884c9f9dcb81a0a067522c3f057b69ad0c27210f8 23.60MB / 23.60MB                   2.2s
 => => sha256:ad3a33b726b12ed0a2013a69491146deda74c174c57edbfb0d07a7a536d1d1e5 12.76MB / 12.76MB                   2.6s
 => => sha256:cf846b836998163e04850ddb5c7e2210eb8c95bc0c221230c5d6a26cc7de63e7 155B / 155B                         0.1s
 => => sha256:66ecb3299fecbc0d05e411d7a62b0e25ba005c3ed4ad1ebdcd89c2692ab95415 36.71MB / 36.71MB                   3.0s
 => => extracting sha256:66ecb3299fecbc0d05e411d7a62b0e25ba005c3ed4ad1ebdcd89c2692ab95415                          0.3s
 => => extracting sha256:cf846b836998163e04850ddb5c7e2210eb8c95bc0c221230c5d6a26cc7de63e7                          0.0s
 => => extracting sha256:ad3a33b726b12ed0a2013a69491146deda74c174c57edbfb0d07a7a536d1d1e5                          0.1s
 => => extracting sha256:82c9b8487b966e2a36d3591884c9f9dcb81a0a067522c3f057b69ad0c27210f8                          0.6s
 => => extracting sha256:cdb9f2f2f20d8b619a2ec71ca268aeb85d54781135d4ce6fe53b7f814b525e21                          2.2s
 => => extracting sha256:e1a43a11c338e681e5189550383f50b55854c0ca90d6a1b29b544eed52260c1e                          0.0s
 => => extracting sha256:60be9a169ff87b6d4f370e679741c0c2715471260372e4468e5cf5b1ed5e66bd                          0.2s
 => [linux/amd64 final 1/3] FROM mcr.microsoft.com/dotnet/runtime-deps:10.0@sha256:69ee6d1a1b7a92cc82a71001342554  8.1s
 => => resolve mcr.microsoft.com/dotnet/runtime-deps:10.0@sha256:69ee6d1a1b7a92cc82a71001342554d4611a509d880114b2  0.0s
 => => sha256:e556599eb01843481eb2ced4641c7afea2a15f40af57aa236dae733bac7035f4 3.54kB / 3.54kB                     0.2s
 => => sha256:05a997a818e912916b5455ed2a572f4f1779812770cf6301e11fb2f86b92a136 16.82MB / 16.82MB                   3.1s
 => => sha256:20043066d3d5c78b45520c5707319835ac7d1f3d7f0dded0138ea0897d6a3188 29.72MB / 29.72MB                   6.5s
 => => extracting sha256:20043066d3d5c78b45520c5707319835ac7d1f3d7f0dded0138ea0897d6a3188                          0.8s
 => => extracting sha256:05a997a818e912916b5455ed2a572f4f1779812770cf6301e11fb2f86b92a136                          0.4s
 => => extracting sha256:e556599eb01843481eb2ced4641c7afea2a15f40af57aa236dae733bac7035f4                          0.0s
 => [linux/arm64 final 2/3] WORKDIR /app                                                                           0.3s
 => [linux/amd64 final 2/3] WORKDIR /app                                                                           0.2s
 => [linux/amd64->arm64 build 2/8] WORKDIR /src                                                                    0.4s
 => [linux/amd64 build 3/8] RUN apt-get update     && apt-get install -y --no-install-recommends     clang zlib1  38.9s
 => [linux/amd64->arm64 build 3/8] RUN apt-get update     && apt-get install -y --no-install-recommends     cla  211.2s
 => [linux/amd64 build 4/8] COPY [test.csproj, .]                                                                  0.0s
 => [linux/amd64 build 5/8] RUN dotnet restore "./test.csproj"                                                     4.0s
 => [linux/amd64 build 6/8] COPY . .                                                                               0.0s
 => [linux/amd64 build 7/8] WORKDIR /src/.                                                                         0.0s
 => [linux/amd64 build 8/8] RUN dotnet build "./test.csproj" -c Release -a amd64 -o /app/build                     7.6s
 => [linux/amd64 publish 1/2] RUN if [ "amd64" = "arm64" ]; then ln -sf /usr/bin/aarch64-linux-gnu-objcopy /usr/l  0.2s
 => [linux/amd64 publish 2/2] RUN dotnet publish "./test.csproj" -c Release -a amd64 -o /app/publish /p:UseAppHo  19.5s
 => [linux/amd64 final 3/3] COPY --from=publish /app/publish .                                                     0.1s
 => [linux/amd64->arm64 build 4/8] COPY [test.csproj, .]                                                           0.1s
 => [linux/amd64->arm64 build 5/8] RUN dotnet restore "./test.csproj"                                              3.8s
 => [linux/amd64->arm64 build 6/8] COPY . .                                                                        0.1s
 => [linux/amd64->arm64 build 7/8] WORKDIR /src/.                                                                  0.0s
 => [linux/amd64->arm64 build 8/8] RUN dotnet build "./test.csproj" -c Release -a arm64 -o /app/build              8.8s
 => [linux/amd64->arm64 publish 1/2] RUN if [ "arm64" = "arm64" ]; then ln -sf /usr/bin/aarch64-linux-gnu-objcopy  0.3s
 => [linux/amd64->arm64 publish 2/2] RUN dotnet publish "./test.csproj" -c Release -a arm64 -o /app/publish /p:U  19.1s
 => [linux/arm64 final 3/3] COPY --from=publish /app/publish .                                                     0.1s
 => exporting to image                                                                                             1.0s
 => => exporting layers                                                                                            0.8s
 => => exporting manifest sha256:32e8341d99b7b23dd8f6b3c21a928012a81132fa9213994f2a5b499824b838c0                  0.0s
 => => exporting config sha256:74e4fe491bc324391b2b08e2c9401772b96d8dc0f79b46fa8beef4bf55d71f54                    0.0s
 => => exporting attestation manifest sha256:167bd3ee1f68e14edefa8b63ca65e373aac2f8ba589bb8b8f44812c6f22d36fb      0.0s
 => => exporting manifest sha256:3d054d4fbfd6ee4be57376561389339e0379ed33798311288f96c61612ee9217                  0.0s
 => => exporting config sha256:5795e8408dc1294fbf45f012bbe5e7a859dd9be5640231e9c1f16873bd99cf17                    0.0s
 => => exporting attestation manifest sha256:d1761acdadfde89e69451f265dad1bd51c21e67bdbae3806ad78db22a49c11f2      0.0s
 => => exporting manifest list sha256:3b7e5f49296d3b38fa075253ab918538974fee542a9f9b40648c165e04ffaaa7             0.0s
 => => naming to docker.io/library/test:latest                                                                     0.0s
 => => unpacking to docker.io/library/test:latest                                                                  0.1s

イメージのビルドが成功しました。これでx64/ARM64両対応したNativeAOTコンテナをビルドできるようになりました。 イメージの状態を見てみましょう。

$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
test         latest    6e2edcd22ece   39 seconds ago   289MB

アプリケーションをローカルLinuxでビルドしてみると、アプリケーション自身は10MB程度 + デバッグシンボルで24MB程度です。

$ ls -l ./bin/Release/net10.0/linux-x64/publish/
total 35444
-rwxrwxrwx 1 guitarrapc guitarrapc      127 Nov 23 01:21 appsettings.Development.json
-rwxrwxrwx 1 guitarrapc guitarrapc      151 Nov 23 01:21 appsettings.json
-rwxrwxrwx 1 guitarrapc guitarrapc 11086080 Nov 23 02:24 test
-rwxrwxrwx 1 guitarrapc guitarrapc 25206536 Nov 23 02:24 test.dbg
-rwxrwxrwx 1 guitarrapc guitarrapc       53 Nov 23 02:24 test.staticwebassets.endpoints.json

こう考えるとコンテナのイメージサイズが289MBなのは大きいですね。イメージサイズが大きいのは、ベースイメージにmcr.microsoft.com/dotnet/runtime-deps:10.0を使用しているためです。 NativeAOTアプリケーションはランタイムを必要としないこと、chiseldイメージでの動作に対応しているので、次のセクションでイメージサイズを削減しましょう。

イメージサイズを削減する

mcr.microsoft.com/dotnet/runtime-deps:10.0は汎用的なランタイム非依存イメージであり、多くのライブラリが含まれています。このため、NativeAOTアプリケーションのコンテナイメージとしては大きすぎます。このような時に利用するのが、chiseledイメージです。

chiseledイメージにはいくつか種類があるのですが、それぞれの特徴は次の通りです。

  • mcr.microsoft.com/dotnet/runtime-deps:10.0-chiseled: CoreCLRのみ。tzdata、ICUは含まれない
  • mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled-extra: CoreCLR, tzdata, ICU, stdc++が含まれる

日本語含むUnicodeやグローバリゼーション対応が不要ならmcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled、あるいはUnicodeやグローバリゼーション対応が必要ならmcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled-extraを使用するといいでしょう。イメージサイズは10.0-chiseledより10.0-noble-chiseled-extraの方が大きくなるのは当然ですが、それでもruntime-deps:10.0よりは小さくなります。

ベースイメージを各chiseledに置き換えて、マルチプラットフォームビルドを実行するには次のように書き換えます。

# Unicode対応不要なら
- FROM ${FINAL_BASE_IMAGE:-mcr.microsoft.com/dotnet/runtime-deps:10.0} AS final
+ FROM ${FINAL_BASE_IMAGE:-mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled} AS final

# Unicode対応必要なら
- FROM ${FINAL_BASE_IMAGE:-mcr.microsoft.com/dotnet/runtime-deps:10.0} AS final
+ FROM ${FINAL_BASE_IMAGE:-mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled-extra} AS final

ベースイメージごとに差し替えてビルドした成果物イメージサイズは次の通りです。AOTアプリケーション自体のサイズは変わらないため、ベースイメージのサイズ差がそのままイメージサイズに反映されます。chiseledイメージはかなり小さく魅力的です。

ベースイメージ イメージサイズ
mcr.microsoft.com/dotnet/runtime-deps:10.0 289MB
mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled 88.3MB
mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled-extra 160MB

コンテナの動作確認

最も小さいmcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseledベースでビルドしたイメージはちゃんと動作するのか、コンテナ実行してみましょう。

$ docker run -it -p 8080:8080 test
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://[::]:8080
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app

期待通り結果が返ってきます。ミニマムな実装とはいえ、スタートラインとしては良いでしょう。

$ curl -i http://localhost:8080/todos
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 22 Nov 2025 17:43:45 GMT
Server: Kestrel
Transfer-Encoding: chunked

[{"id":1,"title":"Walk the dog","dueBy":null,"isComplete":false},{"id":2,"title":"Do the dishes","dueBy":"2025-11-22","isComplete":false},{"id":3,"title":"Do the laundry","dueBy":"2025-11-23","isComplete":false},{"id":4,"title":"Clean the bathroom","dueBy":null,"isComplete":false},{"id":5,"title":"Clean the car","dueBy":"2025-11-24","isComplete":false}]

まとめ

C#でWeb APIというと、どうしてもASP.NET CoreでJITランタイムを使うイメージが強いですが、NativeAOTを使うことでネイティブコード化も当然のように可能です。特にスタートアップ速度を求める環境やWASMを想定するなら、NativeAOTは非常に有用です。一方で、JITランタイムはPGOやDATASをはじめとする実行後も最適化をかける技術が進展しています。結果として、長時間稼働するサーバーアプリケーションではJITランタイムの方が高速2になるケースもあります。

NativeAOTにする課題として、スタックトレースがネイティブコード由来になるためJITランタイムよりもデバッグが難しくなります。また、AOTコンパイル時にすべてのコードが解決されるため、リフレクションや動的コード生成を多用するアプリケーションは対応が難しいですが、これはSource Generatorを活用するなどスタンダードが変わっているところです。NativeAOTはビルド時間も伸びるのですが、.NET9からみても.NET10でさらに改善されてきました。もっと早くなるともっと嬉しい。

NativeAOTが最高、絶対これがいい、とすべてに対して言えるわけではありません。しかし、C#はJITランタイムだけでなくNativeAOTも選択肢として提供されており、すでに多くのシナリオで利用可能です。用途に応じてJITランタイムとNativeAOTを使い分けていきましょう。


  1. Visual Studio 2022以降でNativeAOTなWeb APIプロジェクトを作る場合、作成段階でDockerfileを作るオプションがあります
  2. スタートアップ時間ではなく、それぞれの処理の実行速度を指す

Devtronを触ったメモ

Kubernetesはいわゆるコンテナオーケストレーターですが、素で使うことはほとんどなく様々なOSSやツールと組み合わせて運用されることが多いです。 私もHelmチャートを使って様々なコントローラーを導入しています。KEDAで水平スケールを管理し、ArgoCDを使ってGitOps運用したり、HeadlampやDatadogでモニタリングしたりです。

多くのツールとクラウドを組み合わせ、連携してようやく本格的なKubernetes運用ができます。これは、Kubernetesの難しさを象徴する一面と言えるでしょう。普段運用していると気づきにくいですが、ふと振り返ったり、誰かに説明したり、オンボーディングをサポートするときに本当に難しいと感じます。そもそもKubernetesを運用するには多くの知識1が必要です。この状況はKubernetesが登場して以来、複雑になる一方で、初心者がKubernetesを使い始めるのは常にハードル高い状態です。

そんなKubernetesの運用に必要な機能をオールインワンで提供するプラットフォームがいくつか登場しています。Devtronもその1つです。今回は、Devtronがどう解決しようとしているのか触ったメモです。

Devtronの目指すところ

Devtronの公式ページを見ると、DevtronはKubernetesのアプリケーションとインフラ管理を統合しAIアシスタントが運用を簡素化し、デリバリーを加速することを目的としています。

Devtron is purpose-built for production Kubernetes teams, unifying app and infrastructure management with an AI teammate that simplifies operations and speeds delivery.

引用: https://devtron.ai/

alt text

DevtronはKubernetesの上に様々な機能を提供しており、アプリケーションのデプロイから監視、セキュリティまで幅広くカバーしています。具体的には、複数のOSSを組み合わせています。代表的な機能は以下の通りです。

  • CI/CD統合 = ビルドをDevtronが制御、そのビルド結果をDevtronが制御するArgoCD/Fluxでデプロイ、デプロイ履歴やデプロイ問題をAIが検知、自動ロールバック
  • セキュリティ統合 = イメージやマニフェストのスキャン、シークレットがコードやコンフィグに露出していないかチェック、シップ前にライセンススキャン
  • Observability = PrometheusやGrafanaをDevtron UIに組み込んで監視を一か所から提供
  • コスト管理 = コストを環境単位でモニタリング、AIによるサイジングや異常検出

これらをDevtronから提供するだけでなくUIも一元化することで、Kubernetesの運用を簡素化しようとしています。

Devtronのインストール

DevtronはHelmチャートを使って導入します。AWS EKS上に導入する場合を例に説明します。前提条件は次の通りです。

  • EKSクラスターが作成済み(この記事ではEKS Automodeを使用)
  • kubectl、helmコマンドがインストール済み
  • クラスターへの接続が確立済み

まずはHelmリポジトリを追加します。

$ helm repo add devtron https://helm.devtron.ai
$ helm repo update

Devtronは内部で利用しているpostgresql podでストレージが必要です。EKS AutomodeならEBS CSIドライバーがデフォルトで組み込まれているので、スムーズに進められます。

$ cat << EOF > storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: auto-ebs-sc
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
allowedTopologies:
  - matchLabelExpressions:
      - key: eks.amazonaws.com/compute-type
        values:
          - auto
provisioner: ebs.csi.eks.amazonaws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
  type: gp3
  encrypted: "true"
EOF

$ kubectl apply -f ./storageclass.yaml

準備ができたら、Devtronをインストールします。少し初期設定から変更したい点をvalues.yamlで定義します。cicdモジュールを入れます2。初期状態はDevtron ServiceをLoadBalancerとして作成するので、ClusterIPに変更してport-forwardでアクセスします。検証ではなくユーザー認証を利用して展開するなら、ingress.enabled: trueでALBを作成すると良いでしょう。

$ cat << EOF > devtron-values.yaml
gateway:
  storageClass: "auto-ebs-sc"
installer
  modules:
    - cicd
components:
  devtron:
    service:
      type: ClusterIP
EOF
$ helm upgrade --install devtron devtron/devtron-operator --version 0.22.98 -n devtroncd --create-namespace -f devtron-values.yaml

インストールすると徐々にPodが起動してきます。Podの依存関係や起動タイミングの問題でスムーズには起動しません。何度かPodステータスがErrorやCrashLoopBackOffになりますが、10-15分ほど待つと最終的に安定します。 cicdモジュールを入れた場合、以下のようなpodが起動します。

$ kubectl get po -A
NAME                                       READY   STATUS      RESTARTS        AGE
app-sync-job-vykie-hj7xv                   1/1     Running     0               4m56s
argocd-dex-server-56b6556c7f-r9xw2         1/1     Running     0               4m57s
dashboard-784b5bbcfb-b8k7d                 1/1     Running     0               4m57s
devtron-584d7b6797-nn6kd                   1/1     Running     2 (118s ago)    2m
devtron-nats-0                             3/3     Running     0               4m56s
devtron-nats-test-request-reply            0/1     Completed   0               4m57s
Git-sensor-0                               1/1     Running     4 (2m29s ago)   4m56s
inception-7b69fb787c-2qj8z                 1/1     Running     0               4m57s
kubelink-f8d5bf985-579gj                   1/1     Running     2 (118s ago)    2m
kubewatch-5c6fd4d4b5-8rlt4                 1/1     Running     4 (2m2s ago)    4m57s
lens-55cdc47f9b-7lmqz                      1/1     Running     4 (2m41s ago)   4m57s
postgresql-migrate-casbin-0b0tv-wldnl      0/1     Completed   0               4m56s
postgresql-migrate-devtron-qqdbt-28hh4     0/1     Completed   2               4m56s
postgresql-migrate-gitsensor-g52lv-pk6gs   0/1     Completed   3               4m56s
postgresql-migrate-lens-rmqgj-skppn        0/1     Completed   3               4m56s
postgresql-postgresql-0                    2/2     Running     0               4m56s

Devtron権限管理

Devtronを触り始めるにあたり、port-forwardによるアクセスではOIDC認証ができないことを前回の記事で紹介しました。このため、port-forwardでアクセスした場合、デフォルトのadminユーザーで操作することになります。いったん操作を試すならこれでも大丈夫です。adminユーザーの初期パスワードは、以下のコマンドで取得できます。3

kubectl -n devtroncd get secret devtron-secret -o jsonpath='{.data.ADMIN_PASSWORD}' | base64 -d

alt text

ちなみにOIDC認証含むSSOを利用する場合、権限を定義してグループやユーザーに紐づけることで操作対象を制約できます。

alt text

DevtronのUI

サイドバーから様々な機能にアクセスできます。

alt text

Applicationがメイン操作です。アプリケーションを定義して、そこにビルド、デプロイを行います。Devtronで定義したアプリケーション以外に、helmで導入したコントローラーはHelm Appsとして表示されたり、ArgoCDで管理しているアプリケーションはArgoCD Appsとして表示されます。

alt text

Resource Browserは、いわゆる簡易Kubernetesダッシュボードです。Kubernetesに展開されているリソースを確認したり、YAML操作できます。

Chart Storeは、Helmチャートを選択してインストールことができます。Artifact Hubが初期状態で登録されているので、そこからチャートを選択してインストールできます。

alt text

Global Configurationは、Devtron全体の設定を調整します。例えば、クラスター登録、チャートリポジトリ登録、Gitリポジトリの登録や、イメージ/OCIレジストリの登録、ユーザーや認証回りの設定です。クラスターを追加してマルチクラスターができるのはいいですね。

alt text

alt text

Devtron Stack Managerは、Devtronが内部で利用するコンポーネントの選択です。ArgoCDやTrivyなど、Devtronが利用するOSSコンポーネントを選択してインストールできます。

alt text

alt text

Devtron Stack Managerでコンポーネントを追加

導入したら手始めに操作するのが、Devtron Stack Managerです。たとえばMonitoring (Grafana)を選択するとGrafana一式がインストールされます。

alt text

他にもArgoCDやTrivyなど、様々なコンポーネントを追加できます。例えばargocdを追加すると次のようなpodが起動します。

$ kubectl get po -A
NAMESPACE   NAME                                       READY   STATUS      RESTARTS      AGE
argo        workflow-controller-7fbdc9c877-4l8h4       1/1     Running     0             17m
devtroncd   argocd-application-controller-0            1/1     Running     0             9m10s
devtroncd   argocd-dex-server-56b6556c7f-r9xw2         1/1     Running     0             17m
devtroncd   argocd-Redis-86b6b9888b-5lmwg              1/1     Running     0             9m10s
devtroncd   argocd-repo-server-78fcb8745b-9mwvk        1/1     Running     0             9m10s
devtroncd   argocd-server-8474776d6-ssrlq              1/1     Running     0             9m10s
devtroncd   dashboard-784b5bbcfb-b8k7d                 1/1     Running     0             17m
devtroncd   devtron-9f55ffd8f-4nkc9                    1/1     Running     0             9m4s
devtroncd   devtron-nats-0                             3/3     Running     0             17m
devtroncd   devtron-nats-test-request-reply            0/1     Completed   0             17m
devtroncd   Git-sensor-0                               1/1     Running     4 (14m ago)   17m
devtroncd   inception-7b69fb787c-2qj8z                 1/1     Running     0             17m
devtroncd   kubelink-545b8f465c-hw46g                  1/1     Running     0             9m4s
devtroncd   kubewatch-5c6fd4d4b5-8rlt4                 1/1     Running     4 (14m ago)   17m
devtroncd   lens-55cdc47f9b-7lmqz                      1/1     Running     4 (14m ago)   17m
devtroncd   postgresql-migrate-casbin-3ef7n-d56rd      0/1     Completed   0             9m10s
devtroncd   postgresql-migrate-devtron-wcxwp-dhpsb     0/1     Completed   0             9m10s
devtroncd   postgresql-migrate-gitsensor-wgxiq-8cxsf   0/1     Completed   0             9m10s
devtroncd   postgresql-migrate-lens-cdgiz-vk452        0/1     Completed   0             9m10s
devtroncd   postgresql-postgresql-0                    2/2     Running     0             17m

さらにtrivyを追加すると、次のようなpodが起動します。

$ k get po -A
NAMESPACE   NAME                                       READY   STATUS      RESTARTS      AGE
argo        workflow-controller-7fbdc9c877-4l8h4       1/1     Running     0             39m
devtroncd   argocd-application-controller-0            1/1     Running     0             31m
devtroncd   argocd-dex-server-56b6556c7f-r9xw2         1/1     Running     0             39m
devtroncd   argocd-Redis-86b6b9888b-5lmwg              1/1     Running     0             31m
devtroncd   argocd-repo-server-78fcb8745b-9mwvk        1/1     Running     0             31m
devtroncd   argocd-server-8474776d6-ssrlq              1/1     Running     0             31m
devtroncd   dashboard-784b5bbcfb-b8k7d                 1/1     Running     0             39m
devtroncd   devtron-7688fbc8d8-ck7qx                   1/1     Running     0             15m
devtroncd   devtron-nats-0                             3/3     Running     0             39m
devtroncd   devtron-nats-test-request-reply            0/1     Completed   0             39m
devtroncd   Git-sensor-0                               1/1     Running     4 (37m ago)   39m
devtroncd   image-scanner-6b5b5b976c-qgfvx             1/1     Running     0             15m
devtroncd   inception-7b69fb787c-2qj8z                 1/1     Running     0             39m
devtroncd   kubelink-668c7ccb7d-qhm6j                  1/1     Running     0             15m
devtroncd   kubewatch-5c6fd4d4b5-8rlt4                 1/1     Running     4 (36m ago)   39m
devtroncd   lens-55cdc47f9b-7lmqz                      1/1     Running     4 (37m ago)   39m
devtroncd   postgresql-migrate-casbin-sspac-zk9×8      0/1     Completed   0             15m
devtroncd   postgresql-migrate-devtron-idjwh-grfwr     0/1     Completed   0             15m
devtroncd   postgresql-migrate-gitsensor-glbv3-8qflx   0/1     Completed   0             15m
devtroncd   postgresql-migrate-lens-jwyyd-b4hzg        0/1     Completed   0             15m
devtroncd   postgresql-postgresql-0                    2/2     Running     0             39m

Devtronの難しさ

Devtronを触っていてなかなか難しいと感じる部分があります。まず、Devtronは3つのワークロードが考えられます。

  • CI/CDワークロード = Devtronが提供するCI/CD機能を使ってビルド、デプロイを行う
  • ArgoCD/Fluxワークロード = ArgoCDアプリケーションをデプロイしてDevtronから管理
  • Helmワークロード = HelmチャートをDevtronからインストールして管理

Devtronの理想的には、CI/CDからDevtron (つまりKubernetes)で行うワークロードに見えます。コンセプトの目指すところはめちゃめちゃ高いですが、現時点では難易度が高めです。

Devtron内部でCI/CDを組む

例えばEKSにおけるGitOpsは、CIでビルドしてコンテナイメージをECRに配置、これを参照するKubernetes定義をシングルソースとしてGitHubなどに配置、CDとしてArgoCD/FluxでKubernetesにデプロイ、というシナリオが一般的です。Devtronはこれをさらに一歩進めて、CIもDevtronで行うことを想定しています。つまりDevtronでGit Cloneしてコンテナビルド -> レジストリに登録 -> これをさらにArgoCDなどで展開というシナリオです。GitOpsだけじゃなくて、CI/CDがセットになるのが特徴と言えます。Dockerfileでのビルド以外にDockerファイルなしのシナリオもDevtronにありますが、それはJava/Go/Python/Nodeなどで.NETはありません。まぁそんなものです、知ってた。

EKS的に難しいのが、DevtronからECRへのPushが必須なところです。DevtronのECRレジストリ登録UIを見ると、AWS認証はEC2 Instance ProfileかAccessKey/AccesSecretしか想定になさそうです。AWS EKSは最低限IRSA、可能ならIAM Pod Identityで認証をとるのが一般的です。特にEKS AutomodeはIMDSv2のホップ調整ができないため、EC2 Instance Profileから認証をとってくる設計は難しいです。EKSでDevtronを使うなら自前KarpenterなどのManaged Nodeである必要があります。

AWSを例に挙げましたが、認証系を各クラウド事情に沿ってアップデートしていくためにはDevtron開発者自身の理解が必要でしょう。AIの補助があっても開発リソースには負担となりそうですね。がんばってほしい。

ユーザー管理

DevtronのUser管理はSSOが前提になっています。これも運用上の難しさがあります。

前の記事で書いた通り、DexのOIDC認証をサブパスで隠すためグローバルな単一エンドポイントが必須です。この設計は、Devtronから自身が内包するDexへのアクセスにこのグローバル単一エンドポイントを使うため、エンドポイントのIP制限にDevtron Podが起動しているEC2のIP許可という制約が伴います。NAT GW配下ならNAT GWで良いのですが、そうじゃないなら次々と入れ替わるノードのIPを許可しないといけません。Devtronを触るなら、まずはこのあたりの設計を考える必要があります。これ、OIDC認証としては通常考えなくていい制約なので、DevtronのOIDC設計はちょっと好みではないです。

ダッシュボード

Kubernetesダッシュボードとしては貧弱と言わざるを得ません。 ここから情報を読み取ろうにも読み取れる情報少なすぎるので、今のダッシュボードだと運用的には微妙です。 AIインテグレート用のコマンドパネルはあるので、そういうところはよさそうなのと、Grafana連携もできるのでダッシュボード埋め込みとか来ると良さそうです。

使うリソースが多い

Pod一覧を見てもわかりますが、Devtronは多くのPodを起動します。運用するということは、これらのPodが安定して動作するために必要なリソースを確保しなくてはいけません。 特に小規模なクラスターでは、Devtron自体がリソースを消費しすぎてノードのコストがバカにならないでしょう。 Kubernetes運用してていつも悩むんですが、運用系のコントローラーってPodが増えやすいので、もう少しPod数自体押さえたいんですよね。1コントローラー1Podだとすごいんですが、なんだかんだで冗長化で2Podが最低限必要ですし。

内包するOSSの最新事情のキャッチアップ

利用者もですが、Devtron開発自体が内包するOSSの最新事情をキャッチアップし続けるのは大変そうです。 例えばArgoCDやFlux、Trivy、Prometheus、Grafanaなど、これらのOSSは頻繁にアップデートされます。Devtronがこれらの最新バージョンをサポートし続けるためには、Devtron自体も頻繁にアップデートされる必要があります。Devtronのリリースサイクルと内包するOSSのリリースサイクルをうまく調整するのは難しい課題です。

各OSSの最新機能を活用したいと考えても、Devtron的にはそれをいい感じに解釈して提供する必要があるでしょう。この辺りはバランスをとるのか、あるいはOSSを直接使うのかっていう感じになります。 でも各種OSSを直接使いだすならDevtronを使う意味が薄れるんですよね。

AIインテグレーションが遠い

ぱっと使い始めようとしてもAIインテグレーションはどこ。とすぐにはわからないです。UXの問題にも感じますが、AIインテグレーションでどのような体験を提供したいのかというチュートリアルがないので難しいですね。 AIをぱっと使いたいっていうとき、まずログインしたらAI画面が開けばいいのにまである。

headlampやkubectl AIとか使っていると割と直感的なこともあり、今後どうしていくのか気になります。

個人的な期待

CI/CDの内、CIを切り離してほしい感じがします。CIはGitHub Actionsなり自身でやるシナリオはやはり主体的で、そこがセットな体験になっているのは相当厳しい。ECR含めて準備も多く、ちょっと試してみるまでがまず遠いです。 やりたいのは、AIドリブンなCDや運用であって、CIやCDセットアップじゃないんですよね。

個人的に、運用していてほしいのは、AI +ArgoCD + KEDA + Headlamp + Grafana + Fluentbit + Security Scanっていう感じがあります。DevtronはCI外せばそこに近い感じもするので今後も注目していきたいです。

まとめ

Devtron面白いのですが、まだまだ難しいなと感じました。Kubernetes運用の複雑さを解決するために多くの機能を提供していますが、その分設定や運用も複雑になりがちです。 有料版を含めて、どこまでサポートされるのかも気になるところです。

参考


  1. コンテナの知識、それを運用するオーケストレーターの知識、クラウドと連携する方法、DNSやクラウドリースやセキュリティ、監視などなど。
  2. 入れないでやっても入れない理由なかったので入れましょう
  3. Helmインストール時にコマンドが表示されます。

DevtronにみるDexのサブパス統合がOIDC連携に生む制約

Kubernetesにおいて、Dexを使ってOIDC連携しているアプリケーションは多くあります。 今回はアプリケーションのDex組み込み方法によってOIDC連携に制約が生まれることをDevtronを例に見てみましょう。

Dexとは

DexはオープンソースのOIDCプロバイダーであり、IdP(Identity Provider)と連携してOIDC認証を提供します。Kubernetesで動作するアプリケーションのOIDCプロバイダーとしてよく使われ、OIDC認証を用いたシングルサインオン(SSO)を提供しているアプリケーションのhelmチャートで見かけたことがある人も多いでしょう。

ただ近年はDex離れも見られ、当初Dexを使ってOIDC認証を提供していたものの、独自OIDC認証実装へ移行しているものもあります。例えばArgoCDは現在、Bundled Dexを使ったSSO認証と、外部OIDCプロバイダーへの直接連携の両方をサポートしています。個人的には、Dexを使っているアプリケーションのOIDC実装次第でOIDC連携を思ったようにできないケースがあり、Dexを使ったOIDC設定は避けたい気持ちがあります。

ということで、Devtronを例にDexを使っているアプリケーションのOIDC連携の制約を紹介します。

Dexの組み込み方法

アプリケーションにDexを組み込む方法は大きく分けて2つあります。

  1. 独立ホスト: Dexが独自のHost URL(例: dex.example.com)を持ち、アプリケーションとは別のエンドポイントとして公開
  2. リバースプロキシ方式(サブパス統合): アプリケーションが単一のHost URLを持ち、その配下に/api/dexのようなパスでDexを統合。同じURLが「外部からのブラウザアクセス」と「Pod内部からのDex API呼び出し」の両方に使われる

ほとんどのアプリケーションは1の方法を採用しており、利用者としてもスムーズなOIDC設定が可能です。しかし、Devtronは2の方法を採用しておりOIDC連携に制約を生んでいます。

DevtronとOIDC認証

Devtronは、OIDC認証を設定することで任意のIdPでSSOログインできます。DevtronのOIDC認証設定はDexであり、OIDCに関する設定要素が並んでいます。以下は、CognitoをIdPとして利用しつつ、DevtronをIngressで公開している場合の設定例です。

要素 説明 設定例
URL Dexの.well-known/openid-configurationエンドポイントURLのベースURL https://devtron.example.com/orchestrator
name Dexの任意の構成名 cognito
id Dexの任意の構成ID cognito
clientID OIDCクライアントID。IdPのクライアントIDを用いる ••••••••
clientSecret OIDCクライアントシークレット。IdPのクライアントシークレットを用いる ••••••••
issuer IdPのIssuer URL https://cognito-idp.REGION.amazonaws.com/REGION_USER-POOL-ID
redirectURI DevtronのOIDCコールバックURL。ブラウザアクセス時のURLホストに相当する https://devtron.example.com/orchestrator/api/dex/callback

Devtron上では、以下yamlのように設定します。

# URL: https://devtron.example.com/orchestrator
name: cognito
id: cognito
config:
 claimMapping:
  groups: cognito:groups
 clientID: ••••••••
 clientSecret: ••••••••
 issuer: https://cognito-idp.REGION.amazonaws.com/USER-POOL-ID
 redirectURI: https://devtron.example.com/orchestrator/api/dex/callback
 scopes:
  - openid
  - email
  - profile
 userNameKey: cognito:username

外部からOIDC認証を試みると、IdPのログイン画面が表示され、ログイン後にRedirectURIへリダイレクトされます。

先ほどのDevtronのOIDC認証設定には、DexのHost URLを指定する項目がありません。なぜなら、DevtronはDexを独自のHost URLで提供しておらず、ブラウザアクセスURLの一部として提供しているからです。代わりにURLの値に.well-known/openid-configurationパスを追加して、Devtron内部のOIDCクライアントがDexのOIDCエンドポイントにアクセスします。上記の例なら、URLに設定したhttp://localhost:8082/orchestratorをベースにしたhttps://devtron.example.com/orchestrator/api/dex/.well-known/openid-configurationを使ってDexエンドポイントにアクセスを試みるということです。

ちなみに、.well-known/openid-configurationエンドポイントはOIDCプロバイダーのメタデータを提供するための標準エンドポイントです。Devtronに組み込まれているDexもこのエンドポイントを提供しており、例えばhttps://devtron.example.com/orchestrator/api/dex/.well-known/openid-configurationエンドポイントにアクセスすると、次のようなOIDCプロバイダーJSONレスポンスが返ってきます。

{
  "issuer": "https://devtron.example.com/orchestrator/api/dex",
  "authorization_endpoint": "https://devtron.example.com/orchestrator/api/dex/auth",
  "token_endpoint": "https://devtron.example.com/orchestrator/api/dex/token",
  "jwks_uri": "https://devtron.example.com/orchestrator/api/dex/keys",
  "userinfo_endpoint": "https://devtron.example.com/orchestrator/api/dex/userinfo",
  "device_authorization_endpoint": "https://devtron.example.com/orchestrator/api/dex/device/code",
  "grant_types_supported": [
    "authorization_code",
    "refresh_token",
    "urn:ietf:params:oauth:grant-type:device_code"
  ],
  "response_types_supported": [
    "code"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "code_challenge_methods_supported": [
    "S256",
    "plain"
  ],
  "scopes_supported": [
    "openid",
    "email",
    "groups",
    "profile",
    "offline_access"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post"
  ],
  "claims_supported": [
    "iss",
    "sub",
    "aud",
    "iat",
    "exp",
    "email",
    "email_verified",
    "locale",
    "name",
    "preferred_username",
    "at_hash"
  ]
}

先のOIDC設定はDevtronのアプリエンドポイント(=Dexのエンドポイント) https://devtron.example.com はグローバルに公開されているので、Devtronをホストしているpod -> グローバルなdevtronエンドポイントという経路でDexの.well-known/openid-configurationエンドポイントにアクセスできます。

このことがDevtronをポートフォワードで接続しつつ、OIDC認証することに制約を生んでいます。

ローカルマシンからポートフォワードでDevtronにアクセスしつつOIDC認証を動かせるのか

結論から言うと、ポートフォワードだけで完結させたままDevtronのOIDC認証は動作しません。これは、devtronは単一URLという前提で設計されており、その単一URLを使って2つの役割を果たしているからです。

  • ブラウザからアクセス元として信頼する(redirectURIの検証)
  • Dexをたたく時のベースURLにも使う(/api/dex/.well-known/openid-configurationエンドポイントの構築)

ポートフォワードとしてlocalhost:8082でDevtronへアクセスする場合と、クラスター内部のサービス名でDevtronにアクセスする場合の2つのケースを考えてみましょう。先に、設定例と結果をまとめた表を示します。

設定項目 localhost:8082の例 cluster.localの例
URL (ベースURL) http://localhost:8082/orchestrator http://devtron-service.devtroncd.svc.cluster.local/orchestrator
redirectURI http://localhost:8082/orchestrator/api/dex/callback http://localhost:8082/orchestrator/api/dex/callback
ブラウザアクセス ✅ 成功 ✅ 成功
Pod内部アクセス ❌ 失敗(Pod内localhostに到達不可) ✅ 成功
RedirectURIの検証 ✅ 成功 ❌ 失敗(URLとRedirectURIでホスト&ポートが不一致)
結果 connection refused Invalid redirect URL

localhost:8082の例

よくあるポートフォワードでDevtronにアクセスする場合どうなるか考えてみましょう。今回はブラウザからはhttp://localhost:8082のようなURLでポートフォワードを介してアクセスします。

設定は以下のようになります。

要素 説明 設定例
URL Dexの.well-known/openid-configurationエンドポイントURLのベースURL http://localhost:8082/orchestrator
name Dexの任意の構成名 cognito
id Dexの任意の構成ID cognito
clientID OIDCクライアントID。IdPのクライアントIDを用いる ••••••••
clientSecret OIDCクライアントシークレット。IdPのクライアントシークレットを用いる ••••••••
issuer IdPのIssuer URL https://cognito-idp.REGION.amazonaws.com/REGION_USER-POOL-ID
redirectURI DevtronのOIDCコールバックURL。ブラウザアクセス時のURLホストに相当する http://localhost:8082/orchestrator/api/dex/callback

Devtron上では、以下yamlのように設定します。

# URL: http://localhost:8082/orchestrator
name: cognito
id: cognito
config:
 claimMapping:
  groups: cognito:groups
 clientID: ••••••••
 clientSecret: ••••••••
 issuer: https://cognito-idp.REGION.amazonaws.com/USER-POOL-ID
 redirectURI: http://localhost:8082/orchestrator/api/dex/callback
 scopes:
  - openid
  - email
  - profile
 userNameKey: cognito:username

この設定でOIDC認証を試みると次のようなエラーが発生します。

Failed to query provider "http://localhost:8082/orchestrator/api/dex": Get "http://localhost:8082/orchestrator/api/dex/.well-known/openid-configuration": dial tcp [::1]:8082: connect: connection refused

エラーは、DevtronのpodからdexのOIDCエンドポイントhttp://localhost:8082/orchestrator/api/dex/.well-known/openid-configurationへアクセスするときに発生しています。なぜなら、localhost:8082はブラウザから見たDevtronのエンドポイントですが、Devtronのpodから見たlocalhost:8082はDevtronのpod自身を指しており、Devtronのpod内で動作しているDexエンドポイントにはアクセスできないからです。 具体的には、Devtronの初期時にUserAuthOidcHelper.go/NewUserAuthOidcHelperImplにてDexConfigのURL設定を使ってclient.GetOidcClient()を呼び出し、その中で{URL}/.well-known/openid-configurationにアクセスしてOIDCプロバイダー情報を取得しようとします。URLに入るlocalhost:8082はポートフォワードで作られたローカルマシン上のトンネルであり、Kubernetes Pod内から見ると存在しないアドレスなので接続が拒否される、それはそう。

この場合、次が検証されています。

  • OK: ブラウザ側のURLと一致するので、RedirectURIの検証は通る
  • NG: Devtronのpodからhttp://localhost:8082/orchestrator/api/dex/.well-known/openid-configurationにアクセスできないので、OIDCプロバイダー情報の取得に失敗する

devtron-service.devtroncd.svc.cluster.localの例

先のlocalhost:8082でアクセスできなかった原因は、Devtronのpodから見たlocalhost:8082がDevtronのpod自身を指してしまうことでした。では、Devtronのpodから見たDevtronのサービス名でアクセスできるように設定すればどうでしょうか?Devtronのサービス名エンドポイントdevtron-service.devtroncd.svc.cluster.localであればクラスター内部のサービスアドレスなので、Devtronのpodからもアクセスできるはずです。

設定は以下のようになります。なお、リダイレクトURIはブラウザからアクセスするURLホストに相当するので、localhost:8082のままとなります。

要素 説明 設定例
URL Dexの.well-known/openid-configurationエンドポイントURLのベースURL http://devtron-service.devtroncd.svc.cluster.local/orchestrator
name Dexの任意の構成名 cognito
id Dexの任意の構成ID cognito
clientID OIDCクライアントID。IdPのクライアントIDを用いる ••••••••
clientSecret OIDCクライアントシークレット。IdPのクライアントシークレットを用いる ••••••••
issuer IdPのIssuer URL https://cognito-idp.REGION.amazonaws.com/REGION_USER-POOL-ID
redirectURI DevtronのOIDCコールバックURL。ブラウザアクセス時のURLホストに相当する http://localhost:8082/orchestrator/api/dex/callback

Devtron上では、以下yamlのように設定します。

# URL: http://devtron-service.devtroncd.svc.cluster.local/orchestrator
name: cognito
id: cognito
config:
 claimMapping:
  groups: cognito:groups
 clientID: ••••••••
 clientSecret: ••••••••
 issuer: https://cognito-idp.REGION.amazonaws.com/USER-POOL-ID
 redirectURI: http://localhost:8082/orchestrator/api/dex/callback
 scopes:
  - openid
  - email
  - profile
 userNameKey: cognito:username

この設定でOIDC認証を試みると次のようなエラーが発生します。

Invalid redirect URL: the protocol and host (including port) must match and the path must be within allowed URLs if provided

エラーは、リダイレクトURIの検証時に発生しています。クラスター内部のSVC URLを用いることでDevtronのPodからDexエンドポイントに疎通できるのですが、リダイレクトURIhttp://localhost:8082とDex URLhttp://devtron-service.devtroncd.svc.cluster.local/orchestratorのhost+portが違うので検証エラーになっています。

この場合、次が検証されています。

  • NG: リダイレクトURLとBaseURLのhost+portが一致しないので、RedirectURIの検証に失敗する
  • OK: Devtronのpodからhttp://devtron-service.devtroncd.svc.cluster.local/orchestrator/api/dex/.well-known/openid-configurationはアクセスできる

なぜDevtronはポートフォワードだけでOIDC認証が動かせないのか

DevtronのOIDC認証はDexを使って提供されていますが、「DevtronのHost URLは単一」で「そのURLを経路の異なるアクセスの両方に使っている」という設計です。経路の異なるアクセスとは、DevtronのPodからDexの.well-known/openid-configurationエンドポイントにアクセスする経路と、ブラウザからDevtronのOIDCコールバックURLにアクセスする経路を指します。Devtronは内部にDexを持ちつつ、この2つの経路で同じURLが到達可能であることを前提としているため、ポートフォワードだけでOIDC認証きません。

  • クライアント側(ブラウザ): OIDC認証フローのリダイレクトURL生成とコールバック検証
  • サーバー側(Pod内部): Dexサービスへの直接接続と.well-known/openid-configurationの取得

まとめ

DevtronのOIDC認証を使う場合は、IngressやLoadBalancerを使って内外両方からアクセスを担保できる統一されたエンドポイントを用意しましょう。ただし、このエンドポイントはDevtronのPodが起動しているNodeからもアクセス可能である必要があるため、IP制限でNodeを考慮する必要がありこれはこれで厄介です。NAT GWを使っているならばNAT GWのIPを許可するなどの対応でよいのですが、そうでない場合はDevtronのPodが起動しているNodeのIPを特定して許可します。

Dexが悪いわけじゃないのですが、Dexをアプリケーションの裏に組み込む時、単一URLでのアクセスを前提とするのはOIDC連携に制約を生むことがあるので注意しましょう。

参考

(続) C#でCPUモデルを取得する

以前の記事でC#からCPUモデルを取得する方法を紹介しましたが、ARM64環境に対する手当が不十分でした。今回はARM64環境も含めてC#でCPUモデルを取得する方法を紹介します。

アーキテクチャとOSごとのCPUモデル取得方法

やり方を検討するにあたり、アーキテクチャごとに各OSでどうやったらCPUモデルを取得できるかをまとめます。

アーキテクチャ OS 取得方法
x86_64 共通 CPUID命令を使う
x86_64 Linux /proc/cpuinfoを読む、lscpuを使う
x86_64 Windows WMI (Win32_Processor)を使う、レジストリを読む
x86_64 macOS sysctlを使う
ARM64 共通 なし
ARM64 Linux lscpuを使う、/proc/cpuinfoを読んでマッピング参照
ARM64 Windows WMI (Win32_Processor)を使う、レジストリを読む
ARM64 macOS sysctlを使う

x86_64環境ではCPUID命令を使ってCPUモデルを直接取得できるのに対し、ARM64環境ではCPUID命令が存在しないため、OS依存の方法で取得します。具体的にはOSが提供するAPIやコマンドを利用してCPUモデルを取得しましょう。 特にLinux環境では、x86_64で/proc/cpuinfoにCPUモデル情報が含まれていたのがARM64では含まれていないため、lscpuコマンドを使うか/proc/cpuinfoを読んでマッピングテーブルを参照しましょう。

C#での実装例

今回は、x86_64とARM64環境の両方をサポートしています。

Linux ARM64環境でCPUモデルを取得するために/proc/cpuinfoを読んでARMのImplementer(CPUベンダー)とパーツ番号からモデル名をマッピングする方法を採用しました。 マッピング表は、util-linux/util-linux | GitHubfastfetch-cli/fastfetch | GitHubを参考にしています。が、この実装ってCPUベンダーが新しいCPUを出すたびに更新しないといけないので、なかなか大変です。とはいえ、この記事でも述べられているように、lscpuも内部実装はマッピングなわけで、ARM64環境でCPUモデルを取得するにはこの方法が2025年時点では最も確実です。1

Windows、macOSはアーキテクチャに関係なく同じ方法で取得できるため、アーキテクチャ判定はx86_64でCPUID命令を使えたら使うだけの分岐です。

Windowsはレジストリを直接読んでいます。WMIを使う方法もありますが、WMIは起動が遅いので、レジストリを直接読む方法の方が高速です。

macOSはsysctlを使っています。ただし、前回はlibcとしていましたが、libSystem.dylibの方が正しくこちらで動作を確認しました。

using System.Runtime.InteropServices;

namespace SystemInfo.Core;

public class CpuModel
{
    const string UnknownPhrase = "Unknown";

    public static CpuModel Current { get; } = new CpuModel();
    public string ModelName { get; } = "";
    public string UnknownReason { get; } = "";

    private CpuModel()
    {
        if (System.Runtime.Intrinsics.X86.X86Base.IsSupported)
        {
            // x86_64 OS (Linux, Windows, macOS) ...
            (ModelName, UnknownReason) = GetX86CpuModelName();
        }
        else
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                // Windows Arm64 will be here...
                (ModelName, UnknownReason) = GetWindowsModelName();
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                // Linux Arm64 will be here...
                (ModelName, UnknownReason) = GetLinuxModelName();
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                // macOS will be here...
                (ModelName, UnknownReason) = GetOSXModelname();
            }
            else
            {
                // Unsupported platform (not Windows, Linux, or macOS)
                (ModelName, UnknownReason) = (UnknownPhrase, $"Platform not supported for... OS: {RuntimeInformation.OSDescription}, Architecture: {RuntimeInformation.OSArchitecture}");
            }
        }
    }

    private static (string modelName, string unknownReason) GetX86CpuModelName()
    {
        Span<int> regs = stackalloc int[12]; // call 3 times (0x80000002, 0x80000003, 0x80000004) for 4 registers

        // Calling __cpuid with 0x80000000 as the InfoType argument and gets the number of valid extended IDs.
        var extendedId = System.Runtime.Intrinsics.X86.X86Base.CpuId(unchecked((int)0x80000000), 0).Eax;

        // Get the information associated with each extended ID.
        if ((uint)extendedId >= 0x80000004)
        {
            int p = 0;
            for (uint i = 0x80000002; i <= 0x80000004; ++i)
            {
                var (Eax, Ebx, Ecx, Edx) = System.Runtime.Intrinsics.X86.X86Base.CpuId((int)i, 0);
                regs[p + 0] = Eax;
                regs[p + 1] = Ebx;
                regs[p + 2] = Ecx;
                regs[p + 3] = Edx;
                p += 4; // advance
            }
            return (ConvertToString(regs), "");
        }

        return (UnknownPhrase, $"CPU Architecture not supported... extendedId: {extendedId}");

        static string ConvertToString(ReadOnlySpan<int> regs)
        {
            Span<byte> bytes = stackalloc byte[regs.Length * 4]; // int 4byte * 12
            for (int i = 0; i < regs.Length; i++)
            {
                BitConverter.TryWriteBytes(bytes.Slice(i * 4, 4), regs[i]);
            }
            return System.Text.Encoding.ASCII.GetString(bytes).Trim();
        }
    }

    private static (string modelName, string unknownReason) GetWindowsModelName()
    {
        const string registryKey = @"HARDWARE\DESCRIPTION\System\CentralProcessor\0";
        const string valueName = "ProcessorNameString";

        if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            throw new PlatformNotSupportedException("Not Windows OS.");

        using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(registryKey);
        if (key != null)
        {
            var value = key.GetValue(valueName);
            if (value is string model)
            {
                return (model, "");
            }
        }
        return (UnknownPhrase, "Windows Registry Key not found.");
    }

    private static (string modelName, string unknownReason) GetLinuxModelName()
    {
        try
        {
            var cpuInfo = File.ReadAllText("/proc/cpuinfo");
            var lines = cpuInfo.Split('\n');
            string vendorId = "";
            string cpuPart = "";
            string modelName = "";

            foreach (var line in lines)
            {
                // x86/amd64 (sometimes arm64 contain this)
                if (line.StartsWith("model name"))
                {
                    var parts = line.Split(':');
                    if (parts.Length > 1)
                    {
                        modelName = parts[1].Trim();
                        return (modelName, "");
                    }
                }
                // ARM64
                else if (line.StartsWith("CPU implementer"))
                {
                    var parts = line.Split(":");
                    if (parts.Length > 1)
                    {
                        vendorId = parts[1].Trim().ToLower();
                    }
                }
                else if (line.StartsWith("CPU part"))
                {
                    var parts = line.Split(":");
                    if (parts.Length > 1)
                    {
                        cpuPart = parts[1].Trim().ToLower();
                    }
                }
            }

            // ARM64 model name resolution (0x41 is ARM Ltd.)
            if (modelName == "" && vendorId != "")
            {
                if (cpuPart == "")
                    return (UnknownPhrase, "CPU part not found for ARM CPU.");

                var armModelName = GetArmCpuName(vendorId, cpuPart);
                return armModelName != "Undefined"
                    ? (armModelName, "")
                    : (armModelName, $"CPU part '{cpuPart}' (vendor '{vendorId}' ({GetArmImplementerName(vendorId)})) is not mapped.");
            }

            return (UnknownPhrase, "'model name' section not found.");
        }
        catch (Exception ex)
        {
            return (UnknownPhrase, $"Exception occurred: {ex.Message}");
        }
    }

    private static (string modelName, string unknownReason) GetOSXModelname()
    {
        try
        {
            nint size = 0;

            // First call to get the size
            int result = sysctlbyname("machdep.cpu.brand_string", IntPtr.Zero, ref size, IntPtr.Zero, 0);
            if (result != 0)
            {
                return (UnknownPhrase, $"sysctlbyname failed to get size. Return code: {result}, errno: {Marshal.GetLastPInvokeError()}");
            }

            if (size == 0)
            {
                return (UnknownPhrase, "sysctlbyname returned size 0");
            }

            IntPtr buffer = Marshal.AllocHGlobal((int)size);
            try
            {
                // Second call to get the actual value
                result = sysctlbyname("machdep.cpu.brand_string", buffer, ref size, IntPtr.Zero, 0);
                if (result != 0)
                {
                    return (UnknownPhrase, $"sysctlbyname failed to get value. Return code: {result}, errno: {Marshal.GetLastPInvokeError()}");
                }

                var cpuBrand = Marshal.PtrToStringAnsi(buffer);
                if (string.IsNullOrEmpty(cpuBrand))
                {
                    return (UnknownPhrase, "machdep.cpu.brand_string returned empty string.");
                }

                return (cpuBrand.Trim(), "");
            }
            finally
            {
                Marshal.FreeHGlobal(buffer);
            }
        }
        catch (Exception ex)
        {
            return (UnknownPhrase, $"Exception occurred: {ex.Message}");
        }
    }

    [DllImport("libSystem.dylib")]
    private static extern int sysctlbyname(string name, IntPtr oldp, ref nint oldlenp, IntPtr newp, nint newlen);

    // ARM CPU detection helpers
    // see: https://github.com/util-linux/util-linux/blob/v2.41.2/sys-utils/lscpu-arm.c
    // also https://github.com/fastfetch-cli/fastfetch/blob/2.55.1/src/detection/cpu/cpu_arm.h
    private enum ArmImplementers
    {
        Ampere,
        ARM,
        APM,
        Apple,
        Broadcom,
        Cavium,
        DEC,
        Faraday,
        Fujitsu,
        HiSilicon,
        Infineon,
        Intel,
        Marvell,
        Microsoft,
        Motorola,
        NVIDIA,
        Phytium,
        Qualcomm,
        Samsung,
        Unknown,
    }

    // Mapping of ARM CPU implementer IDs
    private static ArmImplementers GetArmImplementerName(string vendorId)
    {
        return vendorId switch
        {
            "0x41" => ArmImplementers.ARM,
            "0x42" => ArmImplementers.Broadcom,
            "0x43" => ArmImplementers.Cavium,
            "0x44" => ArmImplementers.DEC,
            "0x46" => ArmImplementers.Fujitsu,
            "0x48" => ArmImplementers.HiSilicon,
            "0x49" => ArmImplementers.Infineon,
            "0x4d" => ArmImplementers.Motorola,
            "0x6d" => ArmImplementers.Microsoft,
            "0x4e" => ArmImplementers.NVIDIA,
            "0x50" => ArmImplementers.APM,
            "0x51" => ArmImplementers.Qualcomm,
            "0x53" => ArmImplementers.Samsung,
            "0x56" => ArmImplementers.Marvell,
            "0x61" => ArmImplementers.Apple,
            "0x66" => ArmImplementers.Faraday,
            "0x69" => ArmImplementers.Intel,
            "0x70" => ArmImplementers.Phytium,
            "0xc0" => ArmImplementers.Ampere,
            _ => ArmImplementers.Unknown,
        };
    }

    // Mapping of ARM CPU part numbers to model names
    private static string GetArmCpuName(string vendorId, string cpuPart)
    {
        var armImpl = GetArmImplementerName(vendorId);
        return armImpl switch
        {
            ArmImplementers.ARM => cpuPart switch
            {
                "0x810" => "ARM810",
                "0x920" => "ARM920",
                "0x922" => "ARM922",
                "0x926" => "ARM926",
                "0x940" => "ARM940",
                "0x946" => "ARM946",
                "0x966" => "ARM966",
                "0xa20" => "ARM1020",
                "0xa22" => "ARM1022",
                "0xa26" => "ARM1026",
                "0xb02" => "ARM11 MPCore",
                "0xb36" => "ARM1136",
                "0xb56" => "ARM1156",
                "0xb76" => "ARM1176",
                "0xc05" => "Cortex-A5",
                "0xc07" => "Cortex-A7",
                "0xc08" => "Cortex-A8",
                "0xc09" => "Cortex-A9",
                "0xc0d" => "Cortex-A17", // Originally A12
                "0xc0f" => "Cortex-A15",
                "0xc0e" => "Cortex-A17",
                "0xc14" => "Cortex-R4",
                "0xc15" => "Cortex-R5",
                "0xc17" => "Cortex-R7",
                "0xc18" => "Cortex-R8",
                "0xc20" => "Cortex-M0",
                "0xc21" => "Cortex-M1",
                "0xc23" => "Cortex-M3",
                "0xc24" => "Cortex-M4",
                "0xc27" => "Cortex-M7",
                "0xc60" => "Cortex-M0+",
                "0xd00" => "Foundation",
                "0xd01" => "Cortex-A32",
                "0xd02" => "Cortex-A34",
                "0xd03" => "Cortex-A53",
                "0xd04" => "Cortex-A35",
                "0xd05" => "Cortex-A55",
                "0xd06" => "Cortex-A65",
                "0xd07" => "Cortex-A57",
                "0xd08" => "Cortex-A72",
                "0xd09" => "Cortex-A73",
                "0xd0a" => "Cortex-A75",
                "0xd0b" => "Cortex-A76",
                "0xd0c" => "Neoverse-N1",
                "0xd0d" => "Cortex-A77",
                "0xd0e" => "Cortex-A76AE",
                "0xd0f" => "AEMv8",
                "0xd13" => "Cortex-R52",
                "0xd20" => "Cortex-M23",
                "0xd21" => "Cortex-M33",
                "0xd40" => "Neoverse-V1",
                "0xd41" => "Cortex-A78",
                "0xd42" => "Cortex-A78AE",
                "0xd43" => "Cortex-A65AE",
                "0xd44" => "Cortex-X1",
                "0xd46" => "Cortex-A510",
                "0xd47" => "Cortex-A710",
                "0xd48" => "Cortex-X2",
                "0xd49" => "Neoverse-N2",
                "0xd4a" => "Neoverse-E1",
                "0xd4b" => "Cortex-A78C",
                "0xd4c" => "Cortex-X1C",
                "0xd4d" => "Cortex-A715",
                "0xd4e" => "Cortex-X3",
                "0xd4f" => "Neoverse-V2",
                "0xd80" => "Cortex-A520",
                "0xd81" => "Cortex-A720",
                "0xd82" => "Cortex-X4",
                "0xd84" => "Neoverse-V3",
                "0xd85" => "Cortex-X925",
                "0xd87" => "Cortex-A725",
                "0xd88" => "Cortex-A520AE",
                "0xd89" => "Cortex-A720AE",
                "0xd8a" => "C1-Nano",
                "0xd8b" => "C1-Pro",
                "0xd8c" => "C1-Ultra",
                "0xd8e" => "Neoverse-N3",
                "0xd8f" => "Cortex-A320",
                "0xd90" => "C1-Premium",
                _ => "Undefined",
            },
            ArmImplementers.Ampere => cpuPart switch
            {
                "0xac3" => "Ampere-1",
                "0xac4" => "Ampere-1a",
                _ => "Undefined",
            },
            ArmImplementers.APM => cpuPart switch
            {
                "0x000" => "X-Gene",
                _ => "Undefined",
            },
            ArmImplementers.Apple => cpuPart switch
            {
                "0x000" => "Swift",
                "0x001" => "Cyclone",
                "0x002" => "Typhoon",
                "0x003" => "Typhoon/Capri",
                "0x004" => "Twister",
                "0x005" => "Twister/Elba/Malta",
                "0x006" => "Hurricane",
                "0x007" => "Hurricane/Myst",
                "0x008" => "Monsoon",
                "0x009" => "Mistral",
                "0x00b" => "Vortex",
                "0x00c" => "Tempest",
                "0x00f" => "Tempest-M9",
                "0x010" => "Vortex/Aruba",
                "0x011" => "Tempest/Aruba",
                "0x012" => "Lightning",
                "0x013" => "Thunder",
                "0x020" => "Icestorm-A14",
                "0x021" => "Firestorm-A14",
                "0x022" => "Icestorm-M1",
                "0x023" => "Firestorm-M1",
                "0x024" => "Icestorm-M1-Pro",
                "0x025" => "Firestorm-M1-Pro",
                "0x026" => "Thunder-M10",
                "0x028" => "Icestorm-M1-Max",
                "0x029" => "Firestorm-M1-Max",
                "0x030" => "Blizzard-A15",
                "0x031" => "Avalanche-A15",
                "0x032" => "Blizzard-M2",
                "0x033" => "Avalanche-M2",
                "0x034" => "Blizzard-M2-Pro",
                "0x035" => "Avalanche-M2-Pro",
                "0x036" => "Sawtooth-A16",
                "0x037" => "Everest-A16",
                "0x038" => "Blizzard-M2-Max",
                "0x039" => "Avalanche-M2-Max",
                "0x046" => "Sawtooth-M11",
                "0x048" => "Sawtooth-M3-Max",
                "0x049" => "Everest-M3-Max",
                _ => "Undefined",
            },
            ArmImplementers.Broadcom => cpuPart switch
            {
                "0x0f" => "Brahma-B15",
                "0x100" => "Brahma-B53",
                "0x516" => "ThunderX2",
                _ => "Undefined",
            },
            ArmImplementers.Cavium => cpuPart switch
            {
                "0x0a0" => "ThunderX",
                "0x0a1" => "ThunderX-88XX",
                "0x0a2" => "ThunderX-81XX",
                "0x0a3" => "ThunderX-83XX",
                "0x0af" => "ThunderX2-99xx",
                "0x0b0" => "OcteonTX2",
                "0x0b1" => "OcteonTX2-98XX",
                "0x0b2" => "OcteonTX2-96XX",
                "0x0b3" => "OcteonTX2-95XX",
                "0x0b4" => "OcteonTX2-95XXN",
                "0x0b5" => "OcteonTX2-95XXMM",
                "0x0b6" => "OcteonTX2-95XXO",
                "0x0b8" => "ThunderX3-T110",
                _ => "Undefined",
            },
            ArmImplementers.DEC => cpuPart switch
            {
                "0x001" => "SA110",
                "0x002" => "SA1100",
                _ => "Undefined",
            },
            ArmImplementers.Faraday => cpuPart switch
            {
                "0x526" => "FA526",
                "0x626" => "FA626",
                _ => "Undefined",
            },
            ArmImplementers.Fujitsu => cpuPart switch
            {
                "0x001" => "A64FX",
                "0x003" => "MONAKA",
                _ => "Undefined",
            },
            ArmImplementers.HiSilicon => cpuPart switch
            {
                "0xd01" => "TaiShan-v110", // used in Kunpeng-920 SoC
                "0xd02" => "TaiShan-v120", // used in Kirin 990A and 9000S SoCs
                "0xd40" => "Cortex-A76", // HiSilicon uses this ID though advertises A76
                "0xd41" => "Cortex-A77", // HiSilicon uses this ID though advertises A77
                _ => "Undefined",
            },
            ArmImplementers.Intel => cpuPart switch
            {
                "0x200" => "i80200",
                "0x210" => "PXA250A",
                "0x212" => "PXA210A",
                "0x242" => "i80321-400",
                "0x243" => "i80321-600",
                "0x290" => "PXA250B/PXA26x",
                "0x292" => "PXA210B",
                "0x2c2" => "i80321-400-B0",
                "0x2c3" => "i80321-600-B0",
                "0x2d0" => "PXA250C/PXA255/PXA26x",
                "0x2d2" => "PXA210C",
                "0x411" => "PXA27x",
                "0x41c" => "IPX425-533",
                "0x41d" => "IPX425-400",
                "0x41f" => "IPX425-266",
                "0x682" => "PXA32x",
                "0x683" => "PXA930/PXA935",
                "0x688" => "PXA30x",
                "0x689" => "PXA31x",
                "0xb11" => "SA1110",
                "0xc12" => "IPX1200",
                _ => "Undefined",
            },
            ArmImplementers.Marvell => cpuPart switch
            {
                "0x131" => "Feroceon-88FR131",
                "0x581" => "PJ4/PJ4b",
                "0x584" => "PJ4B-MP",
                _ => "Undefined",
            },
            ArmImplementers.Microsoft => cpuPart switch
            {
                "0xd49" => "Azure-Cobalt-100",
                _ => "Undefined",
            },
            ArmImplementers.NVIDIA => cpuPart switch
            {
                "0x000" => "Denver",
                "0x003" => "Denver 2",
                "0x004" => "Carmel",
                "0x010" => "Olympus",
                _ => "Undefined",
            },
            ArmImplementers.Phytium => cpuPart switch
            {
                "0x303" => "FTC310",
                "0x660" => "FTC660",
                "0x661" => "FTC661",
                "0x662" => "FTC662",
                "0x663" => "FTC663",
                "0x664" => "FTC664",
                "0x862" => "FTC862",
                _ => "Undefined",
            },
            ArmImplementers.Qualcomm => cpuPart switch
            {
                "0x001" => "Oryon 1",
                "0x002" => "Oryon 2",
                "0x00f" => "Scorpion",
                "0x02d" => "Scorpion",
                "0x04d" => "Krait",
                "0x06f" => "Krait",
                "0x201" => "Kryo",
                "0x205" => "Kryo",
                "0x211" => "Kryo",
                "0x800" => "Falkor-V1/Kryo",
                "0x801" => "Kryo-V2",
                "0x802" => "Kryo-3XX-Gold",
                "0x803" => "Kryo-3XX-Silver",
                "0x804" => "Kryo-4XX-Gold",
                "0x805" => "Kryo-4XX-Silver",
                "0xc00" => "Falkor",
                "0xc01" => "Saphira",
                _ => "Undefined",
            },
            ArmImplementers.Samsung => cpuPart switch
            {
                "0x001" => "Exynos-M1",
                "0x002" => "Exynos-M3",
                "0x003" => "Exynos-M4",
                "0x004" => "Exynos-M5",
                _ => "Undefined",
            },
            ArmImplementers.Infineon => "Undefined", // no definitions found
            ArmImplementers.Motorola => "Undefined", // no definitions found
            ArmImplementers.Unknown => "Undefined",
            _ => "Undefined",
        };
    }
}

動作確認

こういったOSとアーキテクチャを組み合わせた動作確認は手元に環境がなくて諦めがちです。安心してください。幸いにしてGitHub Actionsはx86_64およびARM64のLinux/Windows/macOS環境を提供しているので、これを使って動作確認ができます。誰でも再現可能ですし、簡単に確認できて最高ですね。

  run:
    permissions:
      contents: read
    strategy:
      fail-fast: false
      matrix:
        runs-on:
          - ubuntu-24.04 # x64
          - ubuntu-24.04-arm # arm64
          - windows-2025 # x64
          - windows-11-arm # arm64
          - macos-15-intel # x64
          - macos-26 # arm64
    runs-on: ${{ matrix.runs-on }}
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          persist-credentials: false
      - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
        with:
          dotnet-version: "10.0.x"
      - name: Get CPU Model
        run: dotnet Program.cs

適当なProgram.csを用意して実行します。.NET 10ならProgram.csファイル1つあればdotnet Program.csで実行可能です。

using System.Runtime.InteropServices;

StartupInfo.Print();

public static class StartupInfo
{
    private static readonly string cpuModel = !string.IsNullOrWhiteSpace(CpuModel.Current.ModelName) ? CpuModel.Current.ModelName : CpuModel.Current.UnknownReason;

    /// <summary>
    /// Get and print startup information about the operating system and CPU.
    /// </summary>
    public static void Print()
    {
        Console.WriteLine($$"""
        Startup info:
          * OS       : {{RuntimeInformation.OSDescription}}
          * CPU Arch : {{RuntimeInformation.ProcessArchitecture}}
          * CPU Model: {{cpuModel}}
          * CPU Cores: {{Environment.ProcessorCount}}
        """);
    }
}

// 以下CPUModelクラスのコードは省略

出力結果です。全部の環境でCPUモデルが正しく取得できていることがわかります。GitHub ActionsのホストランナーはAzure VMなのですが、Ubuntu ARM64ではNeoverse-N2、Windows ARM64ではCobalt 100が使われているのが興味深いですね。Azure Cobaltドキュメントにあるように、Azure Cobalt 100はNeoverse-N2ベースのカスタムSoCなので、整合性が取れています。

# Linux x86_64
Startup info:
  * OS       : Ubuntu 24.04.3 LTS
  * CPU Arch : X64
  * CPU Model: AMD EPYC 7763 64-Core Processor
  * CPU Cores: 4

# Linux Arm64
Startup info:
  * OS       : Ubuntu 24.04.3 LTS
  * CPU Arch : Arm64
  * CPU Model: Neoverse-N2
  * CPU Cores: 4

# Windows x86_64
Startup info:
  * OS       : Microsoft Windows 10.0.26100
  * CPU Arch : X64
  * CPU Model: AMD EPYC 7763 64-Core Processor
  * CPU Cores: 4

# Windows Arm64
Startup info:
  * OS       : Microsoft Windows 10.0.26200
  * CPU Arch : Arm64
  * CPU Model: Cobalt 100
  * CPU Cores: 4

# macOS x86_64
Startup info:
  * OS       : macOS 15.7.1
  * CPU Arch : X64
  * CPU Model: Intel(R) Core(TM) i7-8700B CPU @ 3.20GHz
  * CPU Cores: 4

# macOS Arm64
Startup info:
  * OS       : macOS 26.0.1
  * CPU Arch : Arm64
  * CPU Model: Apple M1 (Virtual)
  * CPU Cores: 3

まとめ

前回はx86_64環境のみの対応でしたが、今回はARM64環境も含めてC#でCPUモデルを取得する方法を紹介しました。今回はマッピングを書きましたが、マッピングテーブルをメンテし続ける必要があるのは面倒なのでlscpuコマンドを実行して省力もよいでしょう。実行頻度やパフォーマンス要件に応じて使い分けてください。

参考


  1. ARM64 CPUがCPUIDでモデル名を直接取得できるようになると良いのですが、その気配ははなさそうです。