Azure Functionsのstaging slotもTimer Trigger動くの知らなかった
はじめに
イオンネクストの@arairyusです。
Azure Functionsのdeployment slot使うようにして、Blue/Greenデプロイに切り替えたら事故りました。
Azure歴が5年目なのですがずっとslotの挙動を勘違いしており懺悔ブログを残します。
同じ過ちを起こす人が少しでも減れば幸いです。
TL;DR
- Deployment slot は独立したインスタンスとして動作するため、
staging slot でも Timer Trigger や Blob Storage Trigger などは通常通り実行される - これにより重複実行が発生し、本番データに影響が出るリスクがある
- 対策:staging slot では HTTP Trigger 以外を
Disabled = true
に設定する - 再発防止:CD で自動的に Disable 化する仕組みを導入
Deployment Slotとは
Azure Functionの「Deployment slot(デプロイ スロット)」とは、本番環境とは別のスロット(インスタンス)を作成し、アプリケーションの新バージョンを無停止でデプロイ・検証し、切り戻し(フォールバック)なども容易に行えるAzure Functionsの機能です。
デプロイ スロットの特徴
デプロイ スロットは、本番とは異なるURL・エンドポイントを持つ検証環境やステージング環境などを作成できます。各スロット間の「スワップ」機能を使えば、ステージングで検証した後にワンクリックで本番スロットと置き換えることが可能です。その際、トラフィックのリダイレクトはシームレスに行われるため、ダウンタイムなしでのリリースや即時ロールバックができます。
by パープレ
このように1つのFunction Appに1つ(または複数)のSlot(インスタンス)を用意して、本番エンドポイントと別で動作検証などに用いることができる機能です。
Swap機能を使うことでBlue Greenデプロイも実現できます。
これ知っているよって方はブラウザバックしていただいてOKです。
詳しくは公式Docを
今回事故った点
リリース時のダウンタイムを防ぐべく、既存のAzure FunctionsにDeployment slotを追加しBlue Greenデプロイに切り替えていったのですが、事故ったFunction AppがHTTP Trigger以外にTimer TriggerやService Bus TriggerのFunctionも存在する我が社の基幹処理をしているFunction Appでした。
該当のFunction AppにSlotを追加する前に複数のFunction Appをすでに追加しており実績十分でリリースしたのですが、無事事故ってしまいました。
気づいたときには、New Relicに重複実行されるとまずい重要なTimer Functionのトランザクションが同時刻に2つ生えてて、「あ、やっちゃった...」となりました。
てっきり、staging slotはFunctionが勝手に動かないと勘違いしておりました。。。
※ productionってバッジついてるし、Traffic 0%にしているし勝手に待機系インスタンスなのかと思ってました。4年間ずっとこの勘違いをしてました。。。
前もってリリースしてたFunction Appは下記ブログ記載の通り名前文字数が長すぎて運よく救われていたようです。(たぶん)
どうやって防ぐか
有識者に聞いたところ、下記パターンが一般的だと教えていただきました。
- イベントソースやデータベースなどの外部連携先をプロダクション環境と分離する
- staging slot側のFunctionを無効化する
結果、"staging slot側のFunctionを無効化する"を採用しました。
Azure PortalでいうDeployment slot setting = true
にしたうえで、AzureWebJobs.<function-name>.Disabled = true
を入れる形です。
こうすることでSlot固定の環境変数となり、環境変数がSwapされない状態になります。
Slot導入の目的がBlue Greenデプロイなのでシンプルに実装したいなと思ってです。
ただ、無効化するといっても運用を考えると地味に難しいポイントがありました。
まず、TerraformでIaCしているのですがこんな感じの設定になります。
HTTP Trigger以外のFunctionは必ずTerraformでこの設定を入れる運用を考えたのですが、絶対誰か設定し忘れて同じ事故起こすなーと
# production slot
resource "azurerm_windows_function_app" "main" {
...
app_settings = {}
sticky_settings {
app_setting_names = [
"AzureWebJobs.TimerFunc01.Disabled",
"AzureWebJobs.TimerFunc02.Disabled",
]
}
...
lifecycle {
ignore_changes = [
app_settings,
]
}
}
# staging slot
resource "azurerm_windows_function_app_slot" "staging" {
...
app_settings = {
"AzureWebJobs.TimerFunc01.Disabled" = "1"
"AzureWebJobs.TimerFunc02.Disabled" = "1"
...
}
...
}
よって、IaCを正としつつ保険でCDでstaging slotでEnableになっているHTTP以外のTriggerを検出してDisableにする処理も追加しました。
Terraformはそもそも宣言的だろとは思いますが、環境変数の変更をリリースするとSwap後tfstateとコードに差分が出ちゃうのでしょうがないかなと思い、このような形にしました。
自動Disableのざっくり処理
- staging slotのappsettingsをjsonに吐く
az webapp config appsettings list \ --resource-group "$RESOURCE_GROUP" \ --name "$FUNCAPP_NAME" \ --slot staging \ --output json
- Disableにする対象をJSONから抽出
- まとめて変更
az webapp config appsettings set \ --resource-group "$RESOURCE_GROUP" \ --name "$FUNCAPP_NAME" \ --slot staging \ --settings $DISABLED_SETTINGS \ --slot-settings $DISABLED_SETTINGS
学んだこと
- Deployment Slot は「待機インスタンス」ではなく「独立した実行環境」
他クラウドのFaaSと比較して何でもできるがゆえに使いこなしが複雑なサービスだなーと改めて思いました。
はやくFlex ConsumptionでもDeployment Slot使えるようになって、VNET統合のためにApp Service Plan/Elastic Premiumを使わざるを得ない状況から抜け出したいなと思いました。
正直まだFunctionsを理解できてない部分もあるため、こうするべきだ!があれば教えていただけると嬉しいです。
イオングループで、一緒に働きませんか?
イオングループでは、エンジニアを積極採用中です。少しでもご興味もった方は、キャリア登録やカジュアル面談登録などもしていただけると嬉しいです。
皆さまとお話できるのを楽しみにしています!
Discussion