tech.guitarrapc.cóm

Technical updates

PulumiでIgnoreChangesを使う際の注意点

Terraformはリソースの特定のプロパティの変更を無視するのにignore_changes: [array]を用いますが、これと同様のことがPulumiのIgnoreChangesオプションで可能です。 PulumiのIgnoreChangesはドキュメントで解説されているのですが、Pulumiで入れ子のキーをIgnoreChangesで無視する方法について記載がないようなのでメモしておきます。

単純なIgnoreChangesの例

例えばリソースにPropsというプロパティがある場合、そのプロパティのスタック内部での表現propをIgnoreChangesで無視すればよいです。単純ですね。

new MyResource("res", new MyResourceArgs {
  Prop = "new-value"
}, new CustomResourceOptions { IgnoreChanges = { "prop" } });

入れ子のキーをIgnoreChangesで無視する方法

例えば以下のようなコードがあったとします。TagsプロパティはTagsというプロパティがあり、その中にEnvironmentやOwnerというキーが入れ子になっています。

new Aws.S3.Bucket("my-bucket", new Aws.S3.BucketArgs
{
    Tags = new InputMap<string>
    {
        { "Environment", "Dev" },
        { "Owner", "Alice" },
    }
});

このとき、Tagsのうち"Owner"キーだけをIgnoreChangesで無視したい場合、以下のように入れ子の親プロパティ[\"入れ子の子プロパティキー\"]と記述します。

new Aws.S3.Bucket("my-bucket", new Aws.S3.BucketArgs
{
    Tags = new InputMap<string>
    {
        { "Environment", "Dev" },
        { "Owner", "Alice" },
    }
}, new CustomResourceOptions
{
    IgnoreChanges = { "tags[\"Owner\"]" }
});

これにより、Tagsの"Owner"キーの変更はPulumiの差分検出から無視されるようになります。

入れ子のキーがなかったのに後からクラウド側が追加してきてそれを無視したい場合

注意点として、もしリソース作成時に入れ子のキーが存在しなかった場合、PulumiはそのキーをIgnoreChangesで無視できません。 例えば、上記の例で"verified"キーが最初は存在せず、後からクラウド側が追加してきた場合、そのキーをIgnoreChangesで指定しても効果がありません。

new Aws.S3.Bucket("my-bucket", new Aws.S3.BucketArgs
{
    Tags = new InputMap<string>
    {
        { "Environment", "Dev" },
        { "Owner", "Alice" },
        // クラウドが後から"verified"キーを追加してきた
    }
}, new CustomResourceOptions
{
    IgnoreChanges = { "tags[\"verified\"]" } // 意味がない
});

この場合、Terraformなら存在しなかったキーを定義上で追加すればIgnoreChangesで無視できますが、Pulumiではできません。 リソースにもよりますが、最初から作っておけば無視されるようです。

new Aws.S3.Bucket("my-bucket", new Aws.S3.BucketArgs
{
    Tags = new InputMap<string>
    {
        { "Environment", "Dev" },
        { "Owner", "Alice" },
        { "verified", "true" }, // 存在しなかったキーを定義上で追加
    }
}, new CustomResourceOptions
{
    IgnoreChanges = { "tags[\"verified\"]" } // 無視される
});

ということで、そういう時は2つ手があります。

  • 一度消してもいいリソースなら、ignoreChangesを設定してからリソースを再作成する
  • Pulumi stackをexportしてから、当該リソースに手動でverifiedキーを追加してからimportし直す力技

リソースの再作成は検証環境とかならいいですが、そうじゃないと無理ゲー感がすごいですね。 たまたま私の試したリソースが再作成必要だっただけならいいのですが、注意点として覚えておくとよいでしょう。

参考

HeadlampではじめるKubernetesダッシュボード

Kubernetesのダッシュボードは無数にありますが、Headlampというオープンソースのダッシュボードが手触りよかったので、備忘録を兼ねて紹介です。

OIDC認証に対応しているのでEKSならCognito連携もできて、Karpenterの状態もプラグインで詳細に追いかけることができます。また、イベントは警告でデフォルトフィルター表示されて、マップビューでクラスター全体のリソース展開状況を見たり、特定のノード、namespaceに絞り込めたり... 運用しててほしい情報が何か考えられている感じなのが気に入りました。

知ったきっかけはKubernetes公式ブログで、Karpenterプラグインの紹介記事を読んで興味を持ちました。公式ブログやXは割と面白いプロダクトを紹介してくれるので、気が向いたらチェックするとよいです。

誰向けのダッシュボード?

ダッシュボードと一口にいっても、対象ユーザーによって求められる機能は異なります。例えばKubernetes管理をするならクラスター全体の状態を把握したい一方で、アプリケーション開発者は自分のアプリケーションに関連するリソースの管理だけに集中したいでしょう。使う人によって欲しい情報の粒度が異なるのはKubernetes運用の地味に難しいところです。

HeadlampはKubernetesのリソース状態を詳細に追いかけたい運用担当者、開発者向けのダッシュボードです。特に以下のようなニーズを持つユーザーに適していると感じます。

  • Kubernetesのリソースを視覚的に管理したい開発者
  • クラスターの状態をリアルタイムで監視したい運用担当者
  • Podのイベントやログを迅速に確認したいエンジニア
  • KarpenterやKEDAなどのオートスケーリングツールの状態を詳細に把握したいユーザー

逆に、初心者向けのシンプルなダッシュボードを求めている場合や、特定のアプリケーションの監視に特化したダッシュボードを探している場合には、精細すぎるきらいがあります。

例えばアプリケーションエンジニアに向いたダッシュボード的な存在としてArgoCDがあります。Git Opsデプロイとして有名ですが、それに付随してアプリケーション単位でリソースをまとめてくれます。結果として、ArgoCD Web UIからリソースの正常状態が確認でき、Deploymentのリスタートもできたり、Ingressからsvc経由してDeploymentまでのネットワーク的な図も見られるため、デプロイしたアプリケーション管理に特化したい場合にはこっちが適しています。

Headlampの特徴

Headlampは以下のような特徴を持っています。

  • Kubernetes-sigが管理しているオープンソースプロジェクト - kubernetes-sigs/headlamp
  • 利用形態を2種類から選択可能
    • デスクトップアプリケーションとしての利用
    • Kubernetesインクラスターでダッシュボードサーバーとして利用
  • プラグインによる拡張性(Karpenterプラグインなど)
  • リソースの詳細な表示と管理
  • マップビューによるクラスター全体のリソース展開状況の可視化
  • OIDC認証のサポート

デスクトップアプリケーション版のHeadlamp

さくっとルックアンドフィールで触りたいならデスクトップアプリケーション版がおすすめです。公式サイトからダウンロードしてインストールするだけで、すぐにKubernetesクラスターに接続して利用できます。

よくあるツール同様、手元の.kube/configを参照して認証してくれるので、割と使い勝手がいいです。

Headlampをインストールする

インストール手順に従って自分のOSに合わせてHeadlampをインストールしましょう。なお、Kubernetes SIGに管理が移行してからアプリ署名ができていないようで、Windows/macOSでは起動時にセキュリティ警告が出る場合もあり自己責任でどうぞとのアナウンスが出ています。

もしバイナリが欲しいなら、リリースページからダウンロードできます。

# Windows
$ winget install headlamp

# macOS
$ brew install --cask --no-quarantine headlamp

# Linux (バイナリ版)
$ ./Headlamp-0.36.0-linux-x64.AppImage

起動してKubernetesクラスターに接続する

起動すると.kube/configに基づいてクラスターを選択できます。認証が取れていないクラスターのステータスはBad Gatewayが表示されます。接続できるときはアクティブ状態になります。

Headlampの起動画面

クラスターを選択するとダッシュボードが表示されます。あとは好きにリソースを見たり操作したりできます。ダッシュボード周りのUIはインクラスター版とほぼ同じなので、後述のクイックツアーを参考にしてください。

デスクトップ版Headlampのダッシュボード

ちなみに設定を見ると言語やテーマも調整できます。言語選択が日本語、英語に限らないのは、たいていのオープンソースが作者の自国+英語対応なのを考えると使いやすいですね。

Headlampの言語選択

インクラスター版のHeadlamp

インクラスター版はKubernetesクラスター内にHeadlampサーバーをデプロイして利用します。利用者各自がHeadlampをインストールせずとも利用できるので、チームでKubernetesクラスターを管理・利用している場合に便利です。

インクラスターを紹介するにあたり、HeadlampはOIDC認証が使えるのは重要なポイントです。1EKSならEKS OIDC Provider ConfigurationでCognito連携させれば、Cognitoに登録したユーザー・グループでOIDC認証が利用できます。

OIDC認証のEKS構成時の要点は次の通りです。Headlampに限らず、Cognito連携ではだいたいこの流れです。

  • Cognitoユーザープール・アプリクライアントを作成し、Headlampの接続URLを元にリダイレクトURIを設定
  • Cognitoドメインを設定 (必要ならCognito IdPも)
  • EKSクラスターのOIDCプロバイダーにCognitoユーザープールを紐づけ、Cognitoからemail/groups属性を取得可能に
  • HelmでHeadlampをデプロイ、values.yamlでoidc.configでOIDC設定
  • ClusterRoleBindingでKubernetes権限とCognitoグループ・Cognitoユーザーを紐づけ
  • Headlampにサインイン時、Cognito認証画面が表示されてHeadlampからKubernetes操作

Pulumiで構成する例

以下は公式のCognito OIDCドキュメントをベースに、PulumiでEKSクラスターとCognito連携のOIDC認証を構成する例です。Terraformも同様なので、参考にしてください。2

Cognitoユーザープールとアプリクライアントを作成する

Pulumi C#でCognitoユーザープールとアプリクライアントを作成するコード例を示します。ユーザープールはemail属性をユーザー名として利用し、セルフサインアップを許可しています。MFAはTOTPアプリを利用する設定です。

using Pulumi;
using Pulumi.Aws.Cognito;
using Pulumi.Aws.Cognito.Inputs;

namespace MyCognitoApp;

var name = "my-cognito";
var opt = new CustomResourceOptions { Parent = this };

var userPoolName = "my-cognito-userpool";
var callBackUrls = new[] { "http://localhost:8080/oidc-callback" }; // kubectl port-forwardでアクセスする想定
var defaultRedirectUri = "http://localhost:8080/oidc-callback";
var users = ["foo@example.com"];

// User Pool
var userPool = new UserPool($"{name}-userpool", new()
{
    Name = userPoolName,
    // emailをユーザー名として利用
    UsernameAttributes = [usernameAttribute],
    // ユーザー確認を行う際にEmailか電話で自動検証が必要、Emailを利用
    AutoVerifiedAttributes = ["email"],
    UserAttributeUpdateSettings = new UserPoolUserAttributeUpdateSettingsArgs
    {
        AttributesRequireVerificationBeforeUpdates = ["email"]
    },
    VerificationMessageTemplate = new UserPoolVerificationMessageTemplateArgs
    {
        DefaultEmailOption = "CONFIRM_WITH_CODE", // コードで確認
        EmailMessage = "Verification Code is {####}",
        EmailSubject = $"Your verification code for cognito userpool {userPoolName}",
        SmsMessage = "Your verification code is {####}",
    },

    // セルフサインアップOKで
    AdminCreateUserConfig = new UserPoolAdminCreateUserConfigArgs
    {
        AllowAdminCreateUserOnly = false,
        InviteMessageTemplate = new UserPoolAdminCreateUserConfigInviteMessageTemplateArgs
        {
            EmailSubject = $"Your temporary password for cognito {userPoolName}",
            EmailMessage = "Your username is {username} and temporary password is {####}.",
            SmsMessage = "Your username is {username} and temporary password is {####}.",
        },
    },

    // ユーザー名 = Email の大文字/小文字は区別しない
    UsernameConfiguration = new UserPoolUsernameConfigurationArgs
    {
        CaseSensitive = false,
    },

    // mfa
    MfaConfiguration = "ON",
    // totp app mfaとする. emailだと料金プランあがるので無料枠の大きいtotp appが使いやすい (Passkeyもいいぞ)
    AccountRecoverySetting = new UserPoolAccountRecoverySettingArgs
    {
        RecoveryMechanisms = [new UserPoolAccountRecoverySettingRecoveryMechanismArgs
        {
            Name = "verified_email",
            Priority = 1,
        }],
    },
    SoftwareTokenMfaConfiguration = new UserPoolSoftwareTokenMfaConfigurationArgs
    {
        Enabled = true,
    },
}, opt);

// Domain
var cognitoDomain = new UserPoolDomain($"{name}-domain", new()
{
    Domain = $"{name}-auth-cysharpdev",
    UserPoolId = userPool.Id,
}, opt);

// Group
var userGroup = new UserGroup($"{name}-group-admin", new()
{
    Name = "admin",
    UserPoolId = userPool.Id,
    Description = "Created by Pulumi",
    Precedence = 10,
}, opt);

// User (セルフサインアップするなら不要ですが、グループ紐づけは必要になります)
foreadch (var item in users)
{
    var user = new User($"{name}-user-{item}", new()
    {
        Username = item,
        UserPoolId = userPool.Id,
        DesiredDeliveryMediums = ["EMAIL"], // メール招待
        Enabled = true,
        ForceAliasCreation = true, // 既に同じEmailのユーザーがいたらマイグレートさせる
        TemporaryPassword = "TEMPORARYPassword!!!!9999999", // パスワードポリシーを満たさないとだめ
        Attributes = new InputMap<string>
        {
            ["email"] = item,
        },
    }, new CustomResourceOptions { Parent = this, IgnoreChanges = ["attributes[\"email_verified\"]"]});

    _ = new UserInGroup($"{name}-useringroup-{item}-admin", new()
    {
        UserPoolId = userPool.Id,
        GroupName = userGroup.Name,
        Username = user.Username,
    }, opt);
}

// Client
var client = new UserPoolClient($"{name}-client-eks", new()
{
    Name = item.Name,

    // ALB経由でOAuth 2.0を仕様するための設定 (典型)
    AllowedOauthFlowsUserPoolClient = true,
    AllowedOauthFlows = ["code"],
    AllowedOauthScopes = ["email", "openid", "profile"],

    // 認証後のリダイレクトURL
    CallbackUrls = callBackUrls,
    DefaultRedirectUri = defaultRedirectUri,
    LogoutUrls = [],

    // 認証プロバイダ
    UserPoolId = userPool.Id,
    SupportedIdentityProviders = ["COGNITO"],

    // クライアントシークレットの生成
    GenerateSecret = true,
}, opt);

// 必要ならCognito IdPも追加。headlamp的にはなくてもいい

EKSクラスターにOIDCプロバイダーを紐づける

EKSのOIDCプロバイダーをCognitoユーザープールに紐づけるコード例を示します。EKSクラスターを作成するコードはよくあるので割愛します。

ちなみにEKSクラスターのOIDCプロバイダーはEKSクラスターがReady状態になってからじゃないと設定できません。depends_onはリソースの作成を待つだけで、クラスターのReady状態を待つわけではないので使えないため、EKSクラスターの作成部分をPulumi Componentクラスに分離してクラスター完了まで待つのがオススメです。TerraformならModule分離すると、同様にクラスター完了まで待ってくれます。

var name = "automode";

// PulumiならComponentに分離するとクラスター完了まで待てるのでオススメ。TerraformならModuleを分離する。
// depends_onではクラスターのReady待ちはできないので注意。
var eksCluster = new Cluster($"{name}-cluster", new ClusterArgs
{
    // ... 省略 ...
}, opt);

// OIDCプロバイダーをCognitoユーザープールに紐づける。EKSが作成完了してからじゃないと紐づけられないので注意....
_ = new IdentityProviderConfig($"{name}-idp-config-cognito", new()
{
    ClusterName = eksCluster.Name,
    Oidc = new IdentityProviderConfigOidcArgs
    {
        IdentityProviderConfigName = "cognito-idp",
        ClientId = client.Id,
        IssuerUrl = userPool.Endpoint.Apply(x => $"https://{x}"),
        UsernameClaim = "email", // Cognito Userのemail属性を認証ユーザー名に使う (Cognitoと設定合わせましょう)
        GroupsClaim = "cognito:groups", // これでグループ属性を取得できる
        GroupsPrefix = "gid:", // ClusterRoleBindingでグループ名の頭にこれを入れることでグループ紐づけできる
    },
}, opt);

HeadlampをHelmでデプロイする

EKSとCognitoのOIDC連携ができたら、HelmでHeadlampをデプロイします。HelmチャートはArtifact Hubで公開されています。values.yamlにはCognito連携のOIDC設定を指定しておくとよいでしょう。

helm repo add headlamp https://kubernetes-sigs.github.io/headlamp/
helm repo update
helm upgrade --install headlamp headlamp/headlamp --version 0.37.0 -n kube-system -f ./values.yaml

以下はvalues.yamlの例です。簡単のためCognitoのclientID、clientSecretを直書きしていますが、Secrets ManagerなどにいれてExternal Secrets Operatorで参照するほうがおすすめです。

# values.yaml
config:
  inCluster: true
  # -- base url path at which headlamp should run
  baseURL: ""
  oidc:
    # cognito UserPool `oidc-sandbox-user-pool` & app client `headlamp`
    # -- OIDC client ID
    clientID: "CognitoUserPoolに作ったクライアントのclient IDを指定"
    # -- OIDC client secret
    clientSecret: "CognitoUserPoolに作ったクライアントのclient secretを指定"
    # -- OIDC issuer URL
    issuerURL: "CognitoユーザープールのURLを指定 (https://cognito-idp.{region}.amazonaws.com/{userPoolId})"
    # -- OIDC scopes to be used
    scopes: "openid,profile,email"
    # -- OIDC callback URL
    callbackURL: "http://localhost:8080/oidc-callback" # kubectl port-forwardで8080経由でアクセスする想定

インストールすると、Headlampサーバーが起動します。HeadlampのサービスタイプがClusterIPの場合、helmコマンドの実行時にkubectl port-forwardでローカルの8080ポートに転送してアクセスする案内が標準出力に出るので、http://localhost:8080でクラスター起動しているHeadlampにアクセスしてみましょう。

export POD_NAME=$(kubectl get pods --namespace {{ include "headlamp.namespace" . }} -l "app.kubernetes.io/name={{ include "headlamp.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ include "headlamp.namespace" . }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ include "headlamp.namespace" . }} port-forward $POD_NAME 8080:$CONTAINER_POR

Headlampにアクセスできるユーザー・グループと権限の紐づけ

ClusterRoleBingingで、OIDC認証したユーザー(UserClaim)やグループ(GroupsClaim)とKubernetesの権限を紐づけます。

kubectl apply -f headlamp-clusterrolebinding.yaml

例えば次のような設定にすると、foo@example.comユーザーがKubernetesのcluster-admin権限を持ってHeadlampにログインできます。

# headlamp-clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: headlamp-admin-user-clusterrolebinding
subjects:
  # user or group. Group is recommended when using Cognito User Pool
  - kind: User
    name: foo@example.com
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

あるいは次のような設定にすると、Cognito Groupadminに所属するユーザーがKubernetesのcluster-admin権限を持てます。

# headlamp-clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: headlamp-admin-user-clusterrolebinding
subjects:
  # user or group. Group is recommended when using Cognito User Pool
  - kind: Group
    name: gid:admin
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

Headlampクイックツアー

さくっとHeadlampのUIを見てみましょう。未認証だとログイン画面が表示されるので、Signinを選んでログインします。Cognito連携しているならCognitoのログイン画面が出ます。

トップページはログイン

サインインすると、デスクトップ版同様にダッシュボードが表示されます。左ペインにリソースなどの選択、右ペインに詳細が表示されます。トップページがイベント、かつ警告フィルターされているのよくわかってる感じあります。Kubernetes運用で一番欲しい情報はこれですからね。PodがPendingならイベントですぐ知りたいあるあるです。

ClusterトップはEventとクラスター全体のリソース表示

Namespace一覧、Node一覧も見やすいです。まぁ、Nodeが100台とかあるときりがないですがしょうがない。Taintsが出ているのは地味に便利です。kubectlでいい感じに拾うの、実はだるいんですよね。

Namespace一覧

Node一覧

他のダッシュボードと違うのがマップビューです。ノード、Namespaceなどで絞り込んだり、全展開してリソースを表示したりできます。イベント的に警告があると黄色マーカーがつくのもわかりやすいです。とはいえ、意外と正常でも警告イベントは出る、っていうのもありがちなので割り切りも必要です。

Map

リソースを選択すると、右ペインに詳細が表示されます。おおむねkubectl describe相当の情報が見られます。下にスクロールするとリソースのイベントも見られますし、右上アイコンからログを見たり、シェルをつないだりもできます。

リソースは右ペインに表示

ワークロードにはPodやDeployment、StatefulSet、DaemonSet、Jobなどよくあるリソースがまとめて表示されます。この辺りはEKSクラスターのUIも同じなので見慣れている人も多いでしょう。

Workloads

きりがないので端折ります。Secretsは目玉アイコンをクリックするとbase64デコードされた値が見られます。kubectlで-o jsonpathとかで取り出すの地味に面倒なので、地味に便利です。

Secretは目玉アイコンでbase64デコードされた値を見られる

CRDは他ダッシュボードより圧倒的に完成度が高いです。CRDであってもいい感じにデータを表示してくれます。プラグインを入れてなくても、YAMLで表示とか決まったカラムだけ出す、みたいな限定的な表示に収まっていないのはすごいです。

CRDがそれぞれ解釈できているのがえらい

Pluginsで機能拡張

Headlampの最大の特徴はPluginsによる機能拡張です。アーキテクチャの図にあるように、プラグインの実体はNodeアプリケーションです。

インクラスター版は、プラグインをクラスター内部で動作させる必要があるため、Helmのvalues.yamlでプラグインを指定します。

インクラスターでのプラグインアーキテクチャ

デスクトップ版は、先に紹介したPlugin Catalogから好きなプラグインを選んでインストールできます。必要なら適当にPluginリポジトリをクローン、ローカルでnpm startもできます。

デスクトップ版でのプラグインアーキテクチャ

デスクトップ版のプラグイン管理

デスクトップ版は、Plugin Catalogから好きなプラグインを選んでインストールできます。デフォルトでapp-catalogplugin-catalogprometheusプラグインが登録されているのでいい感じに選べる感じです。プラグインのインストールはインクラスターに比べると格段に楽です。デスクトップ版なので、各自で好きなプラグインを利用できるのも使いやすいポイントです。

Plugin Catalogからプラグインを選択できる

インストールしたプラグインはPlugin CatalogのInstalledタブから確認できます。

インストール済みプラグインの確認

例えばKarpenterプラグインを入れると、Karpenterで管理しているNodePoolやEC2NodeClassをHeadlampから確認できます。プラグインの詳細は後ほど。

Karpenterプラグインの表示例

インクラスター版のプラグイン管理

インクラスター版は、Helmのvalues.yamlでプラグインをインストールするのがGit履歴でも管理しやすくおすすめです。公式サイトのPluginページを見ると、Helm Valuesでプラグインを指定する方法が紹介されています。ちなみに、Pluginだけ別ファイルにしてHelm実行時にcatして割り当てる方法も載っていますが、ここではvalues.yamlに直接書き込む方法を紹介します。

利用できるプラグインは、Artifact HubにてHeadlamp Pluginカテゴリで公開されています。3 例えば、Karpenterプラグイン、KEDAプラグイン、Trivyプラグイン、AI Assistantプラグインを導入してみましょう。

ちなみに、config.watchPlugins: trueを指定しないとプラグイン認識しないので注意してください。私はこれに気づかず時間を使いました。設定キー名からそんな挙動だって思わなくないですか。

config:
  watchPlugins: true # ここをtrueにしないとプラグインが認識されない
pluginsManager:
  enabled: true
  # configFile: "plugin.yml"   # ここをコメントアウトしてconfigContentを使う
  configContent: |
    plugins:
      - name: karpenter
        source: https://artifacthub.io/packages/headlamp/headlamp-plugins/headlamp_karpenter
        version: 0.1.0
      - name: keda
        source: https://artifacthub.io/packages/headlamp/headlamp-plugins/headlamp_keda
        version: 0.1.1-beta
      - name: trivy
        source: https://artifacthub.io/packages/headlamp/headlamp-trivy/headlamp_trivy
        version: 0.3.1
      - name: ai-assistant
        source: https://artifacthub.io/packages/headlamp/headlamp-plugins/headlamp_ai_assistant
        version: 0.1.0-alpha
    installOptions:
      parallel: true
      maxConcurrent: 2
  baseImage: node:lts-alpine
  version: latest

  # これを追加しないと https://github.com/kubernetes-sigs/headlamp/issues/3999 のようにnpm permissionsエラーになる
  securityContext:
    runAsNonRoot: false
    readOnlyRootFilesystem: false
    runAsUser: 0

helmインストールすると、Headlamp Podにサイドカーコンテナheadlamp-pluginが生えて、プラグインがインストールされます。

headlamp-78d487fbd8-597zf headlamp-plugin 5 packages are looking for funding
headlamp-78d487fbd8-597zf headlamp-plugin   run `npm fund` for details
headlamp-78d487fbd8-597zf headlamp-plugin npm notice
headlamp-78d487fbd8-597zf headlamp-plugin npm notice New patch version of npm available! 11.6.1 -> 11.6.2
headlamp-78d487fbd8-597zf headlamp-plugin npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.6.2
headlamp-78d487fbd8-597zf headlamp-plugin npm notice To update run: npm install -g npm@11.6.2
headlamp-78d487fbd8-597zf headlamp-plugin npm notice
headlamp-78d487fbd8-597zf headlamp-plugin Installed headlamp-plugin successfully.
headlamp-78d487fbd8-597zf headlamp-plugin Installing plugins from config...
headlamp-78d487fbd8-597zf headlamp {"level":"info","duration_ms":"2.45","source":"/headlamp/backend/cmd/headlamp.go","line":1390,"time":"2025-10-29T09:37:08Z","message":"Request completed successfully"}
headlamp-78d487fbd8-597zf headlamp-plugin
headlamp-78d487fbd8-597zf headlamp-plugin plugins:
headlamp-78d487fbd8-597zf headlamp {"level":"info","duration_ms":"12.84","source":"/headlamp/backend/cmd/headlamp.go","line":1390,"time":"2025-10-29T09:37:08Z","message":"Request completed successfully"}
headlamp-78d487fbd8-597zf headlamp-plugin   - name: karpenter
headlamp-78d487fbd8-597zf headlamp {"level":"info","duration_ms":"9.28","source":"/headlamp/backend/cmd/headlamp.go","line":1390,"time":"2025-10-29T09:37:09Z","message":"Request completed successfully"}
headlamp-78d487fbd8-597zf headlamp-plugin     source: https://artifacthub.io/packages/headlamp/headlamp-plugins/headlamp_karpenter
headlamp-78d487fbd8-597zf headlamp-plugin     version: 0.1.0
headlamp-78d487fbd8-597zf headlamp-plugin   - name: keda
headlamp-78d487fbd8-597zf headlamp-plugin     source: https://artifacthub.io/packages/headlamp/headlamp-plugins/headlamp_keda
headlamp-78d487fbd8-597zf headlamp-plugin     version: 0.1.1-beta
headlamp-78d487fbd8-597zf headlamp-plugin   - name: trivy
headlamp-78d487fbd8-597zf headlamp-plugin     source: https://artifacthub.io/packages/headlamp/headlamp-trivy/headlamp_trivy
headlamp-78d487fbd8-597zf headlamp-plugin     version: 0.3.1
headlamp-78d487fbd8-597zf headlamp-plugin   - name: ai-assistant
headlamp-78d487fbd8-597zf headlamp-plugin     source: https://artifacthub.io/packages/headlamp/headlamp-plugins/headlamp_ai_assistant
headlamp-78d487fbd8-597zf headlamp-plugin     version: 0.1.0-alpha
headlamp-78d487fbd8-597zf headlamp-plugin installOptions:
headlamp-78d487fbd8-597zf headlamp-plugin   parallel: true
headlamp-78d487fbd8-597zf headlamp-plugin   maxConcurrent: 2
headlamp-78d487fbd8-597zf headlamp-plugin info: Installing plugins from config: /config/plugin.yml
headlamp-78d487fbd8-597zf headlamp-plugin 1 of 4 (karpenter): info: Installing plugin karpenter
headlamp-78d487fbd8-597zf headlamp-plugin 1 of 4 (karpenter): info: Fetching Plugin Metadata
headlamp-78d487fbd8-597zf headlamp-plugin 2 of 4 (keda): info: Installing plugin keda
headlamp-78d487fbd8-597zf headlamp-plugin 2 of 4 (keda): info: Fetching Plugin Metadata
headlamp-78d487fbd8-597zf headlamp-plugin 2 of 4 (keda): info: Plugin Metadata Fetched
headlamp-78d487fbd8-597zf headlamp-plugin 2 of 4 (keda): info: Downloading Plugin
headlamp-78d487fbd8-597zf headlamp-plugin 1 of 4 (karpenter): info: Plugin Metadata Fetched
headlamp-78d487fbd8-597zf headlamp-plugin 1 of 4 (karpenter): info: Downloading Plugin
headlamp-78d487fbd8-597zf headlamp-plugin 1 of 4 (karpenter): info: Plugin Downloaded
headlamp-78d487fbd8-597zf headlamp-plugin 1 of 4 (karpenter): info: Extracting Plugin
headlamp-78d487fbd8-597zf headlamp-plugin 2 of 4 (keda): info: Plugin Downloaded
headlamp-78d487fbd8-597zf headlamp-plugin 2 of 4 (keda): info: Extracting Plugin
headlamp-78d487fbd8-597zf headlamp-plugin 2 of 4 (keda): info: Plugin Extracted
headlamp-78d487fbd8-597zf headlamp-plugin Moved directory from /tmp/headlamp-plugin-temp-iMEOnN/headlamp_keda to /headlamp/plugins/headlamp_keda
headlamp-78d487fbd8-597zf headlamp-plugin 2 of 4 (keda): success: Plugin Installed
headlamp-78d487fbd8-597zf headlamp-plugin 2 of 4 (keda): success: Plugin installed successfully
headlamp-78d487fbd8-597zf headlamp-plugin 1 of 4 (karpenter): info: Plugin Extracted
headlamp-78d487fbd8-597zf headlamp-plugin Moved directory from /tmp/headlamp-plugin-temp-ApkBDO/headlamp_karpenter to /headlamp/plugins/headlamp_karpenter
headlamp-78d487fbd8-597zf headlamp-plugin 1 of 4 (karpenter): success: Plugin Installed
headlamp-78d487fbd8-597zf headlamp-plugin 1 of 4 (karpenter): success: Plugin installed successfully
headlamp-78d487fbd8-597zf headlamp-plugin 3 of 4 (trivy): info: Installing plugin trivy
headlamp-78d487fbd8-597zf headlamp-plugin 3 of 4 (trivy): info: Fetching Plugin Metadata
headlamp-78d487fbd8-597zf headlamp-plugin 4 of 4 (ai-assistant): info: Installing plugin ai-assistant
headlamp-78d487fbd8-597zf headlamp-plugin 4 of 4 (ai-assistant): info: Fetching Plugin Metadata
headlamp-78d487fbd8-597zf headlamp-plugin 3 of 4 (trivy): info: Plugin Metadata Fetched
headlamp-78d487fbd8-597zf headlamp-plugin 3 of 4 (trivy): info: Downloading Plugin
headlamp-78d487fbd8-597zf headlamp-plugin 4 of 4 (ai-assistant): info: Plugin Metadata Fetched
headlamp-78d487fbd8-597zf headlamp-plugin 4 of 4 (ai-assistant): info: Downloading Plugin
headlamp-78d487fbd8-597zf headlamp-plugin 3 of 4 (trivy): info: Plugin Downloaded
headlamp-78d487fbd8-597zf headlamp-plugin 3 of 4 (trivy): info: Extracting Plugin
headlamp-78d487fbd8-597zf headlamp-plugin 4 of 4 (ai-assistant): info: Plugin Downloaded
headlamp-78d487fbd8-597zf headlamp-plugin 4 of 4 (ai-assistant): info: Extracting Plugin
headlamp-78d487fbd8-597zf headlamp-plugin 3 of 4 (trivy): info: Plugin Extracted
headlamp-78d487fbd8-597zf headlamp-plugin Moved directory from /tmp/headlamp-plugin-temp-NlAooa/headlamp_trivy to /headlamp/plugins/headlamp_trivy
headlamp-78d487fbd8-597zf headlamp-plugin 3 of 4 (trivy): success: Plugin Installed
headlamp-78d487fbd8-597zf headlamp-plugin 3 of 4 (trivy): success: Plugin installed successfully
headlamp-78d487fbd8-597zf headlamp-plugin 4 of 4 (ai-assistant): info: Plugin Extracted
headlamp-78d487fbd8-597zf headlamp-plugin Moved directory from /tmp/headlamp-plugin-temp-FlcHCb/headlamp_ai_assistant to /headlamp/plugins/headlamp_ai_assistant
headlamp-78d487fbd8-597zf headlamp-plugin 4 of 4 (ai-assistant): success: Plugin Installed
headlamp-78d487fbd8-597zf headlamp-plugin 4 of 4 (ai-assistant): success: Plugin installed successfully
headlamp-78d487fbd8-597zf headlamp-plugin info: Bulk installation completed: {"total":4,"failed":0,"skipped":0,"successful":4}
headlamp-78d487fbd8-597zf headlamp-plugin Watching /config/plugin.yml for changes...

Headlampでプラグインを確認する

Headlampにプラグインをインストールすると、UIに変化が現れます。例えばKarpenter、Keda、Trivyプラグインをインストールすると左ペインにメニューが追加されます。

プラグインが左ペインメニューに表示

導入しているプラグインは、Headlampの設定画面からも確認できます。Helm values.yamlで指定したプラグインが導入できたかはここを見るのがオススメです。

設定 > プラグインから一覧で確認できる

Karpenterプラグイン

Karpenterプラグインを使うと、Karpenterで管理しているNodePoolやEC2NodeClassをHeadlampから確認できます。Karpenterでスケーリングしているノードの状態を詳細に把握できるのは便利です。

ただ、EKS AutomodeはEC2NodeClassではなくNodeClassを使うため、HeadlampのKarpenterプラグインではNodeClassは表示されません。プラグインリポジトリ的には対応している的な文言なので、将来的に対応されそうです。気長に待ちましょう。

EKS AutomodeではEC2 NodeClassは存在しない

Kubernetesを運用していて割とほしいのが、クラスター全体のCPU、メモリに対してどれぐらい使っているかです。これはHeadlampのClusterトップページで表示されないので弱いなぁと感じるのですが、KarpenterプラグインのNode Poolから、NodePoolごとにCPU、メモリ使用率が表示されるので便利です。

NodePool一覧で利用割合がわかる

運用していて気になるのが「ちゃんとPodはスケールアウトできているか」です。これを確認するにはKubernetesイベントの「Pending Podイベント」を見る必要があるのですがイベントって流れるんですよね。でも、Karpenterプラグインを入れるとPending PodsタブでPodの状態をリアルタイムで把握できます。これは本当に便利です。この気持ち、伝わってほしい。

Pending PodsタブでPodの状態を確認

Scaling ViewでNode Claimを見られるので、Karpenterでスケーリングしているノードの状態を詳細に把握できます。Zoneが出ているあたりがわかってる感じあります。

Node Claimの詳細が確認できる

KEDAプラグイン

KEDAプラグインを使うと、KEDAのスケーリング設定であるScaledObjectやTriggerAuthenticationリソースをHeadlampから確認できます。ネームスペース横断でスケール対象を一括確認できるのは結構便利です。KEDAはZero to Scale4ができることもあって、複数ネームスペースにアプリケーションやDeploymentが分散していても漏れにくくなるのは運用してて嬉しいポイントです。

KEDAのScaledObjectが一覧表示

ScaledObjectの対象Podやトリガーが確認できるのも便利です。KEDAのスケール条件って実はちょいちょい触るわりに、詳細を把握していないとkubectlから読み解くのも難しい代表です。UIでいい感じに見られるのは助かりますよね。

ScaledObjectの詳細が確認できる1

ScaledObjectの詳細が確認できる2

MapにもScaledObjectが表示されるので、KEDAでスケーリングしているアプリケーションの分布も把握できます。

MapでScaledObjectも表示される

AI Assistantプラグイン

AI Assistantプラグインを使うと、そのKubernetesクラスターについてAIを使ったサポートを受けられます。プラグインをインストールすると、右上にAIアイコンが表示され、チャット形式でKubernetesに関する質問ができる感じです。ローカル環境にKubernetes MCPを使って聞く方法も広く使われつつありますが、Headlampに組み込まれているというのはAI統制的には悪くないアプローチに思えます。

AIメニューが右上にある

AIチャットが表示する

AIの一覧はよくある感じです。OpenAIのモデルはGPT-4.1などでちょっと古いです。Azure OpenAIも選択できるので、企業としても使いやすい余地があります。

AI一覧

OpenAIの設定例

軽くやり取りをしてみましょう。

チャットは英語だけでなく日本語も行けます。例えば以下のような質問で英語同様の回答が返ってきていることがわかります。

さて、質問の回答はKubernetes管理者ならある程度なるほどとなる情報です。ちなみにtaintが... という部分は適切で、この時のEKSクラスターはsystemノードのみが起動していたので、taintが原因でスケジューリングされなかったことがわかります。

ただ、肝心の質問に対する回答としてはちょっと足りなくもあります。この時Podが起動しない原因は、Podが期待するnodeSelector: arm64を満たすノードに相当するNodeがないためです。Podを起動できるようにするには、KarpenterでNodePoolとしてarm64ノードを追加する必要があります。このAI回答から、どうすればいいかまで一直線にたどり着くのは難しいですが、モデルが少し古いのと、ファンクションコーリングが浅いあたり、今後のチューニングに期待です。

User
Why my nginx deployment'pod won't be running?

AI Assistant
Based on the reported warnings for your main cluster, here are several reasons why your nginx deployment pod (nginx-698f5c6f78) isn’t running:

Service Account Not Found:

The deployment is trying to use service account test-app in the default namespace, but it does not exist. You’ll see an error like:
pods "nginx-698f5c6f78-" is forbidden: error looking up service account default/test-app: serviceaccount "test-app" not found

Node Scheduling Problems:

No suitable nodes are available for pod scheduling. Specifically:

0/1 nodes are available: 1 node(s) had untolerated taint {CriticalAddonsOnly: }. no new claims to deallocate...

Your only node, i-0647b9224fd6861a2, has a taint discouraging regular pods from being scheduled unless they tolerate it.

Image Filesystem Capacity Issues:

There’s a warning indicating invalid capacity (0) on the image filesystem. This can prevent pods from starting due to lack of space.
To resolve these issues, you should:

Create the test-app service account in the default namespace, or change your nginx deployment to use an existing service account.
Either remove or add a toleration for the {CriticalAddonsOnly:} taint in your pod spec.
Check node disk/volume availability.
You
nginx Deploymentのpodが起動してこないのはなぜですか?

AI Assistant
main クラスターの警告から、nginx Deployment の Pod が起動しない主な理由は以下の通りです。

ServiceAccount が存在しない

Pod 起動時「pods "nginx-698f5c6f78-" is forbidden: error looking up service account default/test-app: serviceaccount "test-app" not found」というエラーが出ています。これは、Deployment で指定している test-app サービスアカウントが default namespace に存在しないためです。
ノードに taint がありスケジューリングできない

「0/1 nodes are available: 1 node(s) had untolerated taint {CriticalAddonsOnly: }」という警告があり、ノード i-0647b9224fd6861a2 に taint が付いているため、Pod がスケジュールされません。Pod の spec に tolerations を追加する必要があります。
image filesystem 容量が無効

「invalid capacity 0 on image filesystem」という警告があり、ノードのディスク容量不足または設定が正しくない可能性があります。
この3つが主な原因です。対応策としては:

test-app サービスアカウントを作成するか、Deployment の serviceAccount 名を修正
Pod の spec に taint を許容する tolerations を追加
ノードのディスク容量を確認し、必要であればリソース追加

チャットの例

調査だけでなく簡単な操作もできます。例えば、チャットからPodを削除させたり、Deploymentをリスタートさせることもできるのは怖さと便利さが入り混じっています。Get以外の操作をすると確認プロンプトが出ますが、結果がでないあたりもファンクションコールが足りない感じもあります。

You
Pod inflate-68c59b845-2phjgをterminateしてください。

削除操作でプロンプト表示される

削除後の結果は出ない

この辺りの挙動はai assistantプラグインの実装を見るといいでしょう。AI Assistantはkubectlのようなコマンドを実行するのではなく、Kubernetes APIを直接叩いて情報を取得しています。例えば、DeleteやUpdateなどの操作でプロンプト制御していることもわかります。

ベースプロンプトから、AIアシスタントでどのようなユーザー体験をさせたいのかがわかります。デスクトップ版ではマルチクラスター管理していることも想定していることもわかります。

AI Assistantの難点として、チャット履歴が残りません。つまり、Headlampのウェブページをリロードすると履歴が吹き飛びます、ひどい。細かな挙動やモデル更新、履歴保持など気になるところはあるので今後のアップデートに期待しつつも、MCPサーバーでローカルからKubernetes APIを叩いて情報を取得するよりも始めやすい感じはあります。

まとめ

割と細かいところまでKubernetesリソースを追いかけられるダッシュボードとしてHeadlampは優秀で好きです。特にプラグインによる拡張性が高く、KarpenterやKEDAなどのオートスケーリングツールの状態を詳細に把握できるのは便利です。

OIDC認証もHelmで完結できるのは割とちゃんと叩かれている感じがして好印象です。EKSでCognito連携しているなら、Headlampを使うことでKubernetesリソースの管理がより効率的になるでしょう。

kubectlでわかるも大事、ArgoCDでアプリケーションデプロイを管理できるのも大事、DatadogでKubernetesクラスター状態がわかるのも大事、でもHeadlampでそこにあるKubernetesクラスターを詳細に追いかけられるのも大事、という感じで、HeadlampはKubernetes管理の選択肢として十分に価値があります。

参考


  1. 実運用を考えるとインクラスターで使うときは認証付きにせざるを得ないので、クラスター認証しておくとかせずOIDC認証できる程度の状態で試すのがおすすめです。細かいけど大事。
  2. EKSのOIDC設定は公式ドキュメントやIntroducing OIDC identity provider authentication for Amazon EKS | AWS Blogが詳しく、悩んだときは大いに参考となります。
  3. カテゴリあるの気づかなかったのですが、便利すぎませんか。
  4. Podをゼロ台までスケールダウンさせ、必要に応じてスケールアウトさせる運用方法

NuGet Trusted PublishingでOIDCを使ってトークンレスでCIからNuGetパッケージを公開する

NuGet Trusted Publishingが2025年9月22日に公開され、OpenID Connect (OIDC)を使ってトークンレスでCIからNuGetパッケージを公開できるようになりました。

今回は、NuGet Trusted Publishingを使ってGitHub ActionsからトークンレスでNuGetパッケージを公開するメリットと手順を解説します。NuGet Trusted Publishingは積極的に使っていきましょう。

NuGet Trusted Publishingとは

従来、NuGetパッケージを公開するには、APIキーを発行してdotnet nuget pushコマンドに渡す必要がありました。 このようなAPI認証はシンプルなため長年利用されてきましたが、ここ数年は複数の課題が指摘されています。

  • APIキーが長期間有効であるため、漏洩リスクが高い
  • APIキーのローテーションが手動であり、運用コストが高い (最長1年)
  • CI/CD環境にAPIキーを安全に保存する必要がある
  • APIキーが漏れたらだれでもどこからでもパッケージを公開できてしまう

APIキー認証の問題はNuGetに限らず、多くのパッケージシステムで共通した課題といえます。例えば利用者が特に多いnpmは、パッケージ作者から詐取したAPIキーを使ってマルウェア入りパッケージを公開する事件が何度も発生しています。npmにおけるパッケージAPI認証の課題に対するGitHubの対策は「TOTPからPasskeyベースへの移行 (認証のフィッシング対策強化)」と「OIDCを使ったトークンレスパッケージ公開(Trusted Publishing)」です。npmにおいてTrusted Publishingが先行していているので、本記事と合わせて読むと参考になる記事を紹介します

本記事で紹介するNuGet Trusted Publishingは、細かい違いはあるものの、おおむねnpm Trusted Publishingと同様の仕組みをNuGetに導入した考えて差し支えないでしょう。

OIDCを使ったトークンレスパッケージ公開は何を解決するのか

OIDCを使ったトークンレスパッケージ公開は、API管理を不要にすることが最大の利点です。「手元からパッケージ公開せず、基本的にパッケージ公開はCI/CDからのみ行う」、という前提を置く限りかなり有効な手法といえます。

  • APIキーが短時間のみ有効な自動発行されたトークンに置き換わるため、漏洩時のリスクが大幅に低減される
  • ユーザーによるAPIキーローテーションが不要になる
  • CI/CD環境にAPIキーを保存する必要がなくなる
  • GitHub ActionsなどのOIDC対応CI/CD環境からのみパッケージを公開できるように制限できる

ただし、OIDCを使ったトークンレスパッケージ公開には以下のような制約もあります。

  • CI/CD環境からのみパッケージを公開できる (手元からOIDCでパッケージ公開はできない)
  • CI/CD環境とパッケージシステムの両方がOIDCに対応している必要がある

また、OIDCを使ったトークンレスパッケージ公開を導入しても、以下のようなリスクは残ります。

  • CI/CD環境へ不正アクセスされた場合、パッケージを公開されるリスクがある
  • あくまでもパッケージ公開の認証を強化するものであり、パッケージ内容の改ざんやマルウェア混入を防止するものではない

OIDCを使った基本的な仕組みについては、業界で取り組んでいるOpenSSFイニシアチブ)を参照するとフローや概念がよくわかります。

alt text

NuGet Trusted Publishingの概要

Trusted PublishingはOIDCを使ってパッケージ公開時のサービス認証を行う仕組みです。NuGet Trusted Publishingでは、事前に登録されたCI/CD環境からリクエストに限り、短時間のみ有効なトークンを発行してパッケージ公開を許可します。このため、ユーザーが直接APIキーを管理する必要はありませんが、コマンドにはdotnet nuget publish -k "token"のように短命なトークンを渡すことになります。

OIDCを使った認証フローは次の通りです。

  1. CI/CD環境がOIDCトークンを発行
  2. CI/CD環境がOIDCトークンを使ってnuget.orgから短時間のみ有効なパッケージ公開トークンを取得
  3. CI/CD環境がパッケージ公開トークンを使ってNuGetパッケージを公開

OIDCには事前認証が必要なため、Trusted Publishingを利用する = NuGetで信頼設定を行う、GitHub Actionsは指定した設定で実行することを意味します。

  • nuget.orgでTrusted Publishingのポリシーを設定
  • CI/CDはポリシーに沿った設定でNuGetパッケージ公開

NuGet Trusted Publishingは、CI/CD環境としてGitHubに対応していますが他のCI/CD環境は対応していません。1

設定方法

NuGet Trusted Publishingを設定する手順を見ていきましょう。

nuget.orgでTrusted Publishingのポリシーを設定

Nugetにログインして、アカウントメニュー -> Trusted Publishingを開きます。

trusted publishing

Createをクリックして、Trusted Publishingポリシーを作成します。下は私の管理しているSkiaSharp.QrCodeリポジトリの設定例です。

  • ポリシー名は任意の名前でOK。識別しやすいようにリポジトリ名などを入れておくとよさそう
  • Package OwnerはNuGetのアカウントを指定。ここでOrgアカウントを選択すれば、NuGet Orgアカウントのパッケージが対象となる
  • Repository OwnerはGitHubのリポジトリ所有者名を指定。GitHub Orgのパッケージなら、GitHub Organization名を指定
  • Repository Nameはリポジトリ名を指定
  • Workflow Fileは、GitHub Actionsワークフローのファイル名を指定。注意書きにあるように、.github/workflows/以下の「ファイル名」のみを指定

Policy

CI/CDワークフローの設定

CI/CDワークフローと言ってもGithub Actionsにしか対応していないので、GitHub Actionsワークフローです。

  • ワークフローファイル名は、NuGet Trusted Publishingポリシーで指定したWorkflow Fileと一致させる必要がある
  • Nuget/loginアクションを実行するジョブのPermissionsにid-token: writeの権限を付与するのがポイント。AWSやGoogle Cloud、AzureといったほかのOIDCデプロイと同じように、OIDCトークン発行のために必要
  • NuGetの短命トークン取得には、NuGet/loginアクションを使うのが推奨。nuget.orgからパッケージ公開トークンを取得し、ステップ変数にセットしてくれる

サンプルのワークフローrelease.yamlは次の通りです。この例では、git tagでバージョンタグをプッシュしたときにパッケージを公開するようにしています。

name: Publish NuGet package

on:
  push:
    tags:
      - 'v*.*.*' # バージョンタグをプッシュしたときに実行

jobs:
  publish:
    permissions:
      contents: read
      id-token: write # OIDCトークン発行のために必要
    runs-on: ubuntu-24.04
    steps:
    - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
    # ビルド & パック
    - name: dotnet pack
      run: dotnet pack -c Release -o ./bin
    # NuGet/loginアクションでOIDCトークンを使って短命トークンを取得
    - name: NuGet Login
      uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544 # v1.0.0
      id: login
      with:
        user: my-nuget-username # NuGetのユーザー名を指定、Orgアカウントでもユーザー名となる
    # 取得した短命トークンを使ってパッケージを公開
    - name: Push package
      run: dotnet nuget push ./bin/*.nupkg --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json

ワークフローを実行

タグをプッシュすれば、GitHub ActionsがトリガーされてOIDCを使ってNuGetパッケージが公開されます。

制約

2025年10月1日時点で、Reusable WorkflowでNuGet/Loginを使った場合に、NuGetで登録したポリシーを見つけられないことを確認しています。Reusable Workflowを使わずに、直接ワークフローに記述する場合は問題ないので、こちらを利用することをお勧めします。

Error: Token exchange failed (401): No matching trust policy owned by user '***' was found.

NuGet/loginのIssueで報告してありますが、これはNuGet側で対応しないといけなさそうな気配があるのでしばらくかかりそうです。

再現ワークフローは次の通り

# `.github/workflows/nuget-push.yaml@main`
name: Push NuGet
on:
  workflow_call:

jobs:
  create-release:
    permissions:
      contents: write
      id-token: write # required for NuGet Trusted Publish
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    steps:
      - name: NuGet login (OIDC → temp API key)
        uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544 # v1.1.0
        id: login
        with:
          user: my-nuget-user
# 呼び出し元ワークフロー
name: Build-Release

on:
  workflow_dispatch:

jobs:
  dummy:
    permissions:
      contents: write
      id-token: write # required for NuGet Trusted Publish
    uses: .github/workflows/nuget-push.yaml@main

まとめ

NuGetのAPI Token認証の課題を解決するTrusted Publishingが公開されました。API Tokenではトークンローテーションや対応パッケージの指定といったわずらわしさがありましたが、Trusted Publishingを使えばリポジトリごとにOIDCで認証できるため、認証管理がかなりシンプルになります。

とはいえ、認証の境界ラインがGitHub ActionsなどのCI/CD環境に移動するため、CI/CD環境のセキュリティがザルだと意味がありません。CI/CD環境のセキュリティ対策をしっかり行った上で、Trusted Publishingを導入することをお勧めします。2025年現在ならGitHub 2FAからSMSは削除し、Passkeyベースの認証に移行するのがよいでしょう。


  1. 2025年10月時点

C#サーバーをなぜLinuxで動かすのか

C#は様々な用途に利用できる言語ですが、.NETになってからはサーバーサイドとしてはLinuxで動かすのが自然な選択肢になっています。なぜLinuxで動かすのか理由をいくつか挙げてみます。

もしかすると

C#はWindows向けに開発された言語、.NET Framework (.NET Coreではないもの)のころはWindowsでしか動作しなかったという印象が残っている方もいるのではないでしょうか。この記事は、2020年に.NETとしてブランドが統一されてからは、Windows専用ではなくなりLinuxで動かすことが妥当になっている理由をなるべく分かりやすく解説することを目的としています。

はじめに

.NET Coreの登場以降(現在は.NET)1、クロスプラットフォーム対応が進み、Linuxでも高いパフォーマンスと安定性を発揮するようになりました。私自身は過去に.NET FrameworkだったころC#をWindowsサーバーで動かしていましたが、.NET以降はLinuxで運用していることがほとんどです。

C#サーバーをLinuxで動かす理由として、コスト、コンテナ、エコシステム、パフォーマンス・安定性、セキュリティ、トラブルシュートの6つが挙げられます。別に難しいことはなく、要するに他の言語と同じ理由であって、C#もLinuxで動かすのが合理的判断ということです。

1. コスト

Linuxを選ぶ最大の理由はコストです。LinuxとWindowsはライセンス費用に大きな差があり、主要クラウドのIaaSにおいてLinuxはWindowsより低コストです。オンプレ環境でWindowsサーバーを安く調達できるなどの理由がないなら、Linuxを選ぶと良いでしょう。

背景

どんなサービスにおいても、コストは最優先でサーバー環境を検討することになります。運用コストが低ければ低いほどサービス継続を決定する損益分岐点を下げ、より長くサービスを継続できるのでビジネスサイドに説明し合意を得やすくなります。このため、どんな言語でもどうやったら安く運用できるかは真っ先に検討されるべきですし、みなさんされていることでしょう。C#も例外ではありません。

ARM64にも対応しているのでAWSのGravitonやAzureのArm VM、Google CloudのArm VMを利用でき、x86_64よりもさらに安価なLinuxサーバーを利用できます。x86_64とARM64はビルド時に切り替えるだけで、ほとんどのコードがそのまま動作するため、積極的に狙っていく価値があります。ARM64は経験上、特に暗号系処理で10%程度パフォーマンスが落ちる傾向ですが、x86_64系よりコストが30-40%下がるためトータルで有利です。

IaaS

利用条件にもよりますが、多くのクラウド環境のオンデマンドインスタンスにおいてLinuxインスタンスとWindowsインスタンスで1.7倍程度(ARM64比は2倍程度)の価格差があります。2

以下に主要なクラウドプロバイダーの例を挙げます。

AWS EC2

OS インスタンスタイプ vCPU メモリ 東京リージョンのオンデマンド料金/hour (2025年9月時点) 同Linuxに対する倍率
Linux m7i.large 2 8 GB USD 0.1302 -
Linux m7a.large 2 8 GB USD 0.14973 -
Linux (ARM64) m7g.large 2 8 GB USD 0.1054 -
Windows m7i.large 2 8 GB USD 0.2222 170-210%
Windows m7a.large 2 8 GB USD 0.24173 172-229%

Google Cloud Compute Engine

OS マシンタイプ vCPU メモリ 東京リージョンのオンデマンド料金 (2025年9月時点) 同Linuxに対する倍率
Linux c4-standard-2 2 7 GB USD 0.12439532 -
Linux (ARM64) c4a-standard-2 2 7 GB USD 0.11532114 -
Windows c4-standard-2 2 7 GB USD 0.21639532 (=0.12439532 + (0.046 * 2 (CPU数))) 173-187%

Azure VM - AHB無効

OS インスタンスタイプ vCPU メモリ 東日本リージョンのオンデマンド料金/hour (2025年9月時点) 同Linuxに対する倍率
Linux D2s v6 2 8 GB USD 0.1300 -
Linux (ARM64) D2ps v6 2 8 GB USD 0.0902 -
Windows D2s v6 2 8 GB USD 0.2220 170-243%

FaaS/CaaS

コストを小さくする場合、FaaSやCaaSは有力な選択肢になります。リクエストが散発的であったり、ある程度のボリュームしか来ないなら、サーバーレスは安く、スケール性も担保できて便利です。C#の場合、スタートアップ時間を稼ぐならReadyToRunビルドやNativeAOTビルドを利用することで、コールドスタート時間を小さくできます。

FaaS(Function as a Service)においても、LinuxベースのランタイムはWindowsベースのランタイムよりも安価です。むしろWindowsベースのランタイムを提供しているのは、Microsoft Azure Functionsだけで、AWS LambdaやGoogle Cloud FunctionsはLinuxベースのランタイムしか提供していません。 CaaS(Container as a Service)においては、Linuxコンテナが前提となっています。

2. コンテナ

Linuxコンテナを利用することで、サーバーのデプロイやスケーリングが容易になります。C#はLinuxコンテナでの動作が公式サポートされており、公式からdistroless,chiseledイメージも提供されています。現在では、20MB~前後までイメージサイズが抑えられるようになりました。もちろんサーバーコードが膨れるほどイメージは大きくなりますが、イメージサイズが問題になることはほぼなく、コンテナ動作させるならLinuxコンテナが第一候補になります。

背景

ここ10年でサーバー環境は大きく変化を遂げましたが、特にコンテナ技術の普及は著しいものがあります。C#サーバーも例外ではありません。 .NETは10年に渡って公式にLinux向けコンテナを提供し、.NET 8以降はLinuxコンテナで非root実行をデフォルト化し、Linuxコンテナ運用のベストプラクティスにも沿っています。最新コンテナイメージもDebian/Alpine/Mariner distroless/Ubuntu chiseled3が提供されています。

2023年には、.NETクラウドアプリをRootless Linuxコンテナで実行することを推奨する記事を公開しました。内容は私が認識する限りにおいて、Linuxコンテナサーバーにおける現在合意が取れているものと遜色なく、C#におけるコンテナ運用が一般的であることを示しています。

コンテナOS

C#サーバーをコンテナで動かす場合、Linuxコンテナが普通に利用されます。このため、他言語同様に、一般的なコンテナサービス・プラクティスがそのまま適用できます。コンテナのメモリハードリミットを設定することでC#内部ランタイムのGCやメモリ管理も適切に管理されるため、コンテナのスペック調整でハマることもありません。コンテナにおいてもC#サーバーは非常に安定して動作します。

Kubernetes

コンテナ運用の2台巨頭は、KubernetesとCaaSになって久しいです。CaaSはコンテナにさえなっていれば割と意識することがありません。KubernetesでC#サーバーを動かす場合も、他言語と同様にKubernetesのリソース定義を作成し、デプロイするだけで動作します。C#サーバーだからといって特別な設定は不要です。環境変数で設定を差し込んだり、ConfigMap/Secretを利用したり、Liveness/Readiness Probeを設定したり、Horizontal Pod Autoscalerを設定したりなど一般的なKubernetes運用がそのまま利用できます。

イメージサイズ

コンテナを運用していて最も面倒なのがイメージサイズです。何しろイメージをプルするたびにネットワーク帯域を消費し、ストレージを消費します。特にC#はVM系言語であるため、イメージサイズが大きくなるんじゃないかと印象が先行します。

実際、.NET7までは標準イメージを利用すると200MBを超えるイメージサイズになっていました。しかし、.NET 8以降はchiseledイメージが提供され、VM系の言語でありながらミニマム40-50MB前後の小さなイメージでC#サーバーを動かせます。Alpineベースのイメージも提供されていますが、現行のスタンダードにそってchiseledイメージを利用するのが良いでしょう。

https://devblogs.microsoft.com/dotnet/announcing-dotnet-chiseled-containers/ より

主要なベースイメージのWebサーバーコンテナイメージサイズ例 (2025年9月時点)

以下は.NETWebサーバー用のコンテナイメージのサイズ比較です。デプロイ時に実際に転送されるCompressedサイズに着目すると、Ubuntu Jammyをベースにした場合、chiseledイメージを利用することで約1/5のサイズに削減できます。

Image Kind Base Image Uncompressed Image Size Compressed Image Size % Size Savings Over Baseline
Baseline aspnet:8.0-jammy 217 MB 90.9 MB
Chiseled aspnet:8.0-jammy-chiseled 111 MB 49.3 MB 46%
Chiseled + ASP.NET Composite Runtime aspnet:8.0-jammy-chiseled-composite 103 MB 40.8 MB 55%

.NETはVM系言語ですが、C#ビルド時に.NETランタイムを同梱(self-contained)して重複排除(triming)することでさらにサイズを小さくできます。このオプションでビルドしても動作に大きな副作用がないので有力な選択です。個人的にまだNativeAOTはおすすめしにくいので、chiseled + Self-contained + Trimmingがおすすめです。

Image Kind Base Image Uncompressed Image Size Compressed Image Size % Size Savings Over Baseline
Self-contained + Trimming runtime-deps:8.0-jammy 146 MB 57.9 MB 36%
Chiseled + Self-contained + Trimming runtime-deps:8.0-jammy-chiseled 39.3 MB 16.4 MB 82%
Native AOT runtime-deps:8.0-jammy-chiseled 27.7 MB 12.4 MB 86%

3. エコシステム

サーバーを動かすにあたりLinuxにはツールやミドルウェアが充実しています。C#サーバーをLinuxで動かすことで、これらを活用できるため、開発や運用が容易になります。Windowsで運用していた時に都度Windowsで動かせるか気にしていたことを振り返ると、豊富なLinuxノウハウがそのまま活用できるのは地味ですが大きなメリットだと感じます。

背景

Linuxで動作させることで、豊富なオープンソースライブラリやツールを利用できます。これにより、開発効率が向上し、コミュニティへのフィードバックしやすくなります。例えばプロキシサーバーのnginxやEnvoy、各種CLIツール、OpenTelemetry系ツールはわかりやすい例でしょう。特に気にしなくてもLinuxで動くツールと連携できます。

サーバーとしてのC#アプリケーションはASP.NET Coreがデファクトスタンダードとなっていますが、nginxやEnvoyをリバースプロキシとして組み合わせることで、SSL終端や負荷分散、キャッシュなどの機能を簡単に追加できます。4これはKubernetesなどの内部サービス通信で依然として利用されるパターンです。

この辺りは、AIとの協業という観点でも重要です。何しろツールの情報が豊富かどうかは回答の精度に大きく影響します。Linuxツールの情報は豊富であることを背景に、AIを活用した開発や運用がしやすくなります。とはいえ、新しいツールなどでは情報が不足していることもあるので、そういう時は自分で頑張りましょう。

FaaSサポート

エコシステムにはツールだけでなく、FaaSサービスも含めるのが適切でしょう。FaaSは基本的にLinuxベースですが、いずれも最新のC# LTSランタイムがサポートされています。5

サービス サポートされているC#ランタイム
AWS Lambda 8.0
Google Cloud Run Functions 8.0
Azure Functions 8.0, 9.0, 10.0 (preview)
OCI Functions 8.0

4. パフォーマンス・安定性

C#は.NET Core(現在は.NET)においてLinuxで動作することを前提に進化してきたため、パフォーマンスや安定性も備わっています。実際、私の経験でもLinuxの方がパフォーマンスよく小さいリソースで安定動作します。特に安定になって困ったことはないので安心して利用しています。

背景

Linuxは低オーバーヘッドのユーザーランド/カーネル、ネットワーク/I/Oスタックを持っています。.NET/C#もLinuxでの動作を改善する方向に多くの改善が行われています。.NET5.NET6.NET7.NET8.NET9と毎年のようにパフォーマンス改善が行われており、これらの記事でもたびたびLinuxでの動作最適化が取り上げられています。

Windowsと比較して、Linuxは軽量でリソース消費が少なく、C#サーバーのパフォーマンスを最大限に引き出せます。特にコンテナ環境では、Linuxの軽量性が際立ちます。Windowsだとミニマムでも4-8GB程度のメモリや2vCPUが推奨されますが、Linuxなら256-512MB、1vCPUでも十分に動作します。サイジングをしていてWindowsはOS負荷が大きく、サーバー運用としては余計にリソースが必要だと感じることは否めません。

TechEmpower

WebサーバーのパフォーマンスベンチマークであるTechEmpowerはWindowsがなくLinuxのデータですが、多くの実装の中でC#の位置づけを客観的に見る1つの指標になります。以下は2025年9月時点のRound 23のPlaintextの結果です。C#サーバーであるaspnetcoreは、Linuxで動かす様々な言語のWebサーバー構成の中でも割と良いスコアを出しています。6

TechEmpowerでのスコアはC#ランタイムやASP.NET CoreのIssueでもたびたび参照されており、C#のパフォーマンス改善に関する議論で引用・改善結果の報告が行われているものの、あくまでも参考程度にどうぞ。7

TechEmpower Round23 - Plaintext

5. セキュリティ

LinuxはOSとしてのセキュリティ機能が豊富です。また、コンテナで動作させることで、より脅威に対する多層防御が可能になります。もちろんOSレベルのセキュリティは最終防御なので、その前にネットワーク・アプリケーションレベルでセキュリティ対策が必要ですが、OSレベルのセキュリティ機能が充実していることは大きな利点です。

背景

SELinux/AppArmor、Capabilities、read-only root filesystem、seccompなど、Linuxには多くのセキュリティ機能が組み込まれています。C#サーバーをLinuxで動かすことで、これらのセキュリティ機能を活用でき、アプリケーションのセキュリティを強化できます。

Windowsにもセキュリティ機能はありますが、Linuxはより細かな制御が可能で、また多くのチューニング情報が公開されているため運用する上で救われることも多いです。もちろん、WindowsにもDefender ATPなど高度なセキュリティ機能があることは付記します。

ディストリビューションの選択

.NETとして複数のLinuxディストリビューションを公式サポートしています。AWSはAmazon Linux (AL2023)もサポートしており、自分が動作する環境に合わせてディストロを選べます。経験上は、コンテナならchiseledイメージ、AWSならAL2023、他ではDebian系(Ubuntu/Debian)が選びやすいです。

6. トラブルシュート

トラブルシュートは、OS、C#アプリケーションそれぞれの観点で行う必要があります。Linuxには多くの診断ツールがあり、C#サーバーもLinuxで診断ツールが利用できるため、Linuxだからといってトラブルシュートで困ったケースはありません。この辺りはどの言語であっても、ある程度の習熟が必要になるため、C#だから特別に難しいということはありません。

背景

トラブルシュートの観点によって異なりますが、Linuxにはstraceltraceperfgdbvalgrindtcpdumpiftophtopなど多くの診断ツールがあります。Windowsにも診断ツールはあるものの、Linuxの方が多くのツールが利用でき、また多くの情報が公開されているため、OSとしてのトラブルシュートが容易になります。

C#サーバーとして考えたとき、Linuxではdotnet-countersdotnet-tracedotnet-dumpdotnet-gcdumpなどの診断ツールが利用できます。これらのツールを用いてメモリリークやパフォーマンス問題時にメモリダンプを取得し、必要ならWindowsで解析もできます。必要ならリモートデバッグも可能なので、Linuxだからといってトラブルシュートの道が閉ざされることはありません。

Windowsが好ましいケース

ここまでLinuxで動かす理由を挙げてきましたが、Windowsで動かす方が好ましいケースもあります。例えば以下のようなケースです。

  • 自前データセンターやオンプレでWindowsサーバーが安く調達できる
  • Windows用ミドルウェアを利用している (あるいはActive Directory連携など)
  • .NET Frameworkを利用している (.NET (Core)でないとLinux動作がサポートされていない)

特に業務システムや官公庁システムには該当することもあるでしょうし、環境を刷新しない選択をしている場合もあるでしょう。この記事はLinuxで動かす理由を挙げたものであり、Windowsで動かすことを否定するものではありません。.NETになっても、Windowsで安定して動作するのはC#サーバーの利点です。

まとめ

C#サーバーはLinuxで動かすのが第一選択になった理由を説明しましたが、Windowsを用いるケースも含めて向いているケースを例示してみます。あくまでも一例なので、ご自身の選択を尊重してください。

選択肢 向いているケース
Linux コスト重視、コンテナ利用、最新.NETを利用する、エコシステム活用(OSSツールやFaaS/CaaSが標準的に利用可能)
Windows オンプレWindows安価調達、AD/Windowsミドルウェア連携、.NET Frameworkを利用する

  1. 過去に.NET Coreと呼ばれていたこともありますが、.NET 5以降は単に.NETと呼ばれています。
  2. 料金ページにある通り、おおむねライセンス費用と考えても差し支えないでしょう。スポットインスタンスやリザーブルドインスタンスに相当する利用条件でも変わります
  3. アプリケーションとランタイム依存関係のみを含む超小型Ubuntuと言えます。https://canonical.com/blog/chiselled-ubuntu-ga
  4. C#自身でリバースプロキシを実装するYARPというライブラリもありますが、既存のプロキシを利用するのが第一候補になるのは他言語と変わりません。
  5. 主要なFaaSのサポート状況: AWS LambdaGoogle Cloud FunctionsAzure FunctionsOCI Functions
  6. TechEmpowerのベンチマークは、ベンチマークハックがやりやすく以前のコードはあまり参考になりませんでしたが、現在は素のシンプルなコードになったので健全なベンチマークと言えます。
  7. dotnet/runtime - Issuesdotnet/aspnetcore -Issues

コードと記事に集中できるはてなブログの新テーマ「CodeFocus」を公開しました

長い間テーマストアで公開されているテーマを使ってはてなブログを書いてきましたが、ここ2,3年は他の技術記事サービスに比べてスタイル的に読みにくいなぁと感じていました。そこで、自分にとって技術記事を読みやすいはてなブログテーマCodeFocusを作成しました。

CodeFocus

CodeFocus自体の紹介はテーマの紹介記事カスタマイズ記事で書いたので、ここではなぜCodeFocusを作成したのかを書いていきます。

技術記事を読みやすいテーマを作ってみたい

数年ブログを書いていると、ブログの流行りや飽きでテーマを変えたくなることがあります。QiitaやZenn、dev.toなど技術ブログを書くためのサービスが増え、記事が書きやすいだけでなくはてなブログよりも読みやすく感じるサービスが増え、毎日読んで刺激を受けていることも影響している気がします。

はてなブログはテーマストアに公開されているテーマのおかげでサクッと見た目を変更できます。ただ、技術記事の読みやすさというよりシンプル、レスポンシブ、写真の見栄えを重視したテーマが多いと感じます。もちろん技術記事を書くことを想定したテーマもあるのですが、好みとばっちり合うものがなく何かしら割り切ってました。そもそもはてなブログは技術ブログだけを対象にしたサービスではないため、Qiita・Zennと同等の読みやすさを求めるのも酷な話ですしね。

ちょうど生成AIが自分で盛り上がっていたこともあり、はてなブログで技術記事を書きやすく・読みやすいテーマを作ることにしました。あまり万人向けは目指していません。

技術記事を書きやすい、読みやすいとは?

ぼんやりとはてなブログよりも読みやすいサービスが増えたと書きましたが、技術記事を読みやすいとはどういうことでしょうか? CodeFocusを作成するにあたり、Zenn、Note、dev.toなどの記事スタイルがなぜ読みやすいのか調査しました。以下に各サービスの特徴をまとめます。

Zenn

Zennは、PC/タブレットで2カラム、スマホでシングルカラムです。2カラムだと右カラムに目次が固定されており、シングルカラムだとヘッダーに目次が固定されます。また目次は現在の見出し箇所を追随してハイライト表示します。目次にいつでもアクセスでき、記事全体に対して今どこにいるかが直感的に分かるので、記事が長くなっても読みやすさを保っています。

本文は全体的にシンプルなデザインで、1行あたりの高さが広く、段落間や見出しh1-6の間に十分な余白を設けています。また、見出しレベルh1・h2で下線を引いています。本文中のリンクは青色でリンクを意識しやすくなっています。技術記事は引用が多くなりやすいので、リンクが見やすいのは必要な配慮だと感じます。

個人的にコードブロックが好みです。背景が十分に濃い色で、コードのハイライトも見やすくなっています。さらにコードブロックの折り返しがデフォルトで無効になっており、スマホでもコードが読みやすいです。折り返しのON/OFF切り替えが可能で、コードブロックコピーもあるので困ったことがありません。

  • 余計な装飾がなく、見出し装飾もシンプル
  • タイトルは中央寄せ
  • PCは記事本文が708px、タブレットが517px、スマホが90%幅と、はてなブログよりも広めにとっている
  • 目次に記事の途中でアクセスでき、いつでもアクセスできる/どこまで読んだか分かる
  • 本文の色合いは白黒ベースメインでリンクが青色というシンプルな色合い
  • 本文以外のリンクは黒色で、リンクの色は青色で目に留まるように
  • 行の高さが1.9と広く、段落間も広くセクションの違いが分かりやすい
  • コードブロックセクションの背景色が濃く、ハイライトの色も見やすい
  • コードブロックの折り返しがデフォルト無効でスマホでコードが折り返されず読み下しやすい(ON/OFF切り替え可能)
  • 警告や注意書きのセクションが存在し目立つ

Zenn

Note

Noteは、事実上のシングルカラムと言えます。PCで左側に著者プロフィールが表示されますが、ほぼ目に入らないでしょう。目次ががなく本文ナビゲーションは重視していないようです。

本文はシンプルなデザインで、1行あたりの高さが広く、段落間や見出しh1-6の間に十分な余白を設けています。また、見出しレベルで下線は一切引いていません。本文中のリンク色が本文と変わらない代わりに下線が引かれています。

Noteはたくさんの記事がありますが、技術記事の割合は少ないようです。調べてみると「目次機能がない」「コードブロックのハイライトが乏しい」「本文スペースが狭いため縦に伸びがち」など、技術記事が読みにくく感じます。通常の文章だと特に読みにくく感じないのですが、技術記事だとちょっと弱く感じました。

  • 余計な装飾がなく、見出し装飾もない
  • タイトルは左寄せ
  • PCは記事本文が620px、タブレットが460px、スマホが100%幅と、はてなブログよりも広めにとっている
  • 目次がなく、記事の途中でどこまで読んだか分からない
  • 本文の色合いは白黒ベースメインでリンクも黒というシンプルな色合い
  • タイトルと本文以外のフォントは小さく目立たせない
  • 本文下の他記事の区切りがシンプルにまとまっている
  • 行の高さが1.5と狭め、段落間は広くセクションの違いが分かりやすい
  • コードブロックは黒背景、コードハイライトは4色
  • コードブロックの折り返しがデフォルト無効でスマホでコードが折り返されず読み下しやすい(ON/OFF切り替えなし)
  • 警告や注意書きのセクションがない

Note

個人的に、本文下の「他記事へのリンク」セクションの表示が揃っていて好みです。タブレット以上の画面サイズで2列表示、それぞれは上から順に画像、タイトル、著者+日付、お気に入りなどが表示されます。スマホで1列表示、左にタイトルとメタ情報、右に画像が表示されます。目が留まりやすいです。

タブレット表示

スマホ表示

dev.to

dev.toは、PC/タブレットで2カラム、スマホでシングルカラムです。2カラムだと右カラムにプロフィールと広告が固定されています。目次はありません。

本文はシンプルなデザインで、1行あたりの高さが狭く、段落間や見出しh1-6の間に十分な余白を設けています。また、見出しレベルで下線は一切引いていません。本文中のリンクは青色でリンクを意識しやすくなっています。dev.toの記事は見出しの頭に絵文字を使っている記事が多いので、下線がなくても以外と目に留まります。

個人的にコードブロックを全画面にするボタンが興味深いです。また、ZennやNoteと違って、基本的にすべて左寄せしています。ログイン以外は左寄せで、英語サイトあるあるな統一感です。

  • 余計な装飾がなく、見出し装飾もない
  • タイトルは左寄せ
  • PCは記事本文が748px、タブレットが584px、スマホが90%幅と、はてなブログよりも広めにとっている
  • 目次がなく、記事の途中でどこまで読んだか分からない
  • 本文の色合いは白黒ベースメインでリンクが青色というシンプルな色合い
  • タイトルと本文以外のフォントは小さく目立たせない
  • 本文下の他記事の区切りがシンプルにまとまっている
  • 行の高さが2と広め、段落間も広くセクションの違いが分かりやすい
  • コードブロックは黒背景、白文字ベース。コードハイライトは最小限でコードは読みにくい
  • コードブロックの折り返しがデフォルト無効でスマホでコードが折り返されず読み下しやすい(ON/OFF切り替えなし)
  • 警告や注意書きのセクションがない

dev.to

他の気になるサイト

MagicOnionのドキュメントサイトは、3カラム構成、PCで記事本文が958pxとかなり広めです。幅が広くても読みやすいです。本文の高さが設定されていませんが、セクション間の幅は設定されているので全体的にきゅっとしている印象があります。窮屈ではないので、割と好みです。

MagicOnionドキュメント

Microsoft Learnは、3カラム構成、PCで記事本文が852pxと広めです。本文の高さは160%です。全体的に一貫性があり、まとまっている印象を受けますね。

Microsoft Learn

Google Cloudブログは、事実上のシングルカラム構成、PCで記事本文が842pxと広めです。本文の高さは1.5です。タイトルが中央寄せなのは面白いですね。

Google Cloudブログ

個人サイトのいくつかを見渡すと、割と長年スタイルは変えていないようなのでここでは割愛します。

CodeFocusのデザイン

自分が感じる技術記事を読みやすいテーマの要素を抜き出してみましょう。

シングルカラムか2カラム1、本文やリンクの色、行の高さ、段落間の余白、見出しの装飾(h1/h2にあったほうがよさそう)、コードブロックの背景・ハイライトの色、コードブロックの折り返し、目次の有無、タイトルと本文以外は目立たせない、などが読みやすさに影響してそうです。特に技術記事ではコードブロックのハイライトと折り返しはスマホで必須のようです。記事が長くなるなら、目次を使いやすくするのも良さそうです。個人的に、Zennのデザインが割と納得感あり、Noteやdev.toの好きなところを取り入れるのがよさそうです。

これを踏まえて、CodeFocusでは以下のようなデザインを採用しています。なお、はてなブログのテーマストアはCSSのみ配布なので、JavaScriptによるカスタマイズをしなくてもデザインは整えつつ入れたい機能を追加できるようにしています。

  • PCでもシングルカラムにする
  • 余計な装飾を設けず、h1/h2見出しだけ下線を引く
  • 記事セクションの広さをPCで850px、タブレットが790px~700px、スマホは100%(余白あり)と広めに
  • はてなブログの[:contents]で表示される目次のコントロールを追加
    • 目次のトグルを導入して目次をクリックで開閉できるように。目立たないようデフォルトは閉じるように
    • JavaScriptカスタマイズでフロート目次を導入可能に、現在の見出し箇所を追随してハイライト表示
  • 本文の色合いは白黒ベースメインでリンクを青色に
    • 本文以外のリンクは黒色、太字リンクは下線を引かない、通常リンクに下線を引く
  • 行の高さは広めに1.9、段落間や見出しh1-6の間に十分な余白を設ける
  • コードブロックセクションの背景色を濃く、ハイライトも見やすく
    • コードハイライトライブラリは導入せず、はてなブログのコードハイライトを利用
  • JavaScriptカスタマイズでコードブロックの折り返しをON/OFF導入、デフォルトで折り返し無効に
  • 警告や注意書きのセクションはなし。独自記法を設けたくない
  • 本文下の他記事の区切りをシンプルにまとめる
  • JavaScriptカスタマイズでダークテーマを導入
  • あくまで技術記事なのでタブやグローバルナビゲーションは用いない

追加機能

CodeFocusはJavaScriptで追加できる機能を用意しています。 JavaScriptカスタマイズをしなければ機能は追加されませんが、必要な人はどうぞという感じです。はてなブログのテーマストアでテーマ入れるだけだと導入されないので、割と使われることは少なそうですが、自分がやりたいだけなのでよし。

目次

本文目次のトグル

はてなブログ組み込みキーワードである[:contents]で表示される目次を、クリックで開閉できるようにします。デフォルトは閉じており、目立たないようにしています。 目次って便利なんですが、記事が長くなるほど本文トップを占有して微妙だと感じていました。目次をクリックで開閉できるようにすることで、目次を必要なときだけ表示できるようにしました。 トグルを閉じておくと割とほとんどの人は開かない模様、という認識も背景にあります。

はてなブログはデフォルトで設けてほしい。

目次のトグル

フロート目次

[:contents]で目次を表示している場合、フロート目次を追加できるようにしました。 Zennや他ドキュメントサイトで本文が長いときに、目次が固定されていると便利だと感じていました。実際便利。

はてなブログはデフォルトで設けてほしい。

フロート目次

コードブロック

コードブロックの折り返し

スマホで技術記事を読んでいてつらいのが、コードブロックの折り返しです。はてなブログがそうですが、折り返されると地味に読みにくくてずっと不満でした。 CodeFocusは、コードブロックの折り返しON/OFFを切り替えられるようにしました。デフォルトは折り返し無効で、スマホでもコードが読みやすいです。

はてなブログはデフォルトで設けてほしい。

折り返しは読みにくい

折り返しなしは読みやすい

コードブロックのコピー

ずっと以前からコードブロックにあると便利なのが、コピーボタンです。 割とコピー&ペーストで手元に持ってきて実行してみることは多いので、CodeFocusではコードブロックの右上にコピーボタンを追加しました。 コードブロックが長かったりすると、コピーしそこねとかよくあるんですよね。

はてなブログはデフォルトで設けてほしい。

コードブロックのコピー

ダークテーマ

ダークテーマは是非に議論があると認識しています。ダークモード:ユーザーの考え方と避けるべき課題は割とそうだなぁと同意するところも多いです。 とはいえ、今現在においてシステムテーマでライト/ダーク(あるいはティルト)を設定できる流れは広がっており、任意で選択できるようになりつつあります。私自身、iOSでは時間でダークテーマ・ライトテーマを切り替えています。

CodeFocusはシステムテーマを尊重しつつ、ダーク/ライトテーマを切り替えられるようにしています。 JavaScriptカスタマイズしなければダークテーマは適用されないので、使いたい人はどうぞという感じで。

ダークテーマ

生成AIを使う

CodeFocusは、GitHub Copilot(Claude Sonnet 3.7Claude Sonnet 4)を使って開発しています。 CSSは苦手なのもあってテーマ作成を避けてたものの、生成AI使うことで完成までこぎつけられたのは良い体験でした。

SCSSを使っているうちにCSS苦手意識が消え、インストラクションベースでの開発に夢中になれたのも良かったです。

テーマのコードベース

はてなブログがボイラーテンプレートとなるテーマhatena/Hatena-Blog-Theme-Boilerplate公開しているので、それぞベースに組んでいます。えらい。

Vite + SCSSを使っており、割とシンプルな構成なのでいじるには最適でした。

インストラクション

インストラクションに、はてなブログのデザインテーマ制作の手引きを食わせています.ただ、CSS回りのチェックリストとして、これを食わせるのがベターかは微妙でした。「デザインカスタマイズの仕様」あたりが一番それっぽい箇所ですが、条件付きのセレクターが多く、AIにとってデザイン構成で考慮するには厳しい感じです。文章でしじするより仕様として与える必要があるのですが、組み込みされない傾向でした。デザインテーマの手引きよりも、はてなブログ公式として「はてなブログデザインのMCP」を公開してもらえると嬉しいなぁと感じます。

インストラクションには、生成AIの動作以外にデザイン仕様もすべて書き出しています。これをCopilotに食わせることで、デザインの意図を理解してもらい、CSSを調整しています。

Playwrightテストを組む

AIで組むにあたって真っ先に考えたのが、PlaywrightでのE2Eテストです。AIでデザイン調整していると、インストラクションにかかわらず割と破壊してきます。その対策にPlaywrightのテストをCopilotに書かせています。 PlaywrightでスペックテストやE2Eテストを組むことで、変更でCSSが壊れていないかAIにフィードバックを与えます。

これでデザイン変更後はテストが通るまで修正が続くので、破壊されるケースは減りました。割とうまくいったのでおすすめです。 デザインに対するテスト以外に、JavaScriptカスタマイズの機能ごとのテストも組むことで、安心して機能追加や変更ができるようになります。

playwrightテスト

テーマストアのキャッチ画像

テーマストアのキャッチ画像もAIに書かせています。 キャッチ画像は何度も調整を繰り返すので再現性が欲しくて画像生成は避けました。代わりに、CopilotがHTMLでキャッチページを用意してPlaywrightで自動的にスクリーンショットを撮るのを提示してきたので採用しています。

他のテーマを見ると、テーマストアのキャッチ画像は実際のブログスクリーンショットを埋めているケースが多いようです。 幸いE2Eテストでスクリーンショットを撮るので、テストに使った画像をテーマストアのキャッチ画像のHTMLに埋め込むだけで済みました。 レイアウトもHTMLで組んでいるので、修正はCSSを調整するだけで済みました。便利。

紹介記事

AIに紹介記事の下書きを書かせて、それを手直しする形で書いています。

実際に書いてある内容があってるかは絶対譲れない部分なので、AIに書かせた内容をそのまま使うことはできないんですよね。 ただ、AIが書いた内容をベースに手直しすることで、記事を書く時間を大幅に短縮できました。構成も私が考えるより丁寧だったのでAIに書かせてよかったです。

まとめ

自分にとって技術記事を書きやすく・読みやすいはてなブログテーマを作成しました。 もし気に入って使ってもらえるなら、うれしいのでぜひ。

インストールはテーマストアからどうぞ。

参考記事

Zenn記事

Note記事

dev.to記事


  1. 本文以外は目立たせない