{"id":618,"date":"2025-12-30T21:26:52","date_gmt":"2025-12-30T12:26:52","guid":{"rendered":"https:\/\/tako.nakano.net\/blog\/?p=618"},"modified":"2026-01-03T14:24:40","modified_gmt":"2026-01-03T05:24:40","slug":"group-from-sheet","status":"publish","type":"post","link":"https:\/\/tako.nakano.net\/blog\/2025\/12\/group-from-sheet\/","title":{"rendered":"Dynamically Managing Google Groups Members with Cloud Identity Groups API"},"content":{"rendered":"<h1>Cloud Identity Groups API \u3092\u4f7f\u3063\u3066 Google Groups \u306e\u30e1\u30f3\u30d0\u30fc\u3092\u52d5\u7684\u306b\u7ba1\u7406\u3059\u308b<\/h1>\n<p>English follows Japanese.<\/p>\n<h2>\u307e\u3068\u3081<\/h2>\n<p>\u3053\u306e\u8a18\u4e8b\u3067\u306f\u3001Cloud Identity Groups API \u3092\u5229\u7528\u3057\u3066\u3001\u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8\u306e\u30c7\u30fc\u30bf\u306b\u57fa\u3065\u3044\u3066 Google Groups \u306e\u30e1\u30f3\u30d0\u30fc\u3092\u81ea\u52d5\u7684\u306b\u8ffd\u52a0\u30fb\u524a\u9664\u3059\u308b\u30b7\u30b9\u30c6\u30e0\u3092\u7d39\u4ecb\u3057\u307e\u3059\u3002<\/p>\n<p><a href=\"https:\/\/github.com\/takotakot\/misc\/tree\/main\/group-from-sheet\">https:\/\/github.com\/takotakot\/misc\/tree\/main\/group-from-sheet<\/a> \u3067\u30bd\u30fc\u30b9\u30b3\u30fc\u30c9\u3092\u516c\u958b\u3057\u3066\u3044\u307e\u3059\u3002<\/p>\n<p>\u30b9\u30e9\u30a4\u30c9\u306f Google Slide <a href=\"https:\/\/docs.google.com\/presentation\/d\/1jsCtTDSjSIWmad2vs777Hs6Kxt4Cqq5R5f8c02jW90I\/edit?slide=id.g3b44be7cb97_0_63#slide=id.g3b44be7cb97_0_63\">https:\/\/docs.google.com\/presentation\/d\/1jsCtTDSjSIWmad2vs777Hs6Kxt4Cqq5R5f8c02jW90I\/edit?slide=id.g3b44be7cb97_0_63#slide=id.g3b44be7cb97_0_63<\/a> \u3092\u3054\u89a7\u304f\u3060\u3055\u3044\u3002<\/p>\n<p>\u52d5\u753b\u3067\u3054\u89a7\u306b\u306a\u308a\u305f\u3044\u65b9\u306f\u3001YouTube \u52d5\u753b <a href=\"https:\/\/youtu.be\/2b1NJEPaYmY\">https:\/\/youtu.be\/2b1NJEPaYmY<\/a> \u3092\u3054\u89a7\u304f\u3060\u3055\u3044\u3002<\/p>\n<ul>\n<li><strong>\u6709\u52b9\u671f\u9593\u30d9\u30fc\u30b9\u306e\u7ba1\u7406<\/strong>: \u958b\u59cb\u6642\u523b\u3068\u7d42\u4e86\u6642\u523b\u3092\u6307\u5b9a\u3059\u308b\u3053\u3068\u3067\u3001\u671f\u9593\u9650\u5b9a\u306e\u30e1\u30f3\u30d0\u30fc\u30b7\u30c3\u30d7\u3092\u5b9f\u73fe<\/li>\n<li><strong>\u5ba3\u8a00\u7684\u8abf\u6574\uff08Declarative Reconciliation\uff09<\/strong>: \u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8\u3092\u300c\u6b63\u300d\u3068\u3057\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u3092\u5e38\u306b\u30b7\u30fc\u30c8\u306b\u5408\u308f\u305b\u308b<\/li>\n<li><strong>GAS + TypeScript<\/strong>: Google Apps Script \u3092 TypeScript \u3067\u958b\u767a\u3057\u3001\u578b\u5b89\u5168\u6027\u3092\u78ba\u4fdd<\/li>\n<\/ul>\n<p><strong>\u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8\u306f\u4ee5\u4e0b\u306e\u69cb\u9020\u306b\u3057\u3066\u304f\u3060\u3055\u3044\uff1a<\/strong><\/p>\n<p>\u30c7\u30fc\u30bf\u7ba1\u7406\u7528\u30b7\u30fc\u30c8\uff08\u30b7\u30fc\u30c8\u540d: <code>\u540c\u671f\u30ea\u30b9\u30c8<\/code>\uff09:<\/p>\n<table>\n<thead>\n<tr>\n<th>A\u5217: \u30b0\u30eb\u30fc\u30d7\u30e1\u30fc\u30eb<\/th>\n<th>B\u5217: \u30e1\u30f3\u30d0\u30fc\u30e1\u30fc\u30eb<\/th>\n<th>C\u5217: \u958b\u59cb\u6642\u523b<\/th>\n<th>D\u5217: \u7d42\u4e86\u6642\u523b<\/th>\n<th>E\u5217: \u30e1\u30f3\u30d0\u30fc\u30b7\u30c3\u30d7\u540d<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>group@example.com<\/td>\n<td>user1@example.com<\/td>\n<td>2025\/01\/01 09:00<\/td>\n<td>2025\/12\/31 23:59<\/td>\n<td>\uff08\u81ea\u52d5\u5165\u529b\uff09<\/td>\n<\/tr>\n<tr>\n<td>group@example.com<\/td>\n<td>user2@example.com<\/td>\n<td>2025\/06\/01 00:00<\/td>\n<td>2025\/06\/30 23:59<\/td>\n<td>\uff08\u81ea\u52d5\u5165\u529b\uff09<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>\u8a2d\u5b9a\u7528\u30b7\u30fc\u30c8\uff08\u30b7\u30fc\u30c8\u540d: <code>\u30b7\u30b9\u30c6\u30e0\u8a2d\u5b9a<\/code>\uff09:<\/p>\n<ul>\n<li><code>B1<\/code>: \u30ed\u30c3\u30af\u30bb\u30eb\uff08\u300cON\u300d\u307e\u305f\u306f\u7a7a\u6b04\uff09<\/li>\n<li><code>B2<\/code>: \u6700\u7d42\u64cd\u4f5c\u6642\u523b\uff08\u81ea\u52d5\u66f4\u65b0\uff09<\/li>\n<\/ul>\n<h2>\u306f\u3058\u3081\u306b<\/h2>\n<p>\u307f\u306a\u3055\u3093\u3001Google Groups \u306e\u30e1\u30f3\u30d0\u30fc\u7ba1\u7406\u3001\u3069\u3046\u3057\u3066\u3044\u307e\u3059\u304b\uff1f<\/p>\n<p>\u5c11\u4eba\u6570\u306e\u30b0\u30eb\u30fc\u30d7\u3067\u3042\u308c\u3070\u3001\u624b\u52d5\u3067\u8ffd\u52a0\u30fb\u524a\u9664\u3059\u308b\u306e\u3082\u82e6\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u3057\u304b\u3057\u3001\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u3054\u3068\u306b\u30b0\u30eb\u30fc\u30d7\u3092\u4f5c\u6210\u3057\u3066\u3044\u305f\u308a\u3001\u671f\u9593\u9650\u5b9a\u3067\u30e1\u30f3\u30d0\u30fc\u3092\u8ffd\u52a0\u3057\u305f\u3044\u5834\u5408\u306b\u306f\u3001\u7ba1\u7406\u304c\u7169\u96d1\u306b\u306a\u308a\u304c\u3061\u3067\u3059\u3002\u300c\u3042\u306e\u4eba\u3001\u3044\u3064\u307e\u3067\u3053\u306e\u30b0\u30eb\u30fc\u30d7\u306b\u3044\u308b\u3093\u3060\u3063\u3051\uff1f\u300d\u300c\u5916\u3059\u306e\u5fd8\u308c\u3066\u305f\u2026\u300d\u3068\u3044\u3046\u7d4c\u9a13\u3001\u3042\u308a\u307e\u305b\u3093\u304b\uff1f<\/p>\n<p>\u4eca\u56de\u306f\u3001Cloud Identity Groups API \u3092\u5229\u7528\u3057\u3066\u3001\u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8\u306b\u8a18\u8f09\u3057\u305f\u30e1\u30f3\u30d0\u30fc\u60c5\u5831\u3092\u57fa\u306b\u3001Google Groups \u306e\u30e1\u30f3\u30d0\u30fc\u3092\u81ea\u52d5\u7684\u306b\u540c\u671f\u3059\u308b\u30b7\u30b9\u30c6\u30e0\u3092\u4f5c\u6210\u3057\u307e\u3057\u305f\u3002\u958b\u59cb\u6642\u523b\u3068\u7d42\u4e86\u6642\u523b\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u3067\u3001\u300c\u3053\u306e\u671f\u9593\u3060\u3051\u30b0\u30eb\u30fc\u30d7\u306b\u8ffd\u52a0\u3059\u308b\u300d\u3068\u3044\u3046\u904b\u7528\u304c\u53ef\u80fd\u306b\u306a\u308a\u307e\u3059\u3002<\/p>\n<h3>\u5c0e\u5165\u65b9\u6cd5<\/h3>\n<p>\u5c0e\u5165\u65b9\u6cd5\u306f\u3068\u3066\u3082\u30b7\u30f3\u30d7\u30eb\u3067\u3001\u4ee5\u4e0b\u3092\u884c\u3046\u3060\u3051\u3067\u3059\u3002<\/p>\n<ol>\n<li>\u30b7\u30fc\u30c8\u3092\u4f5c\u6210<\/li>\n<li>\u30a8\u30c7\u30a3\u30bf\u753b\u9762\u306e\u300c\u30b5\u30fc\u30d3\u30b9\u300d\u304b\u3089\u3001<code>CloudIdentityGroups<\/code> \u3092\u8ffd\u52a0<\/li>\n<li>\u30bd\u30fc\u30b9\u30b3\u30fc\u30c9\u3092\u8cbc\u308a\u4ed8\u3051<\/li>\n<li>\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u8a2d\u5b9a\u3067\u3001\u30bf\u30a4\u30e0\u30be\u30fc\u30f3\u3092\u8a2d\u5b9a<\/li>\n<li>\u30b9\u30af\u30ea\u30d7\u30c8\u30d7\u30ed\u30d1\u30c6\u30a3 <code>EXCLUDED_USERS<\/code> \u306b\u3001\u9664\u5916\u3059\u308b\u30e6\u30fc\u30b6\u30fc\u3092\u8a2d\u5b9a<\/li>\n<li><code>onTimeTriggerd<\/code> \u95a2\u6570\u306b\u30c8\u30ea\u30ac\u30fc\u3092\u8a2d\u5b9a<\/li>\n<\/ol>\n<p>\u307e\u305a\u3001\u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8\u306b\u30012\u3064\u3001\u30b7\u30fc\u30c8\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002\u30b7\u30fc\u30c8\u540d\u306f\u30b7\u30b9\u30c6\u30e0\u3067\u5b9a\u3081\u3066\u3044\u307e\u3059\u3002<br \/>\n2\u756a\u76ee\u306b\u3001\u62e1\u5f35\u6a5f\u80fd\u306e Apps Script \u304b\u3089 GAS \u306e\u8a2d\u5b9a\u753b\u9762\u306b\u79fb\u52d5\u3057\u3001\u30a8\u30c7\u30a3\u30bf\u753b\u9762\u306e\u300c\u30b5\u30fc\u30d3\u30b9\u300d\u304b\u3089\u3001CloudIdentityGroups \u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002<br \/>\n3\u756a\u76ee\u306b\u3001\u30bd\u30fc\u30b9\u30b3\u30fc\u30c9\u3092\u8cbc\u308a\u4ed8\u3051\u307e\u3059\u3002GitHub \u30ea\u30dd\u30b8\u30c8\u30ea\u306b\u3001\u8cbc\u308a\u4ed8\u3051\u3089\u308c\u308b\u72b6\u614b\u306e\u30bd\u30fc\u30b9\u30b3\u30fc\u30c9\u3092\u7528\u610f\u3057\u3066\u3044\u307e\u3059\u306e\u3067\u3001\u305d\u306e\u307e\u307e\u8cbc\u308a\u4ed8\u3051\u3066\u304f\u3060\u3055\u3044\u3002<br \/>\n4\u756a\u76ee\u306b\u3001\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u8a2d\u5b9a\u304b\u3089\u3001\u30bf\u30a4\u30e0\u30be\u30fc\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002<br \/>\n5\u756a\u76ee\u306b\u3001\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u8a2d\u5b9a\u304b\u3089\u3001\u30b9\u30af\u30ea\u30d7\u30c8\u30d7\u30ed\u30d1\u30c6\u30a3 EXCLUDED_USERS \u306b\u3001\u9664\u5916\u3059\u308b\u30e6\u30fc\u30b6\u30fc\u3092\u30ab\u30f3\u30de\u533a\u5207\u308a\u3067\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u5b9f\u884c\u30e6\u30fc\u30b6\u30fc\u81ea\u8eab\u304c\u30ea\u30b9\u30c8\u306b\u306a\u304f\u3066\u3082\u524a\u9664\u3057\u306a\u3044\u305f\u3081\u306e\u4ed5\u7d44\u307f\u3067\u3059\u3002<br \/>\n\u6700\u5f8c\u306b\u3001\u30c8\u30ea\u30ac\u30fc\u753b\u9762\u304b\u3089\u30c8\u30ea\u30ac\u30fc\u3092\u8ffd\u52a0\u3057\u3001onTimeTriggerd \u95a2\u6570\u306b\u30c8\u30ea\u30ac\u30fc\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u30025\u5206\u6bce\u306b\u8d77\u52d5\u3059\u308b\u3088\u3046\u306b\u8a2d\u5b9a\u3057\u3066\u304a\u304f\u306e\u304c\u304a\u52e7\u3081\u3067\u3059\u3002<br \/>\n\u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8\u306b\u3001\u300c\u30b0\u30eb\u30fc\u30d7\u540c\u671f\u300d\u30e1\u30cb\u30e5\u30fc\u3092\u8ffd\u52a0\u3057\u3066\u3042\u308a\u307e\u3059\u3002\u30c6\u30b9\u30c8\u306e\u3068\u304d\u306b\u306f\u624b\u52d5\u5b9f\u884c\u3082\u4fbf\u5229\u3067\u3059\u3002<\/p>\n<p><a href=\"\/blog\/wp-content\/uploads\/2026\/01\/list_sheet.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2026\/01\/list_sheet.png\" alt=\"\" \/><\/a><br \/>\n<a href=\"\/blog\/wp-content\/uploads\/2026\/01\/gas_menu.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2026\/01\/gas_menu.png\" alt=\"\" \/><\/a><br \/>\n<a href=\"\/blog\/wp-content\/uploads\/2026\/01\/service.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2026\/01\/service.png\" alt=\"\" \/><\/a><br \/>\n<a href=\"\/blog\/wp-content\/uploads\/2026\/01\/cloud_identity_groups.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2026\/01\/cloud_identity_groups.png\" alt=\"\" \/><\/a><br \/>\n<a href=\"\/blog\/wp-content\/uploads\/2026\/01\/editor.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2026\/01\/editor.png\" alt=\"\" \/><\/a><br \/>\n<a href=\"\/blog\/wp-content\/uploads\/2026\/01\/excluded_users.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2026\/01\/excluded_users.png\" alt=\"\" \/><\/a><br \/>\n<a href=\"\/blog\/wp-content\/uploads\/2026\/01\/trigger.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2026\/01\/trigger.png\" alt=\"\" \/><\/a><\/p>\n<h3>\u306a\u305c Cloud Identity Groups API \u3092\u9078\u3093\u3060\u306e\u304b<\/h3>\n<p>Google Groups \u306e\u7ba1\u7406\u306b\u306f\u5f93\u6765\u304b\u3089 Google Admin SDK \u306e Directory API\uff08<code>admin.googleapis.com<\/code>\uff09\u3092\u4f7f\u3046\u65b9\u6cd5\u3082\u3042\u308a\u307e\u3059\u3002\u4eca\u56de\u3001Cloud Identity Groups API\uff08<code>cloudidentity.googleapis.com<\/code>\uff09\u3092\u63a1\u7528\u3057\u305f\u5927\u304d\u306a\u7406\u7531\u306f\u3001<strong>Google Workspace \u306e\u7ba1\u7406\u8005\u6a29\u9650\u304c\u306a\u304f\u3066\u3082\u5229\u7528\u3067\u304d\u308b<\/strong>\u3068\u3044\u3046\u70b9\u3067\u3059\u3002<\/p>\n<p>Directory API \u3067\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc\u3092\u64cd\u4f5c\u3059\u308b\u306b\u306f\u3001\u901a\u5e38 Google Workspace \u306e\u7ba1\u7406\u8005\u6a29\u9650\u304c\u5fc5\u8981\u3067\u3059 [<a href=\"http:\/\/zenn.dev\/ohsawa0515\/articles\/cloud-identity-gas\">Cloud Identity\u3067Google\u30b0\u30eb\u30fc\u30d7\u304b\u3089\u30e1\u30f3\u30d0\u30fc\u3092\u8ffd\u52a0\u30fb\u524a\u9664\u3059\u308bGoogle Apps Script\u3092\u66f8\u3044\u305f<\/a>]\u3002\u4e00\u65b9\u3001Cloud Identity Groups API \u3067\u306f\u3001<strong>\u5bfe\u8c61\u30b0\u30eb\u30fc\u30d7\u306e Manager \u4ee5\u4e0a\u306e\u30ed\u30fc\u30eb\u3092\u6301\u3063\u3066\u3044\u308c\u3070\u3001\u8ab0\u3067\u3082\u30e1\u30f3\u30d0\u30fc\u306e\u8ffd\u52a0\u30fb\u524a\u9664\u304c\u3067\u304d\u307e\u3059<\/strong>\u3002\u3053\u308c\u306f\u3001\u90e8\u9580\u3084\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u5358\u4f4d\u3067\u30b0\u30eb\u30fc\u30d7\u7ba1\u7406\u3092\u59d4\u4efb\u3057\u305f\u3044\u5834\u5408\u306b\u975e\u5e38\u306b\u4fbf\u5229\u3067\u3059\u3002<\/p>\n<p>\u305d\u306e\u4ed6\u306b\u3082\u3001Cloud Identity Groups API \u306b\u306f\u4ee5\u4e0b\u306e\u3088\u3046\u306a\u30e1\u30ea\u30c3\u30c8\u304c\u3042\u308a\u307e\u3059\u3002<\/p>\n<ul>\n<li>Google Workspace \u3060\u3051\u3067\u306a\u304f Cloud Identity \u3092\u4f7f\u3046\u74b0\u5883\u3067\u3082\u5229\u7528\u53ef\u80fd<\/li>\n<li>\u30e1\u30f3\u30d0\u30fc\u30b7\u30c3\u30d7\u306e\u6709\u52b9\u671f\u9650\u8a2d\u5b9a\uff08Expiry\uff09\u306a\u3069\u3001\u3088\u308a\u7d30\u304b\u306a\u5236\u5fa1\u304c\u53ef\u80fd<\/li>\n<li>\u30e2\u30c0\u30f3\u306a REST API \u8a2d\u8a08<\/li>\n<\/ul>\n<h3>PAM\uff08\u7279\u6a29\u30a2\u30af\u30bb\u30b9\u7ba1\u7406\uff09\u306e\u4ee3\u66ff\u3068\u3057\u3066<\/h3>\n<p>\u3053\u306e\u30b7\u30b9\u30c6\u30e0\u306f\u3001<strong>\u7c21\u6613\u7684\u306a PAM\uff08Privileged Access Management\uff09<\/strong> \u3068\u3057\u3066\u3082\u6d3b\u7528\u3067\u304d\u307e\u3059\u3002<\/p>\n<p>Google Cloud \u306b\u306f <a href=\"https:\/\/cloud.google.com\/iam\/docs\/pam-overview\">Privileged Access Manager<\/a> \u3068\u3044\u3046\u30b5\u30fc\u30d3\u30b9\u304c\u3042\u308a\u307e\u3059\u304c\u3001\u3053\u308c\u306f\u4e3b\u306b Google Cloud \u306e IAM \u30ed\u30fc\u30eb\u306b\u5bfe\u3059\u308b\u4e00\u6642\u7684\u306a\u6a29\u9650\u6607\u683c\u3092\u7ba1\u7406\u3059\u308b\u3082\u306e\u3067\u3059\u3002<\/p>\n<p>\u3057\u304b\u3057\u3001PAM \u306b\u306f\u3044\u304f\u3064\u304b\u306e\u5236\u9650\u304c\u3042\u308a\u307e\u3059:<\/p>\n<ul>\n<li><strong>PAM \u3067\u5236\u5fa1\u3057\u306b\u304f\u3044 \/ \u3067\u304d\u306a\u3044\u30ea\u30bd\u30fc\u30b9\u304c\u3042\u308b<\/strong>:\n<ul>\n<li><strong>Google Drive<\/strong>: \u30d5\u30a1\u30a4\u30eb\u3078\u306e\u30a2\u30af\u30bb\u30b9\u6a29\u306f IAM \u30ed\u30fc\u30eb\u3067\u306f\u306a\u3044\u305f\u3081\u3001PAM \u306e\u5bfe\u8c61\u5916\u3067\u3059\u3002<\/li>\n<li><strong>IAP (SSH\/TCP)<\/strong>: IAP-secured Tunnel \u3092\u7279\u5b9a\u306e IP \u7bc4\u56f2\uff08Access Level\uff09\u306b\u5236\u9650\u3057\u305f\u3044\u5834\u5408\u3001\u624b\u52d5\u3067\u306e IAM Condition \u5f0f\u306e\u8a18\u8ff0\u304c\u5fc5\u8981\u3067\u3059\u3002PAM \u306e\u52d5\u7684\u6a29\u9650\u4ed8\u4e0e\u306e\u4e2d\u3067\u7ba1\u7406\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093\u3002\u4e8b\u524d\u306b IP \u7bc4\u56f2\u7b49\u3068\u3068\u3082\u306b\u8a2d\u5b9a\u3057\u305f\u30b0\u30eb\u30fc\u30d7\u3067\u7ba1\u7406\u3059\u308b\u65b9\u304c\u9065\u304b\u306b\u30b7\u30f3\u30d7\u30eb\u3067\u3059\uff08<a href=\"https:\/\/issuetracker.google.com\/issues\/274953344\">\u53c2\u8003: Issue 274953344<\/a>\uff09\u3002<\/li>\n<li>\u6ce8: \u7d44\u7e54\u3084\uff08Identity \u306e\uff09\u30d5\u30a9\u30eb\u30c0\u306b\u306f PAM \u306e\u8a2d\u5b9a\u304c\u53ef\u80fd\u3067\u3059<\/li>\n<\/ul>\n<\/li>\n<li><strong>PAM \u3067\u4ed8\u4e0e\u3067\u304d\u306a\u3044\u6a29\u9650\u304c\u3042\u308b<\/strong>: \u4e00\u90e8\u306e\u57fa\u672c\u30ed\u30fc\u30eb\uff08\u30aa\u30fc\u30ca\u30fc\u7b49\uff09\u3084\u7279\u5b9a\u306e\u6a29\u9650\u306f\u3001PAM \u7d4c\u7531\u3067\u306f\u4ed8\u4e0e\u3067\u304d\u307e\u305b\u3093<\/li>\n<li><strong>\u627f\u8a8d\u30ef\u30fc\u30af\u30d5\u30ed\u30fc\u304c\u5fc5\u9808<\/strong>: PAM \u306f\u627f\u8a8d\u30d7\u30ed\u30bb\u30b9\u3092\u7d4c\u308b\u8a2d\u8a08\u306b\u306a\u3063\u3066\u304a\u308a\u3001\u30b7\u30f3\u30d7\u30eb\u306a\u6642\u9650\u4ed8\u304d\u30a2\u30af\u30bb\u30b9\u306b\u306f\u904e\u5270\u306a\u5834\u5408\u304c\u3042\u308a\u307e\u3059\n<ul>\n<li>\u6ce8: \u7121\u6761\u4ef6\u627f\u8a8d\u3082\u53ef\u80fd\u3067\u3059<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>\u4e00\u65b9\u3067\u3001\u300c\u7279\u5b9a\u306e\u671f\u9593\u3060\u3051\u30b0\u30eb\u30fc\u30d7\u306b\u8ffd\u52a0\u3057\u3001\u305d\u306e\u30b0\u30eb\u30fc\u30d7\u7d4c\u7531\u3067\u30ea\u30bd\u30fc\u30b9\u3078\u306e\u30a2\u30af\u30bb\u30b9\u6a29\u3092\u4ed8\u4e0e\u3059\u308b\u300d\u3068\u3044\u3046\u30a2\u30d7\u30ed\u30fc\u30c1\u3067\u3042\u308c\u3070\u3001\u3053\u308c\u3089\u306e\u5236\u9650\u3092\u56de\u907f\u3067\u304d\u307e\u3059\u3002\u4f8b\u3048\u3070:<\/p>\n<ul>\n<li><strong>Google Drive \u306e\u5171\u6709\u30d5\u30a9\u30eb\u30c0<\/strong>\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u671f\u9593\u9650\u5b9a\u3067\u4ed8\u4e0e<\/li>\n<li><strong>IAP \u3067\u4fdd\u8b77\u3055\u308c\u305f\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3<\/strong>\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u671f\u9593\u9650\u5b9a\u3067\u8a31\u53ef<\/li>\n<li>PAM \u3067\u3082\u884c\u3048\u307e\u3059\u304c\u3001\u4ee5\u4e0b\u3082\u7c21\u5358\u3067\u3059\n<ul>\n<li>\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u671f\u9593\u4e2d\u3060\u3051\u3001\u7279\u5b9a\u306e GCS \u30d0\u30b1\u30c3\u30c8\u3084 BigQuery \u30c7\u30fc\u30bf\u30bb\u30c3\u30c8\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u8a31\u53ef<\/li>\n<li>\u5916\u90e8\u30d9\u30f3\u30c0\u30fc\u306e\u30a8\u30f3\u30b8\u30cb\u30a2\u306b\u3001\u5951\u7d04\u671f\u9593\u4e2d\u306e\u307f\u958b\u767a\u74b0\u5883\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u4ed8\u4e0e<\/li>\n<li>\u76e3\u67fb\u5bfe\u5fdc\u306a\u3069\u3067\u3001\u4e00\u6642\u7684\u306b\u7279\u5b9a\u306e\u30ea\u30bd\u30fc\u30b9\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u8a31\u53ef<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>\u3053\u306e\u3088\u3046\u306a\u30b1\u30fc\u30b9\u3067\u306f\u3001\u30b0\u30eb\u30fc\u30d7\u306b IAM \u30dd\u30ea\u30b7\u30fc\u3084\u5171\u6709\u8a2d\u5b9a\u3092\u4ed8\u4e0e\u3057\u3066\u304a\u304d\u3001\u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8\u3067\u300c\u3044\u3064\u304b\u3089\u3044\u3064\u307e\u3067\u300d\u3092\u7ba1\u7406\u3059\u308b\u3060\u3051\u3067\u3001\u7c21\u6613\u7684\u306a\u6642\u9650\u4ed8\u304d\u30a2\u30af\u30bb\u30b9\u5236\u5fa1\u3092\u5b9f\u73fe\u3067\u304d\u307e\u3059\u3002PAM \u306e\u5236\u9650\u306b\u8a72\u5f53\u3059\u308b\u30b1\u30fc\u30b9\u3084\u3001\u30d5\u30eb\u306e PAM \u30bd\u30ea\u30e5\u30fc\u30b7\u30e7\u30f3\u3092\u5c0e\u5165\u3059\u308b\u307b\u3069\u3067\u306f\u306a\u3044\u5c0f\u898f\u6a21\u306a\u30b1\u30fc\u30b9\u3067\u3001\u3053\u306e\u30a2\u30d7\u30ed\u30fc\u30c1\u306f\u7279\u306b\u6709\u52b9\u3067\u3059\u3002<\/p>\n<h2>\u30b7\u30b9\u30c6\u30e0\u306e\u6982\u8981<\/h2>\n<h3>\u6280\u8853\u30b9\u30bf\u30c3\u30af<\/h3>\n<p>\u3053\u306e\u30b7\u30b9\u30c6\u30e0\u306f\u4ee5\u4e0b\u306e\u6280\u8853\u3067\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059\u3002<\/p>\n<ul>\n<li><strong>Google Apps Script (GAS)<\/strong>: \u5b9f\u884c\u74b0\u5883<\/li>\n<li><strong>TypeScript<\/strong>: \u578b\u5b89\u5168\u306a\u958b\u767a<\/li>\n<li><strong>Cloud Identity Groups API<\/strong>: \u30b0\u30eb\u30fc\u30d7\u30e1\u30f3\u30d0\u30fc\u306e\u64cd\u4f5c<\/li>\n<li><strong>Google Sheets<\/strong>: \u30e1\u30f3\u30d0\u30fc\u60c5\u5831\u306e\u7ba1\u7406<\/li>\n<\/ul>\n<h3>\u5168\u4f53\u306e\u6d41\u308c<\/h3>\n<ol>\n<li>\u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8\u306b\u30e1\u30f3\u30d0\u30fc\u60c5\u5831\uff08\u30b0\u30eb\u30fc\u30d7\u30e1\u30fc\u30eb\u3001\u30e1\u30f3\u30d0\u30fc\u30e1\u30fc\u30eb\u3001\u958b\u59cb\u6642\u523b\u3001\u7d42\u4e86\u6642\u523b\uff09\u3092\u8a18\u8f09<\/li>\n<li>GAS \u306e\u30c8\u30ea\u30ac\u30fc\u304c\u5b9a\u671f\u7684\uff08\u4f8b: 5\u5206\u3054\u3068\uff09\u306b\u5b9f\u884c\u3055\u308c\u308b<\/li>\n<li>\u5404\u30b0\u30eb\u30fc\u30d7\u306b\u3064\u3044\u3066\u3001\u300c\u3042\u308b\u3079\u304d\u72b6\u614b\u300d\u3068\u300c\u73fe\u5728\u306e\u72b6\u614b\u300d\u3092\u6bd4\u8f03<\/li>\n<li>\u5dee\u5206\u3092\u8a08\u7b97\u3057\u3001\u8ffd\u52a0\u30fb\u524a\u9664\u3092\u5b9f\u884c<\/li>\n<li>\u7d50\u679c\u3092\u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8\u306b\u66f8\u304d\u623b\u3059<\/li>\n<\/ol>\n<p>\u3053\u306e\u300c\u3042\u308b\u3079\u304d\u72b6\u614b\u3068\u73fe\u5728\u306e\u72b6\u614b\u3092\u6bd4\u8f03\u3057\u3066\u5dee\u5206\u3092\u9069\u7528\u3059\u308b\u300d\u30d1\u30bf\u30fc\u30f3\u306f\u3001<strong>\u5ba3\u8a00\u7684\u8abf\u6574\uff08Declarative Reconciliation\uff09<\/strong>\u3068\u547c\u3070\u308c\u3001Kubernetes \u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306a\u3069\u3067\u3082\u63a1\u7528\u3055\u308c\u3066\u3044\u308b\u8a2d\u8a08\u30d1\u30bf\u30fc\u30f3\u3067\u3059\u3002<\/p>\n<h2>\u5b9f\u88c5\u306e\u8a73\u7d30<\/h2>\n<h3>Cloud Identity Groups API \u306e\u5229\u7528<\/h3>\n<p>\u307e\u305a\u3001Cloud Identity Groups API \u3092\u4f7f\u3046\u305f\u3081\u306e\u6e96\u5099\u306b\u3064\u3044\u3066\u8aac\u660e\u3057\u307e\u3057\u3087\u3046\u3002<\/p>\n<p>GAS \u3067 Cloud Identity Groups API \u3092\u4f7f\u3046\u306b\u306f\u3001\u300cAdvanced Service\u300d\u3068\u3057\u3066\u8ffd\u52a0\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002GAS \u30a8\u30c7\u30a3\u30bf\u3067\u300c\u30b5\u30fc\u30d3\u30b9\u300d\u2192\u300c\u30b5\u30fc\u30d3\u30b9\u3092\u8ffd\u52a0\u300d\u3092\u30af\u30ea\u30c3\u30af\u3057\u3001\u300cCloud Identity Groups API\u300d\u3092\u691c\u7d22\u3057\u3066\u8ffd\u52a0\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u8b58\u5225\u5b50\u306f <code>CloudIdentityGroups<\/code> \u306b\u8a2d\u5b9a\u3057\u307e\u3059\u3002<\/p>\n<p>\u30b0\u30eb\u30fc\u30d7\u3092\u64cd\u4f5c\u3059\u308b\u306b\u306f\u3001\u307e\u305a\u30b0\u30eb\u30fc\u30d7\u306e\u30ea\u30bd\u30fc\u30b9\u540d\uff08<code>groups\/{group_id}<\/code> \u306e\u5f62\u5f0f\uff09\u3092\u53d6\u5f97\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u3053\u308c\u306f <code>lookup<\/code> \u30e1\u30bd\u30c3\u30c9\u3067\u884c\u3044\u307e\u3059\u3002<\/p>\n<pre><code class=\"language-typescript\">export const lookupGroup = (groupEmail: GroupEmail): GroupName | null =&gt; {\n  try {\n    const response = CloudIdentityGroups.Groups.lookup({\n      &#039;groupKey.id&#039;: groupEmail,\n    });\n\n    return (response?.name as GroupName) || null;\n  } catch (e) {\n    return null; \/\/ \u8a73\u7d30\u306f GitHub \u53c2\u7167\n  }\n};<\/code><\/pre>\n<p>\u30e1\u30f3\u30d0\u30fc\u306e\u4e00\u89a7\u53d6\u5f97\u3082\u540c\u69d8\u306b\u5b9f\u88c5\u3067\u304d\u307e\u3059\u3002\u5b9f\u969b\u306b\u306f\u3001\u30da\u30fc\u30b8\u30cd\u30fc\u30b7\u30e7\u30f3\u306b\u5bfe\u5fdc\u3059\u308b\u305f\u3081\u3001<code>nextPageToken<\/code> \u304c\u8fd4\u3055\u308c\u308b\u9650\u308a\u30eb\u30fc\u30d7\u3092\u7d9a\u3051\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002GitHub \u306e\u30bd\u30fc\u30b9\u30b3\u30fc\u30c9\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002<\/p>\n<pre><code class=\"language-typescript\">export const listMembers = (groupName: GroupName): MembershipInfo[] =&gt; {\n  const members: MembershipInfo[] = [];\n\n  try {\n    \/\/ \u5b9f\u969b\u306b\u306f\u3001\u30da\u30fc\u30b8\u30f3\u30b0\u3092\u5229\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002GitHub \u53c2\u7167\n    const response = CloudIdentityGroups.Groups.Memberships.list(groupName);\n\n    if (response.memberships) {\n      for (const membership of response.memberships) {\n        const email = membership.preferredMemberKey?.id;\n        if (email) {\n          members.push({\n            name: membership.name,\n            email,\n          });\n        }\n      }\n    }\n\n    return members;\n  } catch (e) {\n    return []; \/\/ \u8a73\u7d30\u306f GitHub \u53c2\u7167\n  }\n};<\/code><\/pre>\n<h3>\u30e1\u30f3\u30d0\u30fc\u306e\u8ffd\u52a0\u3068\u524a\u9664<\/h3>\n<p>\u30e1\u30f3\u30d0\u30fc\u306e\u8ffd\u52a0\u306b\u306f <code>Memberships.create<\/code> \u3092\u4f7f\u7528\u3057\u307e\u3059\u3002\u8ffd\u52a0\u3057\u305f\u3044\u30e6\u30fc\u30b6\u30fc\u306e\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3068\u3001\u4ed8\u4e0e\u3057\u305f\u3044\u30ed\u30fc\u30eb\uff08\u901a\u5e38\u306f <code>MEMBER<\/code>\uff09\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002<\/p>\n<pre><code class=\"language-typescript\">export const addMember = (\n  groupName: GroupName,\n  memberEmail: MemberEmail\n): MembershipInfo | null =&gt; {\n  try {\n    const membership = {\n      preferredMemberKey: { id: memberEmail },\n      roles: [{ name: &#039;MEMBER&#039; }],\n    };\n\n    const result = CloudIdentityGroups.Groups.Memberships.create(\n      membership,\n      groupName\n    );\n\n    if (result &amp;&amp; result.response) {\n      Logger.log(`\u30e1\u30f3\u30d0\u30fc\u3092\u8ffd\u52a0\u3057\u307e\u3057\u305f: ${memberEmail}`);\n      return {\n        name: result.response.name,\n        email: memberEmail,\n      };\n    }\n    return null;\n  } catch (e) {\n    return null; \/\/ \u8a73\u7d30\u306f GitHub \u53c2\u7167\n  }\n};<\/code><\/pre>\n<p>\u524a\u9664\u3092\u884c\u3046\u969b\u306b\u306f\u3001\u5c11\u3057\u6ce8\u610f\u304c\u5fc5\u8981\u3067\u3059\u3002<code>Memberships.remove<\/code> \u3092\u5b9f\u884c\u3059\u308b\u306b\u306f\u3001\u30e1\u30f3\u30d0\u30fc\u306e\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3067\u306f\u306a\u304f\u3001<strong>\u300c\u30e1\u30f3\u30d0\u30fc\u30b7\u30c3\u30d7\u306e\u30ea\u30bd\u30fc\u30b9\u540d\uff08Relation ID\uff09\u300d<\/strong>\uff08<code>groups\/...\/memberships\/...<\/code> \u306e\u5f62\u5f0f\uff09\u3092\u6307\u5b9a\u3057\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002<\/p>\n<p>\u3053\u308c\u3092\u7279\u5b9a\u3059\u308b\u305f\u3081\u306b <code>Memberships.lookup<\/code> \u3092\u4f7f\u7528\u3057\u307e\u3059\u3002<\/p>\n<pre><code class=\"language-typescript\">const getMembershipName = (\n  groupName: GroupName,\n  memberEmail: MemberEmail\n): MembershipName | null =&gt; {\n  try {\n    const response = CloudIdentityGroups.Groups.Memberships.lookup(groupName, {\n      &#039;memberKey.id&#039;: memberEmail,\n    });\n    return (response?.name as MembershipName) || null;\n  } catch (e) {\n    return null; \/\/ \u5b58\u5728\u3057\u306a\u3044\u5834\u5408\u306f null\n  }\n};<\/code><\/pre>\n<p>\u3053\u306e ID \u3092\u53d6\u5f97\u3067\u304d\u308c\u3070\u3001\u3042\u3068\u306f <code>Memberships.remove<\/code> \u3092\u547c\u3073\u51fa\u3059\u3060\u3051\u3067\u3059\u3002<\/p>\n<h3>\u540c\u671f\u30ed\u30b8\u30c3\u30af<\/h3>\n<p>\u540c\u671f\u306e\u30b3\u30a2\u90e8\u5206\u306f\u3001\u300c\u3042\u308b\u3079\u304d\u72b6\u614b\u300d\u3068\u300c\u73fe\u5728\u306e\u72b6\u614b\u300d\u306e\u5dee\u5206\u3092\u8a08\u7b97\u3059\u308b\u30ed\u30b8\u30c3\u30af\u3067\u3059\u3002<\/p>\n<pre><code class=\"language-typescript\">export const calculateDesiredState = (\n  rows: SheetRow[],\n  currentTime: Date,\n  excludedUsers: Set&lt;MemberEmail&gt;\n): Set&lt;MemberEmail&gt; =&gt; {\n  const desiredEmails = rows\n    .filter(row =&gt; !excludedUsers.has(row.memberEmail))\n    .filter(row =&gt; row.startTime &lt;= currentTime &amp;&amp; currentTime &lt;= row.endTime)\n    .map(row =&gt; row.memberEmail);\n\n  return new Set&lt;MemberEmail&gt;(desiredEmails);\n};\n\nexport const calculateDiff = (\n  desired: Set&lt;MemberEmail&gt;,\n  actual: Set&lt;MemberEmail&gt;\n): { toAdd: MemberEmail[]; toRemove: MemberEmail[] } =&gt; {\n  const toAdd = [...desired].filter(email =&gt; !actual.has(email));\n  const toRemove = [...actual].filter(email =&gt; !desired.has(email));\n\n  return { toAdd, toRemove };\n};<\/code><\/pre>\n<p><code>calculateDesiredState<\/code> \u3067\u306f\u3001\u73fe\u5728\u6642\u523b\u304c\u958b\u59cb\u6642\u523b\u3068\u7d42\u4e86\u6642\u523b\u306e\u7bc4\u56f2\u5185\u306b\u3042\u308b\u884c\u306e\u307f\u3092\u300c\u3042\u308b\u3079\u304d\u30e1\u30f3\u30d0\u30fc\u300d\u3068\u3057\u3066\u62bd\u51fa\u3057\u3066\u3044\u307e\u3059\u3002\u307e\u305f\u3001\u7ba1\u7406\u8005\u306a\u3069\u64cd\u4f5c\u9664\u5916\u30e6\u30fc\u30b6\u30fc\u306f\u30d5\u30a3\u30eb\u30bf\u3067\u9664\u5916\u3057\u307e\u3059\u3002<\/p>\n<p><code>calculateDiff<\/code> \u3067\u306f\u3001\u96c6\u5408\u6f14\u7b97\u3092\u7528\u3044\u3066\u8ffd\u52a0\u30fb\u524a\u9664\u304c\u5fc5\u8981\u306a\u30e1\u30f3\u30d0\u30fc\u3092\u8a08\u7b97\u3057\u3066\u3044\u307e\u3059\u3002\u30b7\u30f3\u30d7\u30eb\u3067\u3059\u304c\u3001\u3053\u306e\u8a2d\u8a08\u306b\u3088\u308a\u300c\u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8\u306e\u72b6\u614b\u304c\u6b63\u300d\u3068\u3044\u3046\u5ba3\u8a00\u7684\u306a\u30e2\u30c7\u30eb\u3092\u5b9f\u73fe\u3067\u304d\u307e\u3059\u3002<\/p>\n<h3>\u6392\u4ed6\u5236\u5fa1<\/h3>\n<p>\u8907\u6570\u306e\u5b9f\u884c\u304c\u540c\u6642\u306b\u8d70\u308b\u3068\u3001\u4e88\u671f\u3057\u306a\u3044\u52d5\u4f5c\u304c\u767a\u751f\u3059\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u305d\u306e\u305f\u3081\u3001\u4e8c\u91cd\u306e\u30ed\u30c3\u30af\u6a5f\u69cb\u3092\u5b9f\u88c5\u3057\u3066\u3044\u307e\u3059\u3002<\/p>\n<ol>\n<li><strong>\u30b7\u30fc\u30c8\u30ed\u30c3\u30af<\/strong>: \u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8\u4e0a\u306e\u7279\u5b9a\u30bb\u30eb (B1) \u306b\u300cON\u300d\u3068\u5165\u529b\u3055\u308c\u3066\u3044\u308b\u5834\u5408\u3001\u51e6\u7406\u3092\u30b9\u30ad\u30c3\u30d7<\/li>\n<li><strong>ScriptLock<\/strong>: GAS \u306e <code>LockService<\/code> \u3092\u4f7f\u3063\u305f\u6392\u4ed6\u5236\u5fa1<\/li>\n<\/ol>\n<p>\u3053\u308c\u306b\u3088\u308a\u3001\u624b\u52d5\u3067\u30b7\u30fc\u30c8\u3092\u7de8\u96c6\u3057\u305f\u3044\u5834\u5408\u306f B1 \u30bb\u30eb\u306b\u300cON\u300d\u3068\u5165\u529b\u3059\u308b\u3060\u3051\u3067\u3001\u30b7\u30b9\u30c6\u30e0\u306e\u52d5\u4f5c\u3092\u4e00\u6642\u505c\u6b62\u3067\u304d\u307e\u3059\u3002<\/p>\n<h2>\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7<\/h2>\n<h3>1. \u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8\u306e\u6e96\u5099<\/h3>\n<p>\u4ee5\u4e0b\u306e\u69cb\u9020\u3067\u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8\u3092\u4f5c\u6210\u3057\u3066\u304f\u3060\u3055\u3044\u3002<\/p>\n<p><strong>\u30c7\u30fc\u30bf\u7ba1\u7406\u7528\u30b7\u30fc\u30c8<\/strong>\uff08\u30b7\u30fc\u30c8\u540d: <code>\u540c\u671f\u30ea\u30b9\u30c8<\/code>\u3001\u518d\u63b2\uff09:<\/p>\n<table>\n<thead>\n<tr>\n<th>A\u5217: \u30b0\u30eb\u30fc\u30d7\u30e1\u30fc\u30eb<\/th>\n<th>B\u5217: \u30e1\u30f3\u30d0\u30fc\u30e1\u30fc\u30eb<\/th>\n<th>C\u5217: \u958b\u59cb\u6642\u523b<\/th>\n<th>D\u5217: \u7d42\u4e86\u6642\u523b<\/th>\n<th>E\u5217: \u30e1\u30f3\u30d0\u30fc\u30b7\u30c3\u30d7\u540d<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>group@example.com<\/td>\n<td>user1@example.com<\/td>\n<td>2025\/01\/01 09:00<\/td>\n<td>2025\/12\/31 23:59<\/td>\n<td>\uff08\u81ea\u52d5\u5165\u529b\uff09<\/td>\n<\/tr>\n<tr>\n<td>group@example.com<\/td>\n<td>user2@example.com<\/td>\n<td>2025\/06\/01 00:00<\/td>\n<td>2025\/06\/30 23:59<\/td>\n<td>\uff08\u81ea\u52d5\u5165\u529b\uff09<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><strong>\u8a2d\u5b9a\u7528\u30b7\u30fc\u30c8<\/strong>\uff08\u30b7\u30fc\u30c8\u540d: <code>\u30b7\u30b9\u30c6\u30e0\u8a2d\u5b9a<\/code>\uff09:<\/p>\n<ul>\n<li><code>B1<\/code>: \u30ed\u30c3\u30af\u30bb\u30eb\uff08\u300cON\u300d\u307e\u305f\u306f\u7a7a\u6b04\uff09<\/li>\n<li><code>B2<\/code>: \u6700\u7d42\u64cd\u4f5c\u6642\u523b\uff08\u81ea\u52d5\u66f4\u65b0\uff09<\/li>\n<\/ul>\n<h3>2. \u30c7\u30d7\u30ed\u30a4<\/h3>\n<pre><code class=\"language-shell\"># \u4f9d\u5b58\u95a2\u4fc2\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\nnpm install\n\n# TypeScript \u306e\u30d3\u30eb\u30c9\nnpm run build\n\n# GAS \u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306b\u30c7\u30d7\u30ed\u30a4\nnpm run deploy<\/code><\/pre>\n<h3>3. \u30c8\u30ea\u30ac\u30fc\u306e\u8a2d\u5b9a<\/h3>\n<p>GAS \u30a8\u30c7\u30a3\u30bf\u3067\u4ee5\u4e0b\u306e\u30c8\u30ea\u30ac\u30fc\u3092\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002<\/p>\n<ul>\n<li>\u5b9f\u884c\u3059\u308b\u95a2\u6570: <code>onTimeTriggered<\/code><\/li>\n<li>\u30a4\u30d9\u30f3\u30c8\u306e\u30bd\u30fc\u30b9: \u6642\u9593\u4e3b\u5c0e\u578b<\/li>\n<li>\u6642\u9593\u30d9\u30fc\u30b9\u306e\u30c8\u30ea\u30ac\u30fc\u306e\u30bf\u30a4\u30d7: \u5206\u30d9\u30fc\u30b9\u306e\u30bf\u30a4\u30de\u30fc<\/li>\n<li>\u6642\u9593\u306e\u9593\u9694: 5\u5206\u304a\u304d<\/li>\n<\/ul>\n<h2>\u904b\u7528\u4e0a\u306e\u6ce8\u610f\u70b9<\/h2>\n<h3>\u64cd\u4f5c\u9664\u5916\u30e6\u30fc\u30b6\u30fc<\/h3>\n<p>\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30fc\u30ca\u30fc\u3084\u7ba1\u7406\u8005\u306a\u3069\u3001\u30b7\u30b9\u30c6\u30e0\u306b\u3088\u308b\u64cd\u4f5c\u3092\u9664\u5916\u3057\u305f\u3044\u30e6\u30fc\u30b6\u30fc\u304c\u3044\u308b\u5834\u5408\u306f\u3001\u30b9\u30af\u30ea\u30d7\u30c8\u30d7\u30ed\u30d1\u30c6\u30a3 <code>EXCLUDED_USERS<\/code> \u306b\u30ab\u30f3\u30de\u533a\u5207\u308a\u3067\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3092\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002<\/p>\n<h3>API \u30af\u30a9\u30fc\u30bf<\/h3>\n<p>Cloud Identity Groups API \u306b\u306f\u5229\u7528\u5236\u9650\u304c\u3042\u308a\u307e\u3059\u3002\u5927\u91cf\u306e\u30b0\u30eb\u30fc\u30d7\u3084\u30e1\u30f3\u30d0\u30fc\u3092\u7ba1\u7406\u3059\u308b\u5834\u5408\u306f\u3001\u30c8\u30ea\u30ac\u30fc\u306e\u9593\u9694\u3092\u8abf\u6574\u3059\u308b\u306a\u3069\u306e\u5de5\u592b\u304c\u5fc5\u8981\u306b\u306a\u308b\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002<\/p>\n<h3>\u91cd\u8981: <code>googlegroups.com<\/code> \u30c9\u30e1\u30a4\u30f3\u306e\u30b0\u30eb\u30fc\u30d7\u306f\u5bfe\u8c61\u5916<\/h3>\n<p>\u3053\u308c\u306f\u5b9f\u969b\u306b\u30cf\u30de\u3063\u305f\u70b9\u306a\u306e\u3067\u3001\u7279\u306b\u5f37\u8abf\u3057\u3066\u304a\u304d\u307e\u3059\u3002<\/p>\n<p><strong>Cloud Identity Groups API \u306f\u3001<code>@googlegroups.com<\/code> \u30c9\u30e1\u30a4\u30f3\u306e\u30b0\u30eb\u30fc\u30d7\u3092\u64cd\u4f5c\u3067\u304d\u307e\u305b\u3093\u3002<\/strong><\/p>\n<p>\u4f8b\u3048\u3070\u3001<code>my-group@googlegroups.com<\/code> \u3068\u3044\u3046\u30b0\u30eb\u30fc\u30d7\u306b\u5bfe\u3057\u3066 <code>lookup<\/code> \u3092\u5b9f\u884c\u3059\u308b\u3068\u3001\u4ee5\u4e0b\u306e\u3088\u3046\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3059\u3002<\/p>\n<pre><code class=\"language-text\">\u30b0\u30eb\u30fc\u30d7\u306e\u691c\u7d22\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f (my-group@googlegroups.com): \nAPI call to cloudidentity.groups.lookup failed with error: \nError(2028): Permission denied for resource my-group@googlegroups.com (or it may not exist).<\/code><\/pre>\n<p>\u3053\u308c\u306f\u3001Cloud Identity Groups API \u304c <strong>Google Workspace \u307e\u305f\u306f Cloud Identity \u4e0a\u3067\u7ba1\u7406\u3055\u308c\u3066\u3044\u308b\u300c\u7d44\u7e54\u306e\u30b0\u30eb\u30fc\u30d7\u300d<\/strong> \u3092\u5bfe\u8c61\u3068\u3057\u3066\u3044\u308b\u305f\u3081\u3067\u3059\u3002\u4e00\u822c\u306e Google \u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u4f5c\u6210\u3057\u305f <code>googlegroups.com<\/code> \u306e\u30b0\u30eb\u30fc\u30d7\u306f\u3001\u7d44\u7e54\u306e\u30a2\u30a4\u30c7\u30f3\u30c6\u30a3\u30c6\u30a3\u7ba1\u7406\u5916\u306b\u3042\u308b\u305f\u3081\u3001\u3053\u306e API \u3067\u306f\u64cd\u4f5c\u3067\u304d\u307e\u305b\u3093\u3002<\/p>\n<p>\u5bfe\u8c61\u306e\u30b0\u30eb\u30fc\u30d7\u304c <code>@googlegroups.com<\/code> \u3067\u306f\u306a\u304f\u3001\u81ea\u7d44\u7e54\u306e\u30c9\u30e1\u30a4\u30f3\uff08\u4f8b: <code>@example.com<\/code>\uff09\u3067\u4f5c\u6210\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002Google Workspace \u306e\u7ba1\u7406\u30b3\u30f3\u30bd\u30fc\u30eb\uff08admin.google.com\uff09\u304b\u3089\u30b0\u30eb\u30fc\u30d7\u3092\u4f5c\u6210\u3059\u308b\u304b\u3001groups.google.com \u304b\u3089\u3001\u30c9\u30e1\u30a4\u30f3\u3092\u6307\u5b9a\u3057\u3066\u30b0\u30eb\u30fc\u30d7\u3092\u4f5c\u6210\u3059\u308c\u3070\u3001\u7d44\u7e54\u306e\u30c9\u30e1\u30a4\u30f3\u306e\u30b0\u30eb\u30fc\u30d7\u3068\u3057\u3066\u6271\u308f\u308c\u307e\u3059\u3002<\/p>\n<h2>\u307e\u3068\u3081<\/h2>\n<p>\u3053\u306e\u8a18\u4e8b\u3067\u306f\u3001Cloud Identity Groups API \u3092\u5229\u7528\u3057\u3066\u3001\u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8\u30d9\u30fc\u30b9\u3067 Google Groups \u306e\u30e1\u30f3\u30d0\u30fc\u3092\u52d5\u7684\u306b\u7ba1\u7406\u3059\u308b\u30b7\u30b9\u30c6\u30e0\u3092\u7d39\u4ecb\u3057\u307e\u3057\u305f\u3002<\/p>\n<ul>\n<li>Cloud Identity Groups API \u306e\u57fa\u672c\u7684\u306a\u4f7f\u3044\u65b9\uff08lookup\u3001listMembers\u3001create\u3001remove\uff09<\/li>\n<li>\u5ba3\u8a00\u7684\u8abf\u6574\u30d1\u30bf\u30fc\u30f3\u306b\u3088\u308b\u540c\u671f\u30ed\u30b8\u30c3\u30af\u306e\u8a2d\u8a08<\/li>\n<li>\u6392\u4ed6\u5236\u5fa1\u306e\u5b9f\u88c5\u65b9\u6cd5<\/li>\n<\/ul>\n<p>\u3053\u306e\u30a2\u30d7\u30ed\u30fc\u30c1\u306f\u3001\u671f\u9593\u9650\u5b9a\u306e\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u30c1\u30fc\u30e0\u3084\u3001\u5b9a\u671f\u7684\u306b\u30e1\u30f3\u30d0\u30fc\u304c\u5165\u308c\u66ff\u308f\u308b\u30b0\u30eb\u30fc\u30d7\u306e\u7ba1\u7406\u306b\u7279\u306b\u6709\u52b9\u3067\u3059\u3002\u30b9\u30d7\u30ec\u30c3\u30c9\u30b7\u30fc\u30c8\u3092\u7de8\u96c6\u3059\u308b\u3060\u3051\u3067\u3001\u81ea\u52d5\u7684\u306b\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u66f4\u65b0\u3055\u308c\u308b\u306e\u3067\u3001\u7ba1\u7406\u306e\u624b\u9593\u3092\u5927\u5e45\u306b\u524a\u6e1b\u3067\u304d\u307e\u3059\u3002<\/p>\n<h2>\u53c2\u8003<\/h2>\n<ul>\n<li><a href=\"https:\/\/cloud.google.com\/identity\/docs\/reference\/rest\">Cloud Identity Groups API<\/a> <a href=\"https:\/\/cloud.google.com\/identity\/docs\/reference\/rest\">https:\/\/cloud.google.com\/identity\/docs\/reference\/rest<\/a><\/li>\n<li><a href=\"https:\/\/developers.google.com\/apps-script\/migration\/groups-cig?hl=ja\">Groups Service \u304b\u3089 Cloud Identity Groups Advanced Service \u306b\u79fb\u884c\u3059\u308b<\/a> <a href=\"https:\/\/developers.google.com\/apps-script\/migration\/groups-cig?hl=ja\">https:\/\/developers.google.com\/apps-script\/migration\/groups-cig?hl=ja<\/a><\/li>\n<li><a href=\"https:\/\/developers.google.com\/apps-script\/reference\/lock\">Google Apps Script &#8211; LockService<\/a> <a href=\"https:\/\/developers.google.com\/apps-script\/reference\/lock\">https:\/\/developers.google.com\/apps-script\/reference\/lock<\/a><\/li>\n<li><a href=\"https:\/\/kubernetes.io\/docs\/concepts\/architecture\/controller\/\">Kubernetes &#8211; Controller Pattern<\/a> <a href=\"https:\/\/kubernetes.io\/docs\/concepts\/architecture\/controller\/\">https:\/\/kubernetes.io\/docs\/concepts\/architecture\/controller\/<\/a><\/li>\n<li><a href=\"https:\/\/kubernetes.io\/ja\/docs\/concepts\/architecture\/controller\/\">ja: Kubernetes &#8211; Controller Pattern<\/a> <a href=\"https:\/\/kubernetes.io\/ja\/docs\/concepts\/architecture\/controller\/\">https:\/\/kubernetes.io\/ja\/docs\/concepts\/architecture\/controller\/<\/a><\/li>\n<li><a href=\"https:\/\/zenn.dev\/ohsawa0515\/articles\/cloud-identity-gas\">Cloud Identity\u3067Google\u30b0\u30eb\u30fc\u30d7\u304b\u3089\u30e1\u30f3\u30d0\u30fc\u3092\u8ffd\u52a0\u30fb\u524a\u9664\u3059\u308bGoogle Apps Script\u3092\u66f8\u3044\u305f<\/a> <a href=\"https:\/\/zenn.dev\/ohsawa0515\/articles\/cloud-identity-gas\">https:\/\/zenn.dev\/ohsawa0515\/articles\/cloud-identity-gas<\/a><\/li>\n<li><a href=\"https:\/\/qiita.com\/nobrin\/items\/e5594150ee99a705c553\">Cloud Identity API\u3067Google Workspace\u306e\u30b0\u30eb\u30fc\u30d7(Google Groups)\u3092\u64cd\u4f5c\u3059\u308b<\/a> <a href=\"https:\/\/qiita.com\/nobrin\/items\/e5594150ee99a705c553\">https:\/\/qiita.com\/nobrin\/items\/e5594150ee99a705c553<\/a><\/li>\n<li><a href=\"http:\/\/officeforest.org\/wp\/google-apps-script%E3%81%A7%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B\/\">Google Apps Script\u3067\u30b0\u30eb\u30fc\u30d7\u30a2\u30c9\u30ec\u30b9\u306e\u4f5c\u6210\u30fb\u524a\u9664\u3092\u884c\u3046\u3010GAS\u3011<\/a> <a href=\"http:\/\/officeforest.org\/wp\/google-apps-script\u3067\u30b0\u30eb\u30fc\u30d7\u3092\u4f5c\u6210\u3059\u308b\/\">http:\/\/officeforest.org\/wp\/google-apps-script\u3067\u30b0\u30eb\u30fc\u30d7\u3092\u4f5c\u6210\u3059\u308b\/<\/a><\/li>\n<\/ul>\n<hr \/>\n<h1>Dynamically Managing Google Groups Members with Cloud Identity Groups API<\/h1>\n<h2>Summary<\/h2>\n<p>This article introduces a system that automatically adds and removes Google Groups members based on spreadsheet data using the Cloud Identity Groups API.<\/p>\n<p>All code is available on GitHub:<br \/>\n<a href=\"https:\/\/github.com\/takotakot\/misc\/tree\/main\/group-from-sheet\">https:\/\/github.com\/takotakot\/misc\/tree\/main\/group-from-sheet<\/a><\/p>\n<p>Slide is available on Google Slide:<br \/>\n<a href=\"https:\/\/docs.google.com\/presentation\/d\/1jsCtTDSjSIWmad2vs777Hs6Kxt4Cqq5R5f8c02jW90I\/edit?slide=id.g3b44be7cb97_0_63#slide=id.g3b44be7cb97_0_63\">https:\/\/docs.google.com\/presentation\/d\/1jsCtTDSjSIWmad2vs777Hs6Kxt4Cqq5R5f8c02jW90I\/edit?slide=id.g3b44be7cb97_0_63#slide=id.g3b44be7cb97_0_63<\/a><\/p>\n<p>YouTube video is available:<br \/>\n<a href=\"https:\/\/youtu.be\/2b1NJEPaYmY\">https:\/\/youtu.be\/2b1NJEPaYmY<\/a><\/p>\n<ul>\n<li><strong>Time-based Management<\/strong>: Enable time-limited memberships by specifying start and end times<\/li>\n<li><strong>Declarative Reconciliation<\/strong>: Treat the spreadsheet as the &quot;source of truth&quot; and keep group state consistent<\/li>\n<li><strong>GAS + TypeScript<\/strong>: Develop with Google Apps Script in TypeScript for type safety<\/li>\n<\/ul>\n<p><strong>Set up your spreadsheet with the following structure:<\/strong><\/p>\n<p>Data Management Sheet (Sheet name: <code>\u540c\u671f\u30ea\u30b9\u30c8<\/code>):<\/p>\n<table>\n<thead>\n<tr>\n<th>Column A: Group Email<\/th>\n<th>Column B: Member Email<\/th>\n<th>Column C: Start Time<\/th>\n<th>Column D: End Time<\/th>\n<th>Column E: Membership Name<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>group@example.com<\/td>\n<td>user1@example.com<\/td>\n<td>2025\/01\/01 09:00<\/td>\n<td>2025\/12\/31 23:59<\/td>\n<td>(Auto-filled)<\/td>\n<\/tr>\n<tr>\n<td>group@example.com<\/td>\n<td>user2@example.com<\/td>\n<td>2025\/06\/01 00:00<\/td>\n<td>2025\/06\/30 23:59<\/td>\n<td>(Auto-filled)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Settings Sheet (Sheet name: <code>\u30b7\u30b9\u30c6\u30e0\u8a2d\u5b9a<\/code>):<\/p>\n<ul>\n<li><code>B1<\/code>: Lock Cell (&quot;ON&quot; or empty)<\/li>\n<li><code>B2<\/code>: Last Operation Time (Auto-updated)<\/li>\n<\/ul>\n<h2>Introduction<\/h2>\n<p>How are you managing your Google Groups members?<\/p>\n<p>For small groups, manually adding and removing members isn&#8217;t too much trouble. However, when creating groups for each project or wanting to add members for a limited time, management can become cumbersome. Ever had the experience of &quot;Wait, how long is this person supposed to be in this group?&quot; or &quot;Oops, forgot to remove them&#8230;&quot;?<\/p>\n<p>In this article, I created a system that automatically synchronizes Google Groups members based on member information written in a spreadsheet using the Cloud Identity Groups API. By setting start and end times, you can implement operations like &quot;add to group only during this period.&quot;<\/p>\n<h3>How to Set Up<\/h3>\n<p>The setup process is very simple; just follow the steps below:<\/p>\n<ol>\n<li>Create the sheets.<\/li>\n<li>Add <code>CloudIdentityGroups<\/code> from &quot;Services&quot; in the Apps Script editor.<\/li>\n<li>Paste the source code.<\/li>\n<li>Set the time zone in the project settings.<\/li>\n<li>Set the <code>EXCLUDED_USERS<\/code> script property.<\/li>\n<li>Set a trigger for the <code>onTimeTriggerd<\/code> function.<\/li>\n<\/ol>\n<p>First, create two sheets in your spreadsheet. The sheet names are predefined by the system.<br \/>\nSecond, go to the Apps Script editor from &quot;Extensions&quot; and add <code>CloudIdentityGroups<\/code> from &quot;Services&quot; in the editor screen.<br \/>\nThird, paste the source code. The source code ready for pasting is available in the <a href=\"https:\/\/github.com\/takotakot\/misc\/tree\/main\/group-from-sheet\">GitHub repository<\/a>, so please use it as is.<br \/>\nFourth, set the time zone from the project settings.<br \/>\nFifth, set the email addresses of users you want to exclude, separated by commas, in the <code>EXCLUDED_USERS<\/code> script property. This prevents the execution user from being removed even if they are not in the list.<br \/>\nFinally, add a trigger from the trigger screen and set it for the <code>onTimeTriggerd<\/code> function. We recommend setting it to run every 5 minutes.<br \/>\nA &quot;Group Sync&quot; menu has been added to the spreadsheet. Manual execution via this menu is also convenient for testing.<\/p>\n<p><a href=\"\/blog\/wp-content\/uploads\/2026\/01\/list_sheet.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2026\/01\/list_sheet.png\" alt=\"\" \/><\/a><br \/>\n<a href=\"\/blog\/wp-content\/uploads\/2026\/01\/gas_menu.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2026\/01\/gas_menu.png\" alt=\"\" \/><\/a><br \/>\n<a href=\"\/blog\/wp-content\/uploads\/2026\/01\/service.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2026\/01\/service.png\" alt=\"\" \/><\/a><br \/>\n<a href=\"\/blog\/wp-content\/uploads\/2026\/01\/cloud_identity_groups.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2026\/01\/cloud_identity_groups.png\" alt=\"\" \/><\/a><br \/>\n<a href=\"\/blog\/wp-content\/uploads\/2026\/01\/editor.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2026\/01\/editor.png\" alt=\"\" \/><\/a><br \/>\n<a href=\"\/blog\/wp-content\/uploads\/2026\/01\/excluded_users.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2026\/01\/excluded_users.png\" alt=\"\" \/><\/a><br \/>\n<a href=\"\/blog\/wp-content\/uploads\/2026\/01\/trigger.png\"><img decoding=\"async\" src=\"\/blog\/wp-content\/uploads\/2026\/01\/trigger.png\" alt=\"\" \/><\/a><\/p>\n<h3>Why Cloud Identity Groups API?<\/h3>\n<p>While Google Admin SDK&#8217;s Directory API has traditionally been used for managing Google Groups, I chose Cloud Identity Groups API for one major reason: <strong>you don&#8217;t need Google Workspace admin rights to use it<\/strong>.<\/p>\n<p>Directory API requires Google Workspace admin privileges to manage group members [<a href=\"https:\/\/zenn.dev\/ohsawa0515\/articles\/cloud-identity-gas\">Wrote a Google Apps Script to add\/remove members from Google Groups with Cloud Identity (Japanese)<\/a>]. With Cloud Identity Groups API, however, <strong>anyone with Manager role or above on the target group can add and remove members<\/strong>. This is extremely useful when you want to delegate group management at the department or project level.<\/p>\n<p>Additional benefits of Cloud Identity Groups API include:<\/p>\n<ul>\n<li>Works with Cloud Identity environments, not just Google Workspace<\/li>\n<li>Finer control like membership expiry settings<\/li>\n<li>Modern REST API design<\/li>\n<\/ul>\n<h3>As an Alternative to PAM (Privileged Access Management)<\/h3>\n<p>This system can also be used as a <strong>lightweight PAM (Privileged Access Management)<\/strong> solution.<\/p>\n<p>Google Cloud offers <a href=\"https:\/\/cloud.google.com\/iam\/docs\/pam-overview\">Privileged Access Manager<\/a>, but it primarily manages temporary privilege escalation for Google Cloud IAM roles.<\/p>\n<p>However, PAM has several limitations:<\/p>\n<ul>\n<li><strong>Incompatibility or Difficulty with Certain Resources<\/strong>:\n<ul>\n<li><strong>Google Drive<\/strong>: Access to shared folders is not managed via IAM roles and is therefore outside PAM&#8217;s scope.<\/li>\n<li><strong>IAP (SSH\/TCP)<\/strong>: Restricting IAP-secured Tunnels to specific IP ranges (Access Levels) requires manual IAM Condition expressions. Such restrictions cannot be managed within PAM&#8217;s dynamic entitlements. It is much simpler to manage access via a group that has been pre-configured with the corresponding IP ranges and conditions (<a href=\"https:\/\/issuetracker.google.com\/issues\/274953344\">Reference: Issue 274953344<\/a>).<\/li>\n<li>Note: PAM can be configured at the organization or folder level.<\/li>\n<\/ul>\n<\/li>\n<li><strong>Some permissions cannot be granted via PAM<\/strong>: Certain basic roles (like Owner) and specific permissions cannot be granted through PAM.<\/li>\n<li><strong>Approval workflow is required<\/strong>: PAM is designed with an approval process, which can be overkill for simple time-limited access.\n<ul>\n<li>Note: Unconditional approval is possible.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>With the approach of &quot;adding someone to a group for a specific period and granting resource access through that group,&quot; you can work around these limitations. For example:<\/p>\n<ul>\n<li>Allow access to specific GCS buckets or BigQuery datasets only during a project period<\/li>\n<li>Grant external vendor engineers access to development environments only during their contract period<\/li>\n<li>Temporarily allow access to specific resources for audit purposes<\/li>\n<li>Grant time-limited access to <strong>Google Drive shared folders<\/strong><\/li>\n<li>Allow time-limited access to <strong>IAP-protected applications<\/strong><\/li>\n<\/ul>\n<p>In such cases, you can achieve lightweight time-limited access control by assigning IAM policies or sharing settings to groups and managing &quot;from when to when&quot; in a spreadsheet. This approach is particularly effective for cases that hit PAM&#8217;s limitations, or for smaller-scale cases where deploying a full PAM solution would be overkill.<\/p>\n<h2>System Overview<\/h2>\n<h3>Technology Stack<\/h3>\n<p>This system consists of the following technologies:<\/p>\n<ul>\n<li><strong>Google Apps Script (GAS)<\/strong>: Runtime environment<\/li>\n<li><strong>TypeScript<\/strong>: Type-safe development<\/li>\n<li><strong>Cloud Identity Groups API<\/strong>: Group member operations<\/li>\n<li><strong>Google Sheets<\/strong>: Member information management<\/li>\n<\/ul>\n<h3>Overall Flow<\/h3>\n<ol>\n<li>Write member information (group email, member email, start time, end time) in the spreadsheet<\/li>\n<li>GAS trigger runs periodically (e.g., every 5 minutes)<\/li>\n<li>For each group, compare &quot;desired state&quot; with &quot;current state&quot;<\/li>\n<li>Calculate the diff and execute additions\/removals<\/li>\n<li>Write results back to the spreadsheet<\/li>\n<\/ol>\n<p>This pattern of &quot;comparing desired state with current state and applying the diff&quot; is called <strong>Declarative Reconciliation<\/strong>, a design pattern also adopted by Kubernetes controllers.<\/p>\n<h2>Implementation Details<\/h2>\n<h3>Using Cloud Identity Groups API<\/h3>\n<p>Let me explain the preparation for using the Cloud Identity Groups API.<\/p>\n<p>To use the Cloud Identity Groups API in GAS, you need to add it as an &quot;Advanced Service.&quot; Click &quot;Services&quot; \u2192 &quot;Add a service&quot; in the GAS editor, search for &quot;Cloud Identity Groups API&quot; and add it. Set the identifier to <code>CloudIdentityGroups<\/code>.<\/p>\n<p>To operate on groups, you first need to get the group&#8217;s resource name (in the format <code>groups\/{group_id}<\/code>). This is done with the <code>lookup<\/code> method.<\/p>\n<pre><code class=\"language-typescript\">export const lookupGroup = (groupEmail: GroupEmail): GroupName | null =&gt; {\n  try {\n    const response = CloudIdentityGroups.Groups.lookup({\n      &#039;groupKey.id&#039;: groupEmail,\n    });\n    return (response?.name as GroupName) || null;\n  } catch (e) {\n    return null; \/\/ Refer to GitHub for details\n  }\n};<\/code><\/pre>\n<p>Listing members can be implemented similarly.<\/p>\n<pre><code class=\"language-typescript\">export const listMembers = (groupName: GroupName): MembershipInfo[] =&gt; {\n  const members: MembershipInfo[] = [];\n  try {\n    \/\/ In practice, please implement pagination. Refer to GitHub for details.\n    const response = CloudIdentityGroups.Groups.Memberships.list(groupName);\n\n    if (response.memberships) {\n      for (const membership of response.memberships) {\n        const email = membership.preferredMemberKey?.id;\n        if (email) {\n          members.push({ name: membership.name, email });\n        }\n      }\n    }\n\n    return members;\n  } catch (e) {\n    return []; \/\/ Refer to GitHub for details\n  }\n};<\/code><\/pre>\n<p>To add a member, use <code>Memberships.create<\/code>. Specify the email address and the role (usually <code>MEMBER<\/code>).<\/p>\n<pre><code class=\"language-typescript\">export const addMember = (\n  groupName: GroupName,\n  memberEmail: MemberEmail\n): MembershipInfo | null =&gt; {\n  try {\n    const membership = {\n      preferredMemberKey: { id: memberEmail },\n      roles: [{ name: &#039;MEMBER&#039; }],\n    };\n\n    const result = CloudIdentityGroups.Groups.Memberships.create(membership, groupName);\n\n    if (result &amp;&amp; result.response) {\n      return { name: result.response.name, email: memberEmail };\n    }\n    return null;\n  } catch (e) {\n    return null; \/\/ Refer to GitHub for details\n  }\n};<\/code><\/pre>\n<p>When removing a member, you need to be a bit careful. To execute <code>Memberships.remove<\/code>, you must specify the <strong>&quot;Membership Resource Name (Relation ID)&quot;<\/strong> (in the format <code>groups\/...\/memberships\/...<\/code>) instead of the member&#8217;s email address.<\/p>\n<p>To identify this, use <code>Memberships.lookup<\/code>.<\/p>\n<pre><code class=\"language-typescript\">const getMembershipName = (\n  groupName: GroupName,\n  memberEmail: MemberEmail\n): MembershipName | null =&gt; {\n  try {\n    const response = CloudIdentityGroups.Groups.Memberships.lookup(groupName, {\n      &#039;memberKey.id&#039;: memberEmail,\n    });\n    return (response?.name as MembershipName) || null;\n  } catch (e) {\n    return null; \/\/ Return null if it doesn&#039;t exist\n  }\n};<\/code><\/pre>\n<p>Once you have this ID, you just need to call <code>Memberships.remove<\/code>.<\/p>\n<h3>Synchronization Logic<\/h3>\n<p>The core part of synchronization is the logic that calculates the diff between &quot;desired state&quot; and &quot;current state.&quot;<\/p>\n<pre><code class=\"language-typescript\">export const calculateDesiredState = (\n  rows: SheetRow[],\n  currentTime: Date,\n  excludedUsers: Set&lt;MemberEmail&gt;\n): Set&lt;MemberEmail&gt; =&gt; {\n  const desiredEmails = rows\n    .filter(row =&gt; !excludedUsers.has(row.memberEmail))\n    .filter(row =&gt; row.startTime &lt;= currentTime &amp;&amp; currentTime &lt;= row.endTime)\n    .map(row =&gt; row.memberEmail);\n\n  return new Set&lt;MemberEmail&gt;(desiredEmails);\n};\n\nexport const calculateDiff = (\n  desired: Set&lt;MemberEmail&gt;,\n  actual: Set&lt;MemberEmail&gt;\n): { toAdd: MemberEmail[]; toRemove: MemberEmail[] } =&gt; {\n  const toAdd = [...desired].filter(email =&gt; !actual.has(email));\n  const toRemove = [...actual].filter(email =&gt; !desired.has(email));\n\n  return { toAdd, toRemove };\n};<\/code><\/pre>\n<p><code>calculateDesiredState<\/code> extracts only the rows where the current time is within the start and end time range as &quot;desired members.&quot; It also filters out excluded users like administrators.<\/p>\n<p><code>calculateDiff<\/code> uses set operations to calculate the members that need to be added or removed. Simple, but this design enables a declarative model where &quot;the spreadsheet state is the truth.&quot;<\/p>\n<h3>Exclusive Control<\/h3>\n<p>When multiple executions run simultaneously, unexpected behavior may occur. Therefore, a dual locking mechanism is implemented:<\/p>\n<ol>\n<li><strong>Sheet Lock<\/strong>: Skips processing if a specific cell (B1) on the spreadsheet contains &quot;ON&quot;.<\/li>\n<li><strong>ScriptLock<\/strong>: Concurrent control using GAS <code>LockService<\/code>.<\/li>\n<\/ol>\n<p>This allows you to pause the system by simply entering &quot;ON&quot; in cell B1 whenever you need to edit the spreadsheet manually.<\/p>\n<h2>Setup<\/h2>\n<h3>1. Prepare Spreadsheet<\/h3>\n<p>Create a spreadsheet with the following structure:<\/p>\n<p><strong>Data Management Sheet<\/strong> (Sheet name: <code>\u540c\u671f\u30ea\u30b9\u30c8<\/code>, reprinted):<\/p>\n<table>\n<thead>\n<tr>\n<th>Column A: Group Email<\/th>\n<th>Column B: Member Email<\/th>\n<th>Column C: Start Time<\/th>\n<th>Column D: End Time<\/th>\n<th>Column E: Membership Name<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>group@example.com<\/td>\n<td>user1@example.com<\/td>\n<td>2025\/01\/01 09:00<\/td>\n<td>2025\/12\/31 23:59<\/td>\n<td>(Auto-filled)<\/td>\n<\/tr>\n<tr>\n<td>group@example.com<\/td>\n<td>user2@example.com<\/td>\n<td>2025\/06\/01 00:00<\/td>\n<td>2025\/06\/30 23:59<\/td>\n<td>(Auto-filled)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>2. Deploy<\/h3>\n<pre><code class=\"language-shell\"># Install dependencies\nnpm install\n\n# Build TypeScript\nnpm run build\n\n# Deploy to GAS project\nnpm run deploy<\/code><\/pre>\n<h3>3. Trigger Settings<\/h3>\n<p>Set the following trigger in the GAS editor:<\/p>\n<ul>\n<li>Function to run: <code>onTimeTriggered<\/code><\/li>\n<li>Event source: Time-driven<\/li>\n<li>Type of time based trigger: Minutes timer<\/li>\n<li>Minute interval: Every 5 minutes<\/li>\n<\/ul>\n<h2>Operational Notes<\/h2>\n<h3>Excluded Users<\/h3>\n<p>If there are users you want to exclude from system operations, such as group owners or administrators, set their email addresses in the script property <code>EXCLUDED_USERS<\/code>, separated by commas.<\/p>\n<h3>API Quotas<\/h3>\n<p>The Cloud Identity Groups API has usage limits. When managing a large number of groups or members, you may need to adjust the trigger interval.<\/p>\n<h3>Important: <code>googlegroups.com<\/code> Groups Are Not Supported<\/h3>\n<p>This is an important point that I actually stumbled upon, so I want to emphasize it.<\/p>\n<p><strong>Cloud Identity Groups API cannot operate on groups with the <code>@googlegroups.com<\/code> domain.<\/strong><\/p>\n<p>For example, if you run <code>lookup<\/code> on a group like <code>my-group@googlegroups.com<\/code>, you will get an error like this (translated from Japanese):<\/p>\n<pre><code class=\"language-text\">Error searching for group (my-group@googlegroups.com): \nAPI call to cloudidentity.groups.lookup failed with error: \nError(2028): Permission denied for resource my-group@googlegroups.com (or it may not exist).<\/code><\/pre>\n<p>This is because Cloud Identity Groups API targets <strong>&quot;organizational groups&quot; managed under Google Workspace or Cloud Identity<\/strong>. Groups created with regular Google accounts on <code>googlegroups.com<\/code> are outside organizational identity management and cannot be operated on with this API.<\/p>\n<p>Make sure your target groups are created under your organization&#8217;s domain (e.g., <code>@example.com<\/code>), not <code>@googlegroups.com<\/code>. Groups created from the Google Workspace admin console (admin.google.com) or from groups.google.com with the domain specified will be treated as organizational domain groups.<\/p>\n<h2>Conclusion<\/h2>\n<p>In this article, I introduced a system that dynamically manages Google Groups members based on spreadsheet data using the Cloud Identity Groups API.<\/p>\n<ul>\n<li>Basic usage of Cloud Identity Groups API (lookup, listMembers, create, remove)<\/li>\n<li>Sync logic design using declarative reconciliation pattern<\/li>\n<li>Implementation of exclusive control<\/li>\n<\/ul>\n<p>This approach is particularly effective for managing time-limited project teams or groups with regularly changing members. Simply editing the spreadsheet automatically updates group members, significantly reducing management overhead.<\/p>\n<h2>References<\/h2>\n<ul>\n<li><a href=\"https:\/\/cloud.google.com\/identity\/docs\/reference\/rest\">Cloud Identity Groups API<\/a><\/li>\n<li><a href=\"https:\/\/developers.google.com\/apps-script\/migration\/groups-cig\">Migrating from Groups Service to Cloud Identity Groups Advanced Service<\/a><\/li>\n<li><a href=\"https:\/\/developers.google.com\/apps-script\/reference\/lock\">Google Apps Script &#8211; LockService<\/a><\/li>\n<li><a href=\"https:\/\/kubernetes.io\/docs\/concepts\/architecture\/controller\/\">Kubernetes &#8211; Controller Pattern<\/a><\/li>\n<li><a href=\"https:\/\/zenn.dev\/ohsawa0515\/articles\/cloud-identity-gas\">Cloud Identity\u3067Google\u30b0\u30eb\u30fc\u30d7\u304b\u3089\u30e1\u30f3\u30d0\u30fc\u3092\u8ffd\u52a0\u30fb\u524a\u9664\u3059\u308bGoogle Apps Script\u3092\u66f8\u3044\u305f<\/a><\/li>\n<li><a href=\"https:\/\/qiita.com\/nobrin\/items\/e5594150ee99a705c553\">Cloud Identity API\u3067Google Workspace\u306e\u30b0\u30eb\u30fc\u30d7(Google Groups)\u3092\u64cd\u4f5c\u3059\u308b<\/a><\/li>\n<li><a href=\"http:\/\/officeforest.org\/wp\/google-apps-script\u3067\u30b0\u30eb\u30fc\u30d7\u3092\u4f5c\u6210\u3059\u308b\/\">Google Apps Script\u3067\u30b0\u30eb\u30fc\u30d7\u30a2\u30c9\u30ec\u30b9\u306e\u4f5c\u6210\u30fb\u524a\u9664\u3092\u884c\u3046\u3010GAS\u3011<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Cloud Identity Groups API \u3092\u4f7f\u3063\u3066 Google Groups \u306e\u30e1\u30f3\u30d0\u30fc\u3092\u52d5\u7684\u306b\u7ba1\u7406\u3059\u308b English follows Japanese. \u307e\u3068\u3081 \u3053\u306e\u8a18\u4e8b\u3067\u306f\u3001Cloud Identity [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[28,13],"tags":[],"class_list":["post-618","post","type-post","status-publish","format-standard","hentry","category-google-apps-script-gas","category-google-cloud"],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/p4dIdP-9Y","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/tako.nakano.net\/blog\/wp-json\/wp\/v2\/posts\/618","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tako.nakano.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tako.nakano.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tako.nakano.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tako.nakano.net\/blog\/wp-json\/wp\/v2\/comments?post=618"}],"version-history":[{"count":4,"href":"https:\/\/tako.nakano.net\/blog\/wp-json\/wp\/v2\/posts\/618\/revisions"}],"predecessor-version":[{"id":632,"href":"https:\/\/tako.nakano.net\/blog\/wp-json\/wp\/v2\/posts\/618\/revisions\/632"}],"wp:attachment":[{"href":"https:\/\/tako.nakano.net\/blog\/wp-json\/wp\/v2\/media?parent=618"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tako.nakano.net\/blog\/wp-json\/wp\/v2\/categories?post=618"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tako.nakano.net\/blog\/wp-json\/wp\/v2\/tags?post=618"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}