Paper2 Blog

ともに、かける

Google Cloud IAMポリシーはread-modify-writeパターンで安全に更新する

サマリ

  • 複数のシステムが同じIAMポリシーに同時に書き込みを行うと、それぞれの変更が上書きされてしまう可能性がある
  • read-modify-writeパターンによりETagを活用して安全に更新できる

解説

Google CloudのIAMポリシーとは

IAM(Identity and Access Management)ポリシーは、Google Cloudのリソースに対するアクセス権を管理するための機能です。IAMポリシーは、どのユーザーやサービスアカウントがどのリソースに対してどのような操作を行うことができるかを定義します。これにより、組織内のアクセス制御を細かく設定できます。

IAMポリシーは以下の要素で構成されます。

要素 説明
bindings 特定のロールとそのロールが割り当てられるメンバーを関連付けるもの
roles Google Cloudリソースに対して実行可能な操作のセットを定義するもの
members ロールが割り当てられるエンティティ(ユーザー、グループ、サービスアカウントなど)
etag ポリシーのバージョン管理に使用される文字列
version IAMポリシーのスキーマのバージョン

実際のデータはこんな感じです。

bindings:
- members:
  - serviceAccount:1068899999834-compute@developer.gserviceaccount.com
  - serviceAccount:1068800000834@cloudservices.gserviceaccount.com
  - serviceAccount:xxxxxx@appspot.gserviceaccount.com
  role: roles/editor
- members:
  - user:hoge@gmail.com
  role: roles/owner
...
etag: BwYduMUi2vM=
version: 1

意図しない上書きが発生するケース

上記のようにIAMポリシーはbindingsのリストを一つのデータとして保持しています。そのため複数のシステムが同じIAMポリシーに同時に書き込みを行うと、意図しない上書きが発生する可能性があります。

例えば以下のようにBさんがポリシーを意図せず上書きしてしまうケースを考えてみます。それぞれがread-modify-write(読み込み、変更、書き込み)をします。

まずお互い現在の IAMポリシーを読み込みます。

IAMポリシーを読み込み

次にそれぞれが保持するIAMポリシーに変更を加えます。

それぞれが保持するIAMポリシーに変更を加える

Aが先にIAMポリシーを上書きします。

Aが先にIAMポリシーを上書き

次にBがIAMポリシーを上書きします。

BがIAMポリシーを上書き

結果的にAさんが追加したかった user+C@gmail.com の変更がBさんの上書きによって消滅してしまいました。

Google CloudのIAMポリシーにおいてはread-modify-write時に後述するETagを活用することで意図しない上書きを回避できます。その仕組みを説明していきます。

なお、上記でややこしい例を出してしまったと後悔しているのですが、実際にはGoogle CloudのIAMポリシーではread-modify-writeをすれば勝手にETagが含まれるので上記のような意図しない上書きは発生しません。

Etagによる意図しない上書きの回避

IAMポリシーはETagによるIAMポリシーの意図しない上書きを防ぐ仕組みを提供しています。

IAMポリシーには etag フィールドが含まれています。このフィールドの値はIAMポリシーが更新されるたびに変化する仕様になっています。

etag フィールドには BwYduMUi2vM= などの値が含まれます。IAMポリシー更新時に etag フィールドを含めることで、etag が一致しないREST APIが失敗するようになります。

HTTP ステータスコード409 Conflict が返されます。レスポンスは以下になります。適切にリトライする必要はありますが、意図しない上書きは回避できます。

{
  "error": {
    "code": 409,
    "message": "There were concurrent policy changes. Please retry the whole read-modify-write with exponential backoff.",
    "status": "ABORTED"
  }
}

このようにIAMポリシーではETagの仕組みがあるため、read-modify-writeのパターンでETagを活用することで安全に変更をすることができます。

試してみる

ETagにより上書きが発生しないかを試してみます。まずは正常に書き込めるパターンを見てみます。

まず現在のIAMポリシーを取得します。

$ gcloud projects get-iam-policy <project-name> --format=json > policy.json

変更するポリシーを作成します。

$ cp policy.json updated_policy.json
$ vi updated_policy.json

検証用のアカウントだったのでbindingをテキトーに削除してみます。

--- policy.json  2024-07-21 13:39:59.000000000 +0900
+++ updated_policy.json   2024-07-21 13:40:12.000000000 +0900
@@ -55,12 +55,6 @@
         "serviceAccount:test-cloud-run-invoke@memory-tweet-35xxxx.iam.gserviceaccount.com"
       ],
       "role": "roles/run.invoker"
-    },
-    {
-      "members": [
-        "serviceAccount:service-106884941xxxx@serverless-robot-prod.iam.gserviceaccount.com"
-      ],
-      "role": "roles/run.serviceAgent"
     }
   ],
   "etag": "BwYduMUi2vM=",

以下コマンドでIAMポリシーを変更できます。

$ gcloud projects set-iam-policy <project-name> updated_policy.json

現在のIAMポリシーと変更前のポリシーのETagを確認してみると違うことがわかります。

$ gcloud projects get-iam-policy <project-name> --format=json | jq '.etag'
"BwYduolgp7U=" <- 変更後のETag
$ cat policy.json| jq '.etag'
"BwYduMUi2vM=" <- 変更前の読み取り時のETag
$ cat updated_policy.json| jq '.etag'
"BwYduMUi2vM="

では、先ほど読み取ったpolicy.jsonで異なる変更をしてみましょう。

$ cp policy.json updated_failed_policy.json
$ vi updated_failed_policy.json

変更できないのでどうでもいいですが、以下のような変更を加えます。

--- policy.json  2024-07-21 13:39:59.000000000 +0900
+++ updated_failed_policy.json    2024-07-21 13:49:36.000000000 +0900
@@ -20,12 +20,6 @@
     },
     {
       "members": [
-        "serviceAccount:service-106884941xxxx@gcp-sa-cloudscheduler.iam.gserviceaccount.com"
-      ],
-      "role": "roles/cloudscheduler.serviceAgent"
-    },
-    {
-      "members": [
         "serviceAccount:service-106884941xxxx@containerregistry.iam.gserviceaccount.com"
       ],
       "role": "roles/containerregistry.ServiceAgent"

上記のポリシーで更新をしてみます。

$ gcloud projects set-iam-policy <project-name> updated_failed_policy.json  
ERROR: (gcloud.projects.set-iam-policy) Resource in projects [<project-name>:setIamPolicy] is the subject of a conflict: There were concurrent policy changes. Please retry the whole read-modify-write with exponential backoff. The request's ETag '\007\006\035\270\305\"\332\363' did not match the current policy's ETag '\007\006\035\272\211`\247\265'.

と、想定通りエラーになりました。Etagの状況を確認すると以下のように異なっているため失敗することがわかります。

$ gcloud projects get-iam-policy <project-name> --format=json | jq '.etag'
"BwYduolgp7U="
$ cat updated_failed_policy.json | jq '.etag'     
"BwYduMUi2vM="

まとめ

複数のシステムが同じIAMポリシーに同時に書き込みを行うと、それぞれの変更が上書きされてしまう可能があります。しかし、ETagによる意図しない上書きの回避が可能なため、read-modify-writeパターンを用いて安全に更新しましょう。