しばやん雑記

Azure Serverless とメイドさんが大好きなフリーランスのプログラマーのブログ

アプリを Azure Government と Azure China に対応させつつ ARM Template でデプロイする

日本在住であれば関わる機会がほぼないと思われる Azure Government と Azure China ですが、OSS で Azure 向けアプリを出していると稀に対応せざるを得ないときがあります。

今回は App Service と Key Vault の Acmebot で対応する機会がやってきました。普通ならまず弄らない部分なので、Issue が上がってきた時から興味がありました。対応した PR は以下になります。

完全に興味ドリブンでの対応だったので、この知識が今後役に立つことは 100% 無いし期待もしていませんが、面白かったのでブログネタとして昇華することにします。

ちなみに自分では動作確認していません、というか出来ません。多分動く書き方の紹介です。

そもそも Azure Government / Azure China とは

それぞれグローバル版 Azure とは異なり完全に別環境に展開された Azure のことですが、Azure Government は Microsoft が直接運用しているのに対して Azure China は 21Vianet が運営する形になっています。

無料アカウントを簡単に作れそうな雰囲気がありますが、連邦政府関係者かどうかのチェックが入るため普通に日本で仕事していると絶対に作れません。

f:id:shiba-yan:20200818210310p:plain

同様に Azure China も、アカウント作成のハードルがめちゃくちゃ高いのでまず無理です。

ぶっちゃけ存在を知らなくても問題なく Azure を使うことは出来ます。連邦政府関係や中国国内限定の会社とお仕事する場合には知っておいた方が良いでしょうが、そんな機会はまず来ない。

環境による差異を理解する

グローバル版 Azure とは完全に別になっているので、Azure Government / Azure China 共にほぼ全てのエンドポイントが別で用意されています。このあたりの対応さえしてしまえば勝ちという感じです。*1

それぞれのドキュメントに差異が記載されていますが、新しいサービスは抜け落ちているようです。

見事なほどに一貫性のないエンドポイントになっているので、何らかの方法で各エンドポイントの情報を取得するか保持しておく必要があります。Azure China の方が僅かにブレが少ないです。

Azure CLI や Azure PowerShell では一部の情報を取得出来ます。ARM に API が用意されているようです。

適当に Active Directory のエンドポイントだけ絞り込むようにすると、以下のように 4 つ返ってきます。ちなみに AzureGermanCloud はグローバル版がデプロイされたのでお役御免です。

f:id:shiba-yan:20200814011027p:plain

この辺りの差異をデプロイ時やアプリケーション側で吸収するように作ることで、Azure Government と Azure China への対応が行えます。正直かなりめんどくさいです。

実際の対応については、この後から適当に必要だった部分ごとに書いていきます。

接続文字列を設定するサービス (Storage / SQL など)

Azure は接続文字列をめちゃくちゃ使うサービスなので、普段から Azure Storage や SQL Database などで使っているはずです。その場合は接続文字列にエンドポイントの情報が含まれているので対応は不要です。

具体的に Azure Storage で見ると EndpointSuffixcore.windows.net という値が設定されます。

なので接続文字列を使っていれば Azure Government / Azure China であることを意識することなく、アプリケーション側の変更はほぼ無しで行けます。

接続文字列が提供されていないサービスの場合は、個別にエンドポイントを意識してコードを書く必要があるので結構面倒です。最近は Managed Identity を使うことで接続文字列が不要になってきているので、さらに意識する必要が出てきました。

Azure AD / Managed Identity

最近は Managed Identity を使って RBAC で必要な権限だけ割り当てることが多いです。

Managed Identity を使ってアクセストークンを取るのは App Service や IMDS が自動でやってくれますが、どのリソースに対してのトークンが必要かどうかは指定する必要があります。

本来なら GetAccessTokenAsync に必要なリソース ID を渡すだけで良さそうですが、AzureServiceTokenProvider のコンストラクタには azureAdInstance という引数でグローバル版 AD のエンドポイントがデフォルトで設定されているので、これをオーバーライドしておきます。

// Azure Government の Resource Manager 用 Access Token を取得
var tokenProvider = new AzureServiceTokenProvider(azureAdInstance: "https://login.microsoftonline.us");

var accessToken = await tokenProvider.GetAccessTokenAsync("https://management.usgovcloudapi.net");

この辺りはドキュメントなどが全然なくて結構厳しいです。とはいえこれ以上設定出来る部分はないです。

Azure Resource Manager

Azure SDK を使って各リソースを弄る場合にはデフォルトのエンドポイントがグローバル版 Resource Manager になっているので、明示的に必要なエンドポイントを指定する必要があります。

例えば Azure Government の App Service に対して操作を行う場合は、以下のように初期化します。

// Access Token は Managed Identity で取得したものを使う
var credentials = new TokenCredentials(accessToken);

// BaseUri を Azure Government の Resource Manager 向けに設定する
var webSiteManagementClient = new WebSiteManagementClient(new Uri("https://management.usgovcloudapi.net"), credentials);

現在の Azure SDK には必ず baseUri というパラメータがあるので、それを指定すれば良いです。

ここまでエンドポイントを直接指定してきましたが、それぞれの Azure 環境に対応しようとすると、オプションなどで変更可能にしておく必要があります。これがまた面倒ですが、結局は ARM Template でのデプロイ時に設定しつつ、アプリにエンドポイント情報を持たせる方法にしました。

ARM Template

全く知らなかったのですが、ARM Template にはデプロイ先の情報を取得するための関数が用意されていました。これを使うことで環境名や一部のエンドポイントをデプロイ時に取得できます。

何故か全てのエンドポイントが網羅されていないので、これで全て解決することは出来ないのが厳しいです。今回は Azure Functions を使うアプリケーションでしたが、考え方は基本的に同じです。

Azure Functions

元々の Issue には Azure Functions のデプロイに失敗するとあり原因が良くわからなかったのですが、世の中に出回っている Azure Functions の ARM Template は Azure Storage の EndpointSuffix を考慮していないものなので、グローバル版 Azure にしかデプロイ出来ない代物でした。

Azure Storage にアクセス出来ない場合には Azure Functions 自体のデプロイが失敗するというのは学びですね。原因はシンプルなので、以下のように environment 関数を使って EndpointSuffix を埋めてあげれば動くようになります。

"appSettings": [
  {
    "name": "AzureWebJobsStorage",
    "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountId'), '2018-11-01').keys[0].value, ';EndpointSuffix=', environment().suffixes.storage)]"
  },
  {
    "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
    "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(variables('storageAccountId'), '2018-11-01').keys[0].value, ';EndpointSuffix=', environment().suffixes.storage)]"
  }
]

これで Azure Functions のデプロイが成功します。アプリケーションが実際に動くかどうかは別ですが。

Application Insights

Azure Storage は簡単でしたが、問題は Application Insights です。元々はグローバルにしか対応していないライブラリでしたが、接続文字列を導入することでそれ以外の環境にも対応するようになりました。

しかしここでも命名に一貫性がない弊害が出てきます。ドキュメントには EndpointSuffix としては Azure Government と Azure China 向けの 2 つが有効な値と記載されています。

さらに Application Insights のエンドポイントは environment で取れないという絶望的な状況でしたが、ドキュメントが実は間違っていたことに気が付いたので、ARM Template レベルで解決しました。

グローバル版の Azure では applicationinsights.azure.com というエンドポイントがひっそりと有効になっていたので、これを使って環境名をキーに切り替えるようにします。

"variables": {
  "appInsightsEndpoints": {
    "AzureCloud": "applicationinsights.azure.com",
    "AzureChinaCloud": "applicationinsights.azure.cn",
    "AzureUSGovernment": "applicationinsights.us"
  }
}

エンドポイントのマッピングは上のように用意しつつ、実際に Application Insights の接続文字列を作っている部分で EndpointSuffix を追加するようにします。

地味に ARM Template で Key-Value な変数を扱うのはやったことがありませんでした。

"appSettings": [
  {
    "name": "APPLICATIONINSIGHTS_CONNECTION_STRING",
    "value": "[concat('InstrumentationKey=', reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2015-05-01').InstrumentationKey, ';EndpointSuffix=', variables('appInsightsEndpoints')[environment().name])]"
  }
]

多分これで 1 つの ARM Template で Azure Government と Azure China へデプロイ出来るようになりました。

無駄な知識を手に入れてしまった感はありますが、ARM Template 周りのノウハウは溜まった気がします。とはいえ接続文字列周りは Terraform を使うと解消されるので、ARM Template で頑張る必要はありません。

*1:例外的に Azure CDN と Front Door に関しては同じエンドポイントが使われている気配