Skip to content

Latest commit

 

History

History
563 lines (400 loc) · 25 KB

File metadata and controls

563 lines (400 loc) · 25 KB

CONFIGURATION.md — 設定ファイルの扱い

対象: root/programs 配下(CS / VB、net48 / net10.0) 配置: root/programs

本書は**「設定がどう読まれ、どう上書きされ、どこで踏むか」**を扱う。 **個々のキーが何を意味するかは書かない。**それは値の隣(各設定ファイルのコメント)にある。


0. どこに何を書くか

設定ファイルは 86 個ある。全部に同じ説明を書くことはできないし、書けば必ずずれる。

書く場所
仕組み(読み込み順・上書き・展開・落とし穴) 本書だけ 「環境変数は __ 区切りで上書きする」
そのキーが何か(値の意味・既定値・選択肢) 値の隣のコメント FxSqlTraceLog : D層のパフォーマンス ログ出力の on / off」
そのサンプル固有の事情 そのサンプルの ANALYSIS.md / README.md Legacy/* は相対パスで書く」

設定ファイルに仕組みを書き足したくなったら、本書に書いて、そこから参照する。 一部のファイルにだけ詳しい説明がある状態は、書かれていないファイルを「該当しない」と 誤読させる。(実際、86 個中 3 個にだけ詳細が書かれている状態になっていた)


1. 設定ファイルの種類

net48 net10.0
デスクトップ・バッチ・CLI app.config appsettings.json
Web Web.config(+ app.config appsettings.json(+ appsettings.{環境名}.json

net48 の Web では、Web.configappSettings別ファイルへ委譲していることがある。

<appSettings file="app.config"/>

このときキーの実体は app.configにある。Web.config だけ見て「無い」と判断しない。

net10.0 では初期化が要る

GetConfigParameter.InitConfiguration(configuration);   // Startup のコンストラクタ

これが無いと Fx* 系の設定がすべて null になる。 net48 は暗黙に読まれるので不要。


2. セクション名の流儀

appSettings / connectionStrings というセクション名を、JSON でもそのまま使う。

{
  "connectionStrings": { "ConnectionString_SQL": "..." },
  "appSettings":       { "FxXMLSPDefinition": "..." }
}

ASP.NET Core の標準(ConnectionStrings / 任意のセクション)とは綴りが違う。 GetConfigParameter がこの名前で引くため、変えると読めなくなる。

appsettings.json はコメント付き JSON(JSONC)で書いてよい。 Microsoft.Extensions.Configuration.Json// を許容する。 ただし厳密な JSON パーサで読むと壊れるので、外部ツールに食わせるときは注意。


3. 読み込み順と優先順位(net10.0)

Host.CreateDefaultBuilder が次の順に読む。後が勝つ。

appsettings.json → appsettings.{環境名}.json → user-secrets(Development のみ)
  → 環境変数 → コマンドライン

{環境名}ASPNETCORE_ENVIRONMENT の値。未指定なら Production

appsettings.{環境名}.json は「キー単位」の上書きであって、丸ごと差し替えではない。 書いたキーだけが上書きされ、書いていないキーは appsettings.json の値が生きる(実測)。

実験 結果
appsettings.Container.json 無し(Production) 200
有り + ASPNETCORE_ENVIRONMENT=Container 500(1 キーだけ壊した値を書いた。読まれている)
同じファイル + 環境名なし 200(読まれない)

環境名を変えると副作用がある。 Development にすると env.IsDevelopment() が真になり、 開発者例外ページが出る(本番向けの UseExceptionHandler / UseHsts 側に入らない)。 実例では Container という名前を使っている。


4. 環境変数で上書きする

4-1. __(下線 2 つ)区切り

appSettings__FxXMLSPDefinition=/app/files/resource/Xml/SPDefinition.xml
connectionStrings__ConnectionString_SQL=Data Source=db;...

__:(セクション区切り)に読み替えられる。上の 3 節のとおり、環境変数は appsettings.json にも appsettings.{環境名}.json にも勝つ。

Startup が受け取った IConfiguration をそのまま InitConfiguration へ渡しているため、 フレームワーク側から読む値にも効く。

Docker / Kubernetes の secrets も同じキー名で渡せる。 Microsoft.Extensions.Configuration.KeyPerFile を足し、 builder.Configuration.AddKeyPerFile("/run/secrets", optional: true) とすると、 ファイル名がそのまま構成キーになる__ がセクション区切りとして効く)。 Host.CreateDefaultBuilder は既定では読まないので、1 行足す必要がある。

4-2. FxContainerization は別の仕組み

混同しないこと。 こちらは ON のとき、接頭辞なしのキー名で環境変数を読む。

// Public/Util/GetConfigParameter.cs : CheckContainerization
if (containerization.ToUpper() == PubLiteral.ON)
{
    return System.Environment.GetEnvironmentVariable(key);   // 例: FxXMLSPDefinition
}
読むもの 有効化
appSettings__ 方式 appSettings__FxXMLSPDefinition 不要(既定で効く)
FxContainerization FxXMLSPDefinition FxContainerization=ON

net10.0 のコンテナで使っているのは前者。 FxContainerization は使っていない。


5. 設定値の中の %変数% はフレームワークが展開する

OS の機能ではない。Open棟梁 が自前で展開している。

// Public/IO/ResourceLoader.cs : ResolveFilePath
loadfilepath = StringVariableOperator.BuiltStringIntoEnvironmentVariable(loadfilepath);

% で分割して Environment.GetEnvironmentVariable(名前, Process) を引くだけの実装で、 Windows の書式に依存しない。Linux コンテナでも効く。

"FxXMLSPDefinition": "%OT_RESOURCE_ROOT%/Xml/SPDefinition.xml"

効く範囲

ResourceLoader を経由してファイルを開くパスは、すべて展開されるResolveFilePath / Exists / LoadAsString のいずれも ResolveFilePath を通る)。

  • 定義 XML(FxXML*Definition
  • log4net / NLog の定義ファイルの場所FxLog4NetConfFile
  • SQL ファイル(SqlTextFilePath + ファイル名。BaseDam.Load2ResourceLoader.LoadAsString

TransactionControl / BaseController / *NameService は、BuiltStringIntoEnvironmentVariable を 明示的に呼んでいる。

ResourceLoader を経由しない読み方をしている箇所では効かない。 新しいパス系キーを足すときは、どちらの経路かを確かめること。

定義ファイルの「中身」は展開しない

Open棟梁 が展開するのは設定ファイルの「場所」までで、中身はライブラリへそのまま渡す。 したがって、ログ定義の中の変数はログ ライブラリの書式で書く。

<!-- log4net : PatternString の %env{}。<param name="File"> ではなく型付きの <file> -->
<file type="log4net.Util.PatternString" value="%env{OT_LOG_ROOT}/ACCESS" />
<!-- NLog : ${...} -->
<target xsi:type="File" name="ACCESS" fileName="${OT_LOG_ROOT}/ACCESS..." />

6. コンテンツ ルート(=プロセスの作業ディレクトリ)

appsettings*.json は、実行ファイルの場所ではなく「作業ディレクトリ」から読まれる。

作業ディレクトリが違うと、設定ファイルが一度も読まれないまま起動する。 それでも画面は出るので気付きにくい。起動ログで確かめられる。

info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\OpenTouryo\root\programs      ← 意図した場所か?
  • コンテナでは DockerfileWORKDIR /app を置く
  • dotnet <パス>\App.dll で起動するときは、出力フォルダを作業ディレクトリにする

7. パスの落とし穴

7-1. 大文字小文字(#550)

実フォルダは Xml / Log / Sql / Test / X509 XMLtest と書いても Windows では通り、Linux では開けない。

7-2. 絶対パス

サンプルは C:/root/files/resource/... を直書きしている(リポジトリを C:\root\ に 展開する前提)。別の場所に置くならビルドは通るが実行時に落ちる。 環境変数(4 節)か %変数%(5 節)で差し替える。

7-3. 相対パスの基準

相対パスはまず作業ディレクトリ基準で探し、見つからなければ AppContext.BaseDirectory(実行ファイルの場所)基準で再探索する (ResolveFilePath)。CLI が任意の場所から起動される場合への対応。

Samples4NetCore/Legacy/* は、この仕組みに乗ってファイル名だけを書いている。

"FxXMLSPDefinition": "SPDefinition.xml"

csproj の CopyToOutputDirectory で出力フォルダへ配る前提なので、 %変数% 化すると壊れる。


8. HTTPS 関連 — 「検知」と「宣言」

リバース プロキシで TLS を終端すると、アプリから見た接続は HTTP になるRequest.IsHttps(net48 は IsSecureConnection)は false のままで、 ブラウザは HTTPS なのに Cookie に Secure が付かない(#549)。

対処は 2 通りある。

方針 手段(net10.0) 手段(net48)
検知(実態を知らせる) UseForwardedHeaders IIS の URL Rewrite で HTTPS を立てる
宣言(そう決め打つ) CookieSecurePolicy=always formsrequireSSL="true"

どちらも実測してあるX-Forwarded-Proto: https を付けた要求で Set-Cookie を見る)。

検知 宣言
net10.0 4 件中 3 件 4 件中 4 件
net48 6 件中 2 件(=フレームワークが立てる SessionTimeOut の生成・削除の両方)

net10.0 の検知が 3 件止まりなのは、削除用 Cookie が CookieExtensions.Delete(key)(オプション無し)を通るため。 net48 側は削除も newCookie.Secure を明示しているので、両方に付く。

net48 では allowedServerVariables への登録も要る。 Web.config からは設定できず(overrideModeDefault="Deny")、 applicationHost.config に書く。忘れると要求ごとに HTTP 500.50 になる。

宣言の方が依存が少なく確実。 CookiePolicy はすべての Set-Cookie を後段で 上書きするため、個々の箇所が何を判断しているかに依存しない。

検知を選ぶなら 3 点が要る

内容 欠けると
① 前段が伝播する X-Forwarded-Proto: https を付ける 何も起きない
アプリが受け取る設定になっている UseForwardedHeaders 付いていても捨てられる
③ 付けられるのが前段だけ ネットワークで閉じる/KnownProxies クライアントが詐称できる

KnownIPNetworks / KnownProxies を設定しないと黙って無視される。 既定ではループバックからの転送しか信用しない。 「on にしたのに直らない」の原因はほぼこれ。

何を矯正でき、何ができないか

分かれ目は「リクエストごとに変わるか」。

情報 性質 内部パラメタで矯正できるか
スキーム(HTTPS か) 環境ごとに固定 できるCookieSecurePolicy / requireSSL
配置パス(サブパス) 環境ごとに固定 できるapp.UsePathBase
外部から見た URL 環境ごとに固定 できる(設定に絶対 URL を書く)
クライアント IP リクエストごとに変わる できない(伝播が必須)
クライアント証明書 リクエストごとに変わる できない(伝播が必須)

クライアント IP は既に動く。 GetClientIpAddressMyBaseAsyncApiControllerX-Forwarded-Forヘッダから直読みしており、UseForwardedHeaders に依存しない。

宣言のトレードオフ

間違えるとはっきり壊れる。 本当に平文 HTTP の環境で always にすると、 Antiforgery の Cookie が返らずログインの POST が 400 になる(実測)。

ただしこれは利点でもある。

間違えたときの現れ方
検知 静かに効かない。 画面は動き、Secure だけが付かない
宣言 はっきり壊れる。 すぐ気付いて直せる

net48 で「検知」する — IIS の URL Rewrite

Request.IsSecureConnection はコードから変えられない(読み取り専用)。 IIS 側で HTTPS サーバ変数を立てる。

MVC でも Web Forms でも同じである。設定するのは IIS であって、アプリの実装ではない。

Web.config に受信規則を書く

<system.webServer>
  <rewrite>
    <rules>
      <rule name="ForwardedProto">
        <match url=".*" />
        <conditions>
          <add input="{HTTP_X_FORWARDED_PROTO}" pattern="^https$" />
        </conditions>
        <serverVariables>
          <set name="HTTPS" value="on" />
        </serverVariables>
        <action type="None" />
      </rule>
    </rules>
  </rewrite>
</system.webServer>

action type="None" は「書き換えも転送もしない」。サーバ変数を立てるためだけの規則である。

applicationHost.configHTTPS を登録する

これを忘れると、すべての要求が HTTP 500.50 になる。

HTTP Error 500.50 - URL Rewrite Module Error.
The server variable "HTTPS" is not allowed to be set.
Add the server variable name to the allowed server variable list.
<rewrite>
  <allowedServerVariables>
    <add name="HTTPS" />
  </allowedServerVariables>
</rewrite>

Web.config からは設定できない。 allowedServerVariablesoverrideModeDefault="Deny" で、applicationHost.config にしか書けない。 アプリが勝手にサーバ変数を書き換えられないための関門である。

実測(IIS Express、Samples/WebApp_sample/MVC_Sample

送ったヘッダ secure が付いた Cookie
X-Forwarded-Proto: https 6 件中 2 件SessionTimeOut の生成・削除の両方)
(ヘッダ無し) 0 件
X-Forwarded-Proto: http 0 件(条件の ^https$ が弾く)

SessionTimeOutFxCmnFunction が立てる Cookie なので、 IsSecureConnection が true になったことの直接の確認になる。

ARR は要らない。 ARR は IIS 自身をリバース プロキシにする機能で、 ここで要るのは URL Rewrite だけ。前段が既にある構成なら ARR は無関係。

IIS Express で試すときの注意。 /path: 起動は %ProgramFiles%\IIS Express\AppServer\applicationhost.config を読む (%USERPROFILE%\Documents\IISExpress\config\... ではない)。 /config:/path: と併用できず、/site: が要る。

net48 で鍵を揃える — machineKey

Web サーバを複数台にするなら要る。 セッションを StateServer や SQLServer へ 外出ししても、それだけでは足りない。

認証 Cookie(forms)と ViewState は machineKey で保護されている。 指定しないとサーバごとに別の鍵が自動生成されるため、台数を増やした途端、 片方が発行したものをもう片方が復号できず、 ログインし直しや「ビューステートの検証に失敗しました」になる。

<machineKey validationKey="<同じ値>" decryptionKey="<同じ値>"
            validation="HMACSHA256" decryption="AES" />

ViewState を使う Web Forms では、より当たりやすい。 netcore 側の対応物は DataProtection(10 節)。


9. 秘密の扱い

サンプルはパスワードを直書きしている(すぐ動かせることを優先しているため)。 実際の環境では、次のいずれかで渡す。

net48 net10.0
別ファイルへ逃がす <connectionStrings configSource="..."/> appsettings.{環境名}.json
暗号化する aspnet_regiis -pef connectionStrings <パス>
環境変数 connectionStrings__ConnectionString_SQL=...
開発機のみ dotnet user-secrets(Development のみ読まれる)
コンテナ secrets + AddKeyPerFile(4-1 節)
そもそも持たない Integrated Security=SSPI 同左

証明書は「パスワードを無くす」選択肢がある。 PFX + パスワードではなく PEM + 秘密鍵ファイルにすると、管理対象が 1 つ減る。

dotnet dev-certs https --format Pem -ep ./https/aspnetapp.pem -np

10. net48 / net10.0 の対応表

net48 net10.0
ファイル app.config / Web.config appsettings.json
環境別 (なし。configSource で分ける) appsettings.{環境名}.json
初期化 暗黙 InitConfiguration() が必須
環境変数での上書き (標準では無い) __ 区切り
Cookie の Secure を必ず立てる requireSSL="true" CookieSecurePolicy=always
転送ヘッダの取り込み IIS の URL Rewrite UseForwardedHeaders
鍵の共有(複数台) machineKey DataProtection(DataProtectionKeyPath
セッションの外出し sessionState mode="StateServer" ほか AddDistributedSqlServerCache ほか

11. CS 版と VB 版で、どこが違ってよいか(#553)

Samples の設定ファイルは CS / VB でほぼ同じだが、完全には同じでない。 「同じはずだから」とコピーすると、VB 固有の記述が消える。(実際に消した。#549 の作業中)

突き合わせの結果(#553 時点の実測)

対応する設定ファイル 25 組を、XML の要素・属性で比較した (コメント・空白・改行・BOM の差は無視)。

組数
完全一致 8
差分あり 17

組数は必ず見ること。 対応づけは「CS からの相対パスを VB に当てる」方式なので、 階層が一致しないプロジェクトは 1 組も対象に入らない。 「差分 0 件」と「対象に入っていない」は、出力の上では区別がつかない。

差分の種類は次のとおり(1 組が複数に該当することがある)。

種類 組数 判断
startup / supportedRuntimeVB のみ 10 違ってよい(VS が VB プロジェクトに自動で入れる)
runtime/assemblyBindingbindingRedirect 8 違ってよい(NuGet が生成。参照するパッケージの版で変わる)
packages.config の構成 2 揃える(下記)
ClientSettingsProvider.ServiceUri(VB のみ) 1 違ってよい(VS のテンプレート由来)
SqlTextFilePath の値 1 違わないといけない(下記)
compilation/assemblies(VB のみ) 1 違ってよい(VB は明示参照が要ることがある)
接続文字列の末尾の ; 1 要確認ConnectionString_MCN。CS だけ ; が付く)
要素の順序だけ 1 無視してよい(MVC_Sample/app.config

違わないといけないもの — 埋め込みリソースの名前

Samples/2CS_sample/GenDaoAndBatUpd_sample/app.config

CS : <add key="SqlTextFilePath" value="GenDaoAndBatUpd_sample.Dao" />
VB : <add key="SqlTextFilePath" value="GenDaoAndBatUpd_sample" />

同じにすると壊れる。 埋め込みリソースの名前の作られ方が、言語で違うため。

マニフェスト名
C# <RootNamespace>.<フォルダ>.<ファイル名>
VB <RootNamespace>.<ファイル名>フォルダ名が入らない

RootNamespace はどちらも GenDaoAndBatUpd_sampleEmbeddedResource Include="Dao\..." も 同じである。違うのはコンパイラの規則の方なので、設定値で吸収するしかない。

SqlTextFilePath は「埋め込みリソースなら名前空間、通常のファイルならフォルダのパス」 という二役を持つ(MyBaseDao.SetSqlByFile2)。5 節・7-3 節も参照。

packages.config も突き合わせの対象である

拡張子が .config なので元から対象に入っている。$allowed には入れていない。

見るのは、陳腐化した宣言packages.config にしか出てこないパッケージ)と targetFramework の残留(プロジェクトの TargetFrameworkVersion と食い違う宣言)である。

どちらもビルドは通る。 だから検査でしか見つからない。

packages.config の見方(サテライト・コンテンツ・メタ パッケージの区別)は BUILDING.md 11 節。「参照が無い=不要」ではない。

揃えるときの原則

丸ごとコピーしない。 上のとおり、丸ごと同じにしてよいファイルは 25 組中 8 組しかない。

差分の行数を必ず見る。 コメントを 16 行足したはずが +23/−53 になっていたことで、 実際に事故に気付けた。

git diff --numstat

値が変わっていないことは、機械的に確かめられる。 CompareConfig.ps1 が、XML の要素・属性だけを取り出して突き合わせる。 コメント・空白・改行・BOM の差は落ちるので、値の差だけが残る。

cd root\programs
.\CompareConfig.ps1                               # 一覧
.\CompareConfig.ps1 -Detail -Only "WebApp_sample" # 内訳
.\CompareConfig.ps1 -Check                        # 合否(想定外の差分があれば 1)

-Check は「差分の有無」ではなく「想定外の差分の有無」を見る。 上の表で「違ってよい」とした種類(startup / runtime / compilation/assemblies / SqlTextFilePath / ClientSettingsProvider.*)は、当てはまれば想定内として扱う。

================ 判定 ================
  **想定外の差分 2 件**

  Samples\WebApp_sample\WebForms_Sample\WebForms_Sample\Web.config
    /configuration/connectionStrings/... [connectionString=...Database=test2;...]

許容する種類を増やすときは、なぜ違ってよいのかをこの節にも書くこと。 スクリプトの $allowed だけに足すと、理由が残らない。

そして、足す前に「なぜ違うのか」を履歴で確かめること。 「差がある」ことと「違ってよい」ことは別である。 理由が「片側だけ作業が済んでいない」なら、許容ではなく、作業を終わらせるのが答えになる。 $allowed に入れてしまうと、やり残しが「仕様」として固定される。

compilation/assemblies の欠落と接続文字列の ; は、この方法で見つけた。

CI は代わりにならない。 VB の疎通テストは 6 対象しか動かさないため、差分のあるファイルの半分以上は 一度も実行されない。 実行される分でも compilation/assemblies の欠落は 0_RunAll.ps1 -Lang Both を通過した(実測)。 CI は「壊れていないこと」の証拠にはなるが、「意図どおり」の証拠にはならない。


12. 実例はどこにあるか

見たいもの 場所
net10.0 の Web(本番向けの切り替えを含む) CS/Samples4NetCore/Backend/MVC_Sample/MVC_Sample/appsettings.json
環境変数・%変数%・環境別ファイルの実運用 CS/Samples4NetCore/Docker/(対象ごとに分かれる。MVC_Sample / ASPNETWebService
net48 の Web(machineKey / 転送ヘッダの注記) CS/Samples/WebApp_sample/MVC_Sample/MVC_Sample/Web.config
相対パスで自己完結させる書き方 CS/Samples4NetCore/Legacy/Bat_sample/*/appsettings.json
領域ごとの事情 ANALYSIS.mdFrameworks / Samples / Samples4NetCore