LoginSignup
1
0

AzureFunctionのストレージをアクセス制限してデプロイしようとしてハマった話

Last updated at Posted at 2024-03-07

この記事について

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エラーになってしまう状況です
image.png
ちなみに、正しく作成ができている状態でURLにアクセスするとこんな画面になります
image.png
しばらくは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_CONTENTOVERVNET1に指定している
    • この環境変数についての詳細はこちら

※色々苦労して検証したつもりですが、条件はこの限りではないかもしれません。

AzureFunctionのバックエンドにおいているストレージアカウントにはログや、インスタンス間で共有するファイルが保管されますが、ストレージアカウントにネットワークアクセス制限を設けたい場合は、WEBSITE_CONTENTOVERVNET1にする必要があります。これは困った。

所感

ちなみに、ストレージアカウントのアクセス制限をプライベートエンドポイントにした場合、サービスエンドポイントにした場合同様のエラーでした。
想像レベルでしかないですが、この環境変数が初期段階で有効になっていると、バックエンドのストレージにうまくアクセスできないのではないかと思います。実際にエラーが出ている時はストレージに何もコンテナが作成されなかったので・・・
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
    ]
  }
}
//追加した設定 ここまで-----

この顔を無事拝むことができてホッとしました。
image.png
WEBSITE_CONTENTOVERVNETの値も無事設定できているようです。
image.png

簡単な解説

ざっくりと、上記のコードの主要部分の流れを説明すると、次のとおりです

  • 作成時に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についても知りたかったら、このページを見ると色々詳細に書いてあるので眺めてみてください
  • 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しか使用されていない方でどうしても実現できないことがある場合は少しかじってみてもいいかもしれません
どなたかの参考になれば幸いです

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0