この記事について
VNET統合して、バックエンドのストレージアカウントはネットワークアクセス制限するという条件でAzureFunctionをTerraformで作ろうとしたのですが、下記のエラーに悩まされました
Error: reading logs configuration for Windows Function App: (Site Name "azfvnettest001" /
Resource Group "rg-azf-test-001"):web.AppsClient#GetDiagnosticLogsConfiguration:
Failure responding to request: StatusCode=503
-- Original Error: autorest/azure: Service returned an error.
Status=503 Code="ServiceUnavailable"
Message="The operation timed out and could not be completed. Please retry the action or try again later."
Details=[
{"Message":"The operation timed out and could not be completed.
Please retry the action or try again later."},
{"Code":"ServiceUnavailable"},
{"ErrorEntity":{"Code":"ServiceUnavailable",
"ExtendedCode":"51007",
"Message":"The operation timed out and could not be completed. Please retry the action or try again later.","MessageTemplate":"The operation timed out and could not be completed. Please retry the action or try again later."}}]
AzurePortalから確認するとAzureFunctionのリソースは出来上がっているのですが、URLにアクセスしても503エラーになってしまう状況です
ちなみに、正しく作成ができている状態でURLにアクセスするとこんな画面になります
しばらくはAzurePortalから手動で作成してからimportをしていたのですが、複数環境を作成するようになってきて、さすがに辛くなってきたので、解決策を検討することにしました。
最初に書いていたコード
あまり特別なことはしていないつもりで、次のことを実現したくて実装しました
- VNET統合をするためにサブネットの作成
- セキュリティを考慮してストレージアカウントはアクセス制限
- ElasticPremiumでよしなにスケーリング
#リソースグループ
resource "azurerm_resource_group" "rg" {
name = "rg-azf-test-001"
location = "japaneast"
}
#仮想ネットワークの作成
resource "azurerm_virtual_network" "vnet" {
name = "vnet-azf-test-001"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
address_space = ["10.0.0.0/16"]
}
#サブネットの作成
resource "azurerm_subnet" "subnet" {
name = "snet-azf-test-001"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.0.0.0/24"]
#AZFをVNET統合するためにサブネット委任を設定する
delegation {
name = "delegation"
service_delegation {
name = "Microsoft.Web/serverFarms"
actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
}
}
#ストレージアカウントにアクセスできるようにサービスエンドポイントを設定
service_endpoints = ["Microsoft.Storage"]
}
#AZFのログやインスタンス間のファイル共有を格納するためのストレージアカウント
resource "azurerm_storage_account" "st" {
name = "stazftest001"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
account_tier = "Standard"
account_replication_type = "LRS"
}
#ストレージアクセス制限
resource "azurerm_storage_account_network_rules" "st" {
storage_account_id = azurerm_storage_account.st.id
default_action = "Deny"
virtual_network_subnet_ids = [azurerm_subnet.subnet.id]
}
# AZFのPlan
resource "azurerm_service_plan" "plan" {
name = "plan-azf-test-001"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
os_type = "Windows"
sku_name = "EP1"
}
#AZF作成
resource "azurerm_windows_function_app" "func" {
name = "azfvnettest001"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
service_plan_id = azurerm_service_plan.plan.id
storage_account_name = azurerm_storage_account.st.name
storage_account_access_key = azurerm_storage_account.st.primary_access_key
virtual_network_subnet_id = azurerm_subnet.subnet.id
builtin_logging_enabled = false
https_only = true
app_settings = {
WEBSITE_ENABLE_SYNC_UPDATE_SITE = "true"
WEBSITE_CONTENTOVERVNET = "1"
WEBSITE_CONTENTSHARE = "azfvnettest001"
}
site_config {
runtime_scale_monitoring_enabled = true
use_32_bit_worker = false
application_stack {
node_version = "~18"
}
vnet_route_all_enabled = true
remote_debugging_version = "VS2019"
ftps_state = "Disabled"
scm_use_main_ip_restriction = true
}
}
原因
結論、以下の条件で発生するということがわかりました
- Elastic Premiumプランを使用している
- 作成時から環境変数
WEBSITE_CONTENTOVERVNET
を1
に指定している- この環境変数についての詳細はこちら
※色々苦労して検証したつもりですが、条件はこの限りではないかもしれません。
AzureFunctionのバックエンドにおいているストレージアカウントにはログや、インスタンス間で共有するファイルが保管されますが、ストレージアカウントにネットワークアクセス制限を設けたい場合は、WEBSITE_CONTENTOVERVNET
を1
にする必要があります。これは困った。
所感
ちなみに、ストレージアカウントのアクセス制限をプライベートエンドポイントにした場合、サービスエンドポイントにした場合同様のエラーでした。
想像レベルでしかないですが、この環境変数が初期段階で有効になっていると、バックエンドのストレージにうまくアクセスできないのではないかと思います。実際にエラーが出ている時はストレージに何もコンテナが作成されなかったので・・・
GUIでAzureFunctionを作成する際に最初からストレージにアクセス制限するようなUIじゃないので、TerraformなどAPIで作ろうとするときにハマるのだと思われます
解決策
原因にも書いた通り作成時に上記の環境変数が設定されているとこのエラーに遭遇します。
なので、AzureFunctionを作成した後にこの変数を追加すればいいのです
とはいっても、宣言的な記述をするTerraformで手続的なコーディング方法がないかと模索してみました。
Terraformでの実装
先に結論ですが、このような実装で「AZFの作成後に環境変数を追加する」ができました
※AzureFunctionをAZF
と略しています
#リソースグループ
resource "azurerm_resource_group" "rg" {
name = "rg-azf-test-001"
location = "japaneast"
}
#仮想ネットワークの作成
resource "azurerm_virtual_network" "vnet" {
name = "vnet-azf-test-001"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
address_space = ["10.0.0.0/16"]
}
#サブネットの作成
resource "azurerm_subnet" "subnet" {
name = "snet-azf-test-001"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.0.0.0/24"]
#AZFをVNET統合するためにサブネット委任を設定する
delegation {
name = "delegation"
service_delegation {
name = "Microsoft.Web/serverFarms"
actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
}
}
#ストレージアカウントにアクセスできるようにサービスエンドポイントを設定
service_endpoints = ["Microsoft.Storage"]
}
#AZFのデータを格納するためのストレージアカウント
resource "azurerm_storage_account" "st" {
name = "stazftest001"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
account_tier = "Standard"
account_replication_type = "LRS"
}
resource "azurerm_storage_account_network_rules" "st" {
storage_account_id = azurerm_storage_account.st.id
default_action = "Deny"
virtual_network_subnet_ids = [azurerm_subnet.subnet.id]
depends_on = [ azapi_update_resource.func_convert_overvnet ] //追加した設定
}
# AZF
resource "azurerm_service_plan" "plan" {
name = "plan-azf-test-001"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
os_type = "Windows"
sku_name = "EP1"
}
resource "azurerm_windows_function_app" "func" {
name = "azfvnettest001"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
service_plan_id = azurerm_service_plan.plan.id
storage_account_name = azurerm_storage_account.st.name
storage_account_access_key = azurerm_storage_account.st.primary_access_key
virtual_network_subnet_id = azurerm_subnet.subnet.id
builtin_logging_enabled = false
https_only = true
app_settings = { //WEBSITE_CONTENTOVERVNETの環境変数をここから削除
WEBSITE_ENABLE_SYNC_UPDATE_SITE = "true"
WEBSITE_CONTENTSHARE = "azfvnettest001"
}
site_config {
runtime_scale_monitoring_enabled = true
use_32_bit_worker = false
application_stack {
node_version = "~18"
}
vnet_route_all_enabled = true
remote_debugging_version = "VS2019"
ftps_state = "Disabled"
scm_use_main_ip_restriction = true
}
lifecycle { //追加した設定
ignore_changes = [
app_settings["WEBSITE_CONTENTOVERVNET"]
]
}
}
//追加した設定 ここから-----
#Functionの環境変数を読み取り
data "azapi_resource_action" "func_app_settings" {
type = "Microsoft.Web/sites@2022-03-01"
action = "config/appsettings/list"
resource_id = azurerm_windows_function_app.func.id
response_export_values = ["properties"]
}
#呼び出したdataと追加したい環境変数をマージして設定する
resource "azapi_update_resource" "func_convert_overvnet" {
type = "Microsoft.Web/sites/config@2022-03-01"
parent_id = azurerm_windows_function_app.func.id
name = "appsettings"
body = jsonencode({
properties = merge(
jsondecode(data.azapi_resource_action.func_app_settings.output).properties,
{
WEBSITE_CONTENTOVERVNET = "1"
}
)
kind = "string"
})
#stateファイルにbodyの全文が残らず、毎回planに表示されるため、bodyをignore_changeにする。
lifecycle {
ignore_changes = [
body
]
}
}
//追加した設定 ここまで-----
この顔を無事拝むことができてホッとしました。
WEBSITE_CONTENTOVERVNET
の値も無事設定できているようです。
簡単な解説
ざっくりと、上記のコードの主要部分の流れを説明すると、次のとおりです
- 作成時に
WEBSITE_CONTENTOVERVNET
を設定できないので、まずはネットワーク制限がないストレージを作成します - AZF作成します
- 完成したAZFの環境変数を読み取ります
- 完成したAZFの環境変数の画像でもわかるとおり、Terraformで設定していない組み込みの環境変数が多数設定されるため、完成したAZFから読み取るのが肝です
- 読み取ったデータと
WEBSITE_CONTENTOVERVNET
を環境変数をマージして設定します- 最初、読み取りとかマージとかをすっ飛ばして設定したら、環境変数が
WEBSITE_CONTENTOVERVNET
だけになってしまいましたので、大事なポイントです
- 最初、読み取りとかマージとかをすっ飛ばして設定したら、環境変数が
- 環境変数が設定されてAZFがNWアクセス制限したストレージアカウントに接続する準備が整ったので、ストレージアカウントのネットワークアクセス制限を設定(これを最後に持ってくるのはdepends_onを使用して実現しています)
詳細な解説
主に追加している内容を詳細に解説していきます。
AZFの環境変数を読み取り
上記のコードのこの部分で実現しています
data "azapi_resource_action" "func_app_settings" {
type = "Microsoft.Web/sites@2022-03-01"
action = "config/appsettings/list"
resource_id = azurerm_windows_function_app.func.id
response_export_values = ["properties"]
azapiについて解説を始めると書ききれないのでざっくりと。このリソースについてのドキュメントはこちらをご覧ください。
-
type
ではAPIのタイプを指定します- AZFやAppServiceのAPIは
Microsoft.Web/sites@2022-03-01
を指定します - 他のAPIについても知りたかったら、このページを見ると色々詳細に書いてあるので眺めてみてください
- AZFやAppServiceのAPIは
-
action
ではリソースに対するアクションを設定できます-
config/appsettings/list
と指定することで、環境変数を設定値を取得できます
-
-
resource_id
はどのリソースからdata取得をするかどうかを指定します -
response_export_values
では数あるレスポンスのうちどの値を戻り値として使用するかを指定することができます-
"*"
を指定して、「とりあえず全部」アウトプットすることも可能です
-
取得したdataを展開してみるとこんな感じの値が入っています
> data.azapi_resource_action.func_app_settings
{
"action" = "config/appsettings/list"
"body" = tostring(null)
"id" = "/subscriptions/***/resourceGroups/rg-azf-test-001/providers/Microsoft.Web/sites/azfvnettest001/config/appsettings/list"
"method" = "POST"
"output" = "{\"properties\":{\"AzureWebJobsStorage\":\"DefaultEndpointsProtocol=https;AccountName=stazftest001;AccountKey=***;EndpointSuffix=core.windows.net\",\"FUNCTIONS_EXTENSION_VERSION\":\"~4\",\"FUNCTIONS_WORKER_RUNTIME\":\"node\",\"WEBSITE_CONTENTAZUREFILECONNECTIONSTRING\":\"DefaultEndpointsProtocol=https;AccountName=stazftest001;AccountKey=****;EndpointSuffix=core.windows.net\",\"WEBSITE_CONTENTSHARE\":\"azfvnettest001\",\"WEBSITE_ENABLE_SYNC_UPDATE_SITE\":\"true\",\"WEBSITE_NODE_DEFAULT_VERSION\":\"~18\"}}"
"resource_id" = "/subscriptions/***/resourceGroups/rg-azf-test-001/providers/Microsoft.Web/sites/azfvnettest001"
"response_export_values" = tolist([
"properties",
])
"timeouts" = null /* object */
"type" = "Microsoft.Web/sites@2022-03-01"
}
見てわかるように、output
という属性の中に読み取られた環境変数がjson形式で格納されてれています。HCL形式のオブジェクトの方が整形や扱いが楽なので、jsondecode
関数を使うことでうまく取り出すことができます。
> jsondecode(data.azapi_resource_action.func_app_settings.output)
{
"properties" = {
"AzureWebJobsStorage" = "DefaultEndpointsProtocol=https;AccountName=stazftest001;AccountKey=***;EndpointSuffix=core.windows.net"
"FUNCTIONS_EXTENSION_VERSION" = "~4"
"FUNCTIONS_WORKER_RUNTIME" = "node"
"WEBSITE_CONTENTAZUREFILECONNECTIONSTRING" = "DefaultEndpointsProtocol=https;AccountName=stazftest001;AccountKey=***;EndpointSuffix=core.windows.net"
"WEBSITE_CONTENTSHARE" = "azfvnettest001"
"WEBSITE_ENABLE_SYNC_UPDATE_SITE" = "true"
"WEBSITE_NODE_DEFAULT_VERSION" = "~18"
}
}
#さらに.propertiesをつければ完全にkey/valueの形式になります
> jsondecode(data.azapi_resource_action.func_app_settings.output).properties
{
"AzureWebJobsStorage" = "DefaultEndpointsProtocol=https;AccountName=stazftest001;AccountKey=***;EndpointSuffix=core.windows.net"
"FUNCTIONS_EXTENSION_VERSION" = "~4"
"FUNCTIONS_WORKER_RUNTIME" = "node"
"WEBSITE_CONTENTAZUREFILECONNECTIONSTRING" = "DefaultEndpointsProtocol=https;AccountName=stazftest001;AccountKey=***;EndpointSuffix=core.windows.net"
"WEBSITE_CONTENTSHARE" = "azfvnettest001"
"WEBSITE_ENABLE_SYNC_UPDATE_SITE" = "true"
"WEBSITE_NODE_DEFAULT_VERSION" = "~18"
}
これで作成後のAZFに設定されている環境変数を読み取ることができました
実際にAZFに環境変数を追加する
環境変数の追加はこの部分で実現しています
resource "azapi_update_resource" "func_convert_overvnet" {
type = "Microsoft.Web/sites/config@2022-03-01"
parent_id = azurerm_windows_function_app.func.id
name = "appsettings"
body = jsonencode({
properties = merge(
jsondecode(data.azapi_resource_action.func_app_settings.output).properties,
{
WEBSITE_CONTENTOVERVNET = "1"
}
)
kind = "string"
})
#stateファイルにbodyの全文が残らず、毎回planに表示されるため、bodyをignore_changeにする。
lifecycle {
ignore_changes = [
body
]
}
}
このリソースについてのドキュメントはこちらをご覧ください。
- 一つ前のdataソースと引数は似たようなものなので、細かい解説は省きますがポイントとしては設定値はJSONで求められます。なので、
jsonencode
関数を使用して、値を入れています - あとは、
merge
関数を利用して、先ほど取得してきた環境変数と{WEBSITE_CONTENTOVERVNET = "1"}
を結合しています
戻り値としてはこんな感じになります
> merge(jsondecode(data.azapi_resource_action.func_app_settings.output).properties,{WEBSITE_CONTENTOVERVNET = "1"})
{
"AzureWebJobsStorage" = "DefaultEndpointsProtocol=https;AccountName=stazftest001;AccountKey=***;EndpointSuffix=core.windows.net"
"FUNCTIONS_EXTENSION_VERSION" = "~4"
"FUNCTIONS_WORKER_RUNTIME" = "node"
"WEBSITE_CONTENTAZUREFILECONNECTIONSTRING" = "DefaultEndpointsProtocol=https;AccountName=stazftest001;AccountKey=***;EndpointSuffix=core.windows.net"
"WEBSITE_CONTENTOVERVNET" = "1"
"WEBSITE_CONTENTSHARE" = "azfvnettest001"
"WEBSITE_ENABLE_SYNC_UPDATE_SITE" = "true"
"WEBSITE_NODE_DEFAULT_VERSION" = "~18"
}
-
ignore_changes
の箇所については最初は書かなかったんですが、毎回このリソースがPlanに上がってきてしまったため、tfstateファイルを確認したところ、body
の値を何も保存できていなかったみたいです。そのせいで毎回差分を検知してPLanに上がってきてしまっていたため、このような実装にしました
その他細かいところ
ネットワークアクセス制限にdepends_on
を追加しました
resource "azurerm_storage_account_network_rules" "st" {
storage_account_id = azurerm_storage_account.st.id
default_action = "Deny"
virtual_network_subnet_ids = [azurerm_subnet.subnet.id]
depends_on = [ azapi_update_resource.func_convert_overvnet ]
}
これによって、環境変数の追加が完了してからストレージアカウントにネットワークアクセス制限がかかります
AzureFunctionにignore_changes
を追加しました
resource "azurerm_windows_function_app" "func" {
name = "azfvnettest001"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
service_plan_id = azurerm_service_plan.plan.id
storage_account_name = azurerm_storage_account.st.name
storage_account_access_key = azurerm_storage_account.st.primary_access_key
virtual_network_subnet_id = azurerm_subnet.subnet.id
builtin_logging_enabled = false
https_only = true
app_settings = {
WEBSITE_ENABLE_SYNC_UPDATE_SITE = "true"
WEBSITE_CONTENTSHARE = "azfvnettest001"
}
site_config {
runtime_scale_monitoring_enabled = true
use_32_bit_worker = false
application_stack {
node_version = "~18"
}
vnet_route_all_enabled = true
remote_debugging_version = "VS2019"
ftps_state = "Disabled"
scm_use_main_ip_restriction = true
}
lifecycle {
ignore_changes = [
app_settings["WEBSITE_CONTENTOVERVNET"]
]
}
}
app_settings["WEBSITE_CONTENTOVERVNET"]
をignore_changes
(変更の無視)しています。
これをしておかないと、2回目以降にApplyを実行した際にazapi_update_resource
によって追加された環境変数を差分としてトリガーしてしまい、環境変数を消そうとしてしまいます。
あとは、当たり前ですが、app_settings
ではWEBSITE_CONTENTOVERVNET
の設定をしないようにしています
最後に
すこし複雑なコードになってしまいましたが、これでAzureFunctionが出来上がったあとに環境変数を追加するということができて、ストレージアクセス制限のデプロイでハマっていたところから抜け出せるかと思います
azapi
を使用するとかゆいところに手が届くことが多いので、azurerm
しか使用されていない方でどうしても実現できないことがある場合は少しかじってみてもいいかもしれません
どなたかの参考になれば幸いです