# StackQL を使用した Google の GitOps IAM バインディングフレームワーク

> SQL を使用した Google Cloud インフラストラクチャとリソースのクエリとデプロイ

Source: https://stackql.io/ja/quick-starts/google/iam-bindings-framework

このガイドでは、StackQL を使用して GCP の IAM バインディング（権限付与）を管理する GitOps フレームワークを紹介します。StackQL は、SQL 構文でクラウドインフラストラクチャとリソースのクエリとデプロイを行える強力な開発ツールです。  

動作確認済み 組み込み SQL バックエンド macos linux powershell

## Google IAM の背景

Google Cloud Identity and Access Management（IAM）は、Google Cloud リソースへのアクセスを管理するためのフレームワークです。管理者は、誰がリソースに対して特定の操作を実行できるかを定義できます。IAM は ID（ユーザーやサービスアカウントなど）、ロール、権限を中心に機能し、ID に割り当てられたロールがそのアクセス権を決定します。このガイドでは、ロールと ID を結び付ける IAM バインディングを StackQL で管理する方法を詳しく説明します。StackQL は、SQL 構文でクラウドリソースの管理を簡素化するツールです。  

IAM バインディングは、次のいずれのレベルでも作成できます：

- 組織レベル
- フォルダーレベル
- プロジェクトレベル
- リソースレベル（バケット、BigQuery データセットなど）
- 請求先アカウントレベル

組織レベルまたはフォルダーレベルで適用された IAM バインディングは、すべての子オブジェクトに継承されます。  

## 宣言的なバインディング定義と GitOps

このガイドでは、IAM バインディングをソースコードリポジトリで一元管理し、リポジトリを状態と履歴の信頼できる唯一の情報源にする方法（GitOps と呼ばれる考え方）と、StackQL を使用して CI/CD ワークフローで変更を適用する方法を説明します。   

この例では、ソース管理に保存された `json` マニフェストから、*複数*のメンバーまたはプリンシパルを*複数*のロールにマッピングする方法を示します。  

> GCP コンソール、`terraform`、`gcloud` ユーティリティでは、1 回の操作で *1 つ*のメンバーを 1 つ以上のロールにマッピングすることしかできません  

## 仕組み

このガイドで説明するフレームワークは、GCP の階層内のすべてのノードに IAM バインディングをデプロイするとともに、カスタムロール（厳選した権限を持つもの）を作成します。これは、たとえば `roles/bigquery.dataEditor` のような事前定義ロールとは異なります。

### プロジェクトのディレクトリ構造

このバインディングデプロイフレームワークでは、次のディレクトリツリーを使用します：

```
├── iql
│   ├── iam.iql
│   ├── ...
├── data
│   ├── iam.jsonnet
│   ├── iam
│   │   ├── bindings.json
│   │   ├── custom_roles.json
```

### `iql/iam.iql` ファイル

これは `stackql` によって実行されるメインファイルです。`data/iam.jsonnet` を使用して、`data/iam/bindings.json` のデータから生成されたトランスパイル済みのポリシーバインディングをレンダリングし、適用します。

> このファイルを変更する必要はありません

**展開して `iql/iam.iql` ファイルを表示**

```sql
/*
     ROOT LEVEL (ORG AND FOLDER) IAM FOR SERVICE ACCOUNTS AND GROUPS
*/
{{ $root := . }}

/*** create custom roles ***/ {{ range .custom_roles }}

INSERT INTO google.iam.`organizations.roles`(
 parent,
 data__role,
 data__roleId )
SELECT   
 '{{ $root.organization_id }}',
 '{"title": "{{ .title }}", "description": "{{ .description }}", "stage": "{{ .stage }}", "includedPermissions": {{ .includedPermissions }}}', 
 '{{ .id }}' 
;{{ end }}

/*** create org role bindings */  

EXEC google.cloudresourcemanager.organizations.setIamPolicy 
@resource = '{{ $root.organization_id }}' 
@@json = '{
  "policy": {
    "bindings": {{ $root.bindings.org }}
   }
}';

/*** create billing role bindings */

EXEC google.cloudbilling.billingAccounts.setIamPolicy 
@resource = '{{ $root.billing_account_id }}' 
@@json = '{
  "policy": {
    "bindings": {{ $root.bindings.billingacct }}
   }
}';

/*** create nonprod folder role bindings */  

EXEC google.cloudresourcemanager.folders.setIamPolicy 
@resource = '{{ $root.nonprod_folder_id }}' 
@@json = '{
  "policy": {
    "bindings": {{ $root.bindings.folders.nonprod }}
   }
}';

/*** create prod folder role bindings */  

EXEC google.cloudresourcemanager.folders.setIamPolicy 
@resource = '{{ $root.prod_folder_id }}' 
@@json = '{
  "policy": {
    "bindings": {{ $root.bindings.folders.prod }}
   }
}';

/*** create datalabs folder role bindings */  

EXEC google.cloudresourcemanager.folders.setIamPolicy 
@resource = '{{ $root.datalabs_folder_id }}' 
@@json = '{
  "policy": {
    "bindings": {{ $root.bindings.folders.datalabs }}
   }
}';

/*** create stackql-audit project level role bindings */  

EXEC google.cloudresourcemanager.folders.setIamPolicy 
@resource = 'projects/stackql-audit' 
@@json = '{
  "policy": {
    "bindings": {{ $root.bindings.projects.stackql_audit }}
   }
}';

/*** create role bindings for buckets in stackql-terraform */ {{ range $root.bindings.buckets }}

-- creating policy bindings for {{ .name }}
EXEC google.storage.buckets.setIamPolicy 
@bucket = '{{ .name }}'
@@json = '{
	"bindings": {{ .data }}
}';
{{ end }}

/*** create role bindings for topics in stackql-audit */ {{ range $root.bindings.topics }}

-- creating policy bindings for {{ .name }}
EXEC google.pubsub.`projects.topics`.setIamPolicy
@resource = '{{ .resource }}'
@@json = '{
	"bindings": {{ .data }}
}';
{{ end }}
```

## `data/iam.jsonnet`

このファイルは `iql/iam.iql` によって前処理され、`data/iam/bindings.json` からデータを取得します。

> このファイルを変更する必要はありません

**展開して `data/iam.jsonnet` ファイルを表示**

```javascript
// update the following files only:
//   data/iam/custom_roles.json
//   data/iam/bindings.json

// variables
local custom_roles_data = import './data/iam/custom_roles.json';
local bindings_data = import './data/iam/bindings.json';
local organization_id = 'organizations/123466304837';
local billing_account_id = 'billingAccounts/123456-9DFD97-EE2B33';
local nonprod_folder_id = 'folders/1234016945998';
local prod_folder_id = 'folders/123431606453';
local datalabs_folder_id = 'folders/123464988355';
local environments = [{name: 'prod'}, {name: 'nonprod'}, {name: 'datalabs'}];

// DO NOT MODIFY BEYOND THIS POINT

local generate_binding(x) =
	local members = [x for x in x.members];
	std.map(function(r) {"role": r, "members": members} , [x for x in x.roles]);

local generate_conditional_binding(x) =
	local members = [x for x in x.members];
	local condition = x.condition;
	std.map(function(r) {"role": r, "members": members, "condition": condition} , [x for x in x.roles]);

{
 organization_id: organization_id,
 billing_account_id: billing_account_id,
 nonprod_folder_id: nonprod_folder_id,
 prod_folder_id: prod_folder_id,
 datalabs_folder_id: datalabs_folder_id,
 custom_roles: custom_roles_data,
 environments: environments,
 bindings: {
	org: std.flattenArrays(std.map(generate_binding, bindings_data.org)),
	billingacct: std.flattenArrays(std.map(generate_binding, bindings_data.billingacct)),
	folders: {
		nonprod: std.flattenArrays(std.map(generate_binding, bindings_data.folders.nonprod)),
		prod: std.flattenArrays(std.map(generate_binding, bindings_data.folders.prod)),
		datalabs: std.flattenArrays(std.map(generate_binding, bindings_data.folders.datalabs)),
	},
	projects: {
		stackql_audit: std.flattenArrays(std.map(generate_conditional_binding, bindings_data.projects.stackql_audit)),
	},
	buckets: [
		{
			name: "stackql-tf-prod",
			data: std.flattenArrays(std.map(generate_binding, bindings_data.buckets.stackql_tf_prod)),
		},
		{
			name: "stackql-tf-modules-prod",
			data: std.flattenArrays(std.map(generate_binding, bindings_data.buckets.stackql_tf_modules_prod)),
		},
		{
			name: "stackql-tf-nonprod",
			data: std.flattenArrays(std.map(generate_binding, bindings_data.buckets.stackql_tf_nonprod)),
		},
		{
			name: "stackql-tf-modules-nonprod",
			data: std.flattenArrays(std.map(generate_binding, bindings_data.buckets.stackql_tf_modules_nonprod)),
		},
		{
			name: "stackql-tf-datalabs",
			data: std.flattenArrays(std.map(generate_binding, bindings_data.buckets.stackql_tf_datalabs)),
		},
		{
			name: "stackql-tf-modules-datalabs",
			data: std.flattenArrays(std.map(generate_binding, bindings_data.buckets.stackql_tf_modules_datalabs)),
		},
	],
	topics: [
		{
			name: "stackql-np-log-topic",
			resource: "projects/stackql-audit/topics/stackql-np-log-topic",
			data: std.flattenArrays(std.map(generate_binding, bindings_data.topics.stackql_np_log_topic)),
		},
		{
			name: "stackql-prod-log-topic",
			resource: "projects/stackql-audit/topics/stackql-prod-log-topic",
			data: std.flattenArrays(std.map(generate_binding, bindings_data.topics.stackql_prod_log_topic)),
		},		
	],
 },
} 
```

## `data/iam/bindings.json`

> ポリシーバインディングを変更するには、このファイルを __変更する必要があります__

これはポリシーとバインディングのデータのマスターソースです。次に示すように、このデータ構造は、特定のレベル（親）で 1 つ以上のプリンシパル（グループ、サービスアカウントなど）を 1 つ以上のロール（事前定義またはカスタム）にマッピングします：

```json
  "<< level >>": [
    {
      "members": [
        "<< principal >>",
        ...
      ],
      "roles": [
        "<< role >>",
        ...
      ]
    },
```

たとえば：

```json
  "folders": {
    "nonprod": [
      {
        "members": [
          "serviceAccount:terraform-nonprod@stackql-terraform.iam.gserviceaccount.com"
        ],
        "roles": [
          "roles/compute.networkAdmin",
          "roles/resourcemanager.folderViewer",
          "roles/resourcemanager.projectCreator",
          "roles/storage.admin",
          "roles/artifactregistry.admin",
          "roles/container.serviceAgent",
          "roles/iam.securityAdmin",
          "roles/bigquery.admin",
          "roles/serviceusage.serviceUsageAdmin",
          "roles/cloudsql.admin",
          "roles/iam.serviceAccountAdmin",
          "roles/pubsub.admin",
          "roles/cloudfunctions.developer"
        ]
      }
    ],
```

## デプロイ

変更の頻度が低く、またデプロイに必要なレベルの権限をサービスアカウントに付与したくないため、このプロセスは意図的に CI/CD パイプラインでは実行しません。このプロセスは、gcp-org-administrators Google グループのメンバーである特権管理者が実行します。  

ユーザーは、サービスアカウントまたは対話型の認証のいずれかを使用して、GCP に認証されている必要があります。StackQL から Google への認証の詳細については、[__GCP の認証__](https://google.stackql.io/providers/google/#authentication) を参照してください。  

ドライランを実行するには、次のコマンドを実行します：
```shell
stackql exec -i ./iql/iam.iql \
--iqldata ./data/iam.jsonnet \
--outfile iam-TEMPLATED.iql \
--dryrun --output text --hideheaders
```

このファイルに出力されたデータは、`stackql shell` で個々の文として実行するか、次のコマンドでバッチとして実行できます：

```shell
stackql exec -i iam-TEMPLATED.iql \
```
または、このスクリプトで生成されたポリシー（JSON ペイロード）を取り出し、`gcloud iam` コマンドで実行することもできます。
