Cloud Audit Logging のチュートリアル(第 2 世代)


このチュートリアルでは、Cloud Audit Logs のトリガーを使用して、イベント ドリブンの Cloud Functions を作成、デプロイ、トリガーする方法について説明します。

Cloud Functions(第 2 世代)独自の機能の一つとして、Cloud Audit Logs のエントリから関数をトリガーする機能があります。多くの Google Cloud プロダクトでは、プロダクト内で重要なアクションが発生すると、Cloud Audit Logs にログが書き込まれます。これらのログエントリは Cloud Functions の実行をリアルタイムでトリガーできるため、ユーザーはこれらを自動的に処理したり、操作することができます。

これらのログは、Google Cloud 上のさまざまなイベントによって生成され、ほとんどの Google Cloud プロダクトを対象としています。そのため、Cloud Audit Logs トリガーを使用すると、Google Cloud のほとんどの状態変化に対応する関数を作成できます。

このチュートリアルでは、Cloud Audit Logs トリガーを使用して、新しく作成された Compute Engine インスタンスに、作成したエンティティ(個人またはサービス アカウント)の名前のラベルを付ける方法を学習します。

Cloud Audit Logs を初めて使用する場合は、Cloud Audit Logs のドキュメントで詳細をご確認ください。

目標

  • Compute Engine VM インスタンスの作成時に Cloud Audit Logs イベントを受信する Cloud Functions のイベント ドリブン関数を作成する。
  • 関数をトリガーする Compute Engine VM インスタンスを作成する。インスタンスが作成されたときに、作成したエンティティ(ユーザーまたはサービス アカウント)の名前をラベルとしてインスタンスに設定する。

費用

このドキュメントでは、Google Cloud の次の課金対象のコンポーネントを使用します。

  • Cloud Functions
  • Cloud Build
  • Pub/Sub
  • Artifact Registry
  • Eventarc
  • Cloud Logging
  • Compute Engine

詳細については、Cloud Functions の料金をご覧ください。

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを生成できます。 新しい Google Cloud ユーザーは無料トライアルをご利用いただける場合があります。

始める前に

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. Google Cloud Console の [プロジェクト セレクタ] ページで、Google Cloud プロジェクトを選択または作成します。

    プロジェクト セレクタに移動

  3. Google Cloud プロジェクトで課金が有効になっていることを確認します

  4. Cloud Functions, Cloud Run, Cloud Build, Artifact Registry, Eventarc, Logging, Compute Engine, and Pub/Sub API を有効にします。

    API を有効にする

  5. Google Cloud Console の [プロジェクト セレクタ] ページで、Google Cloud プロジェクトを選択または作成します。

    プロジェクト セレクタに移動

  6. Google Cloud プロジェクトで課金が有効になっていることを確認します

  7. Cloud Functions, Cloud Run, Cloud Build, Artifact Registry, Eventarc, Logging, Compute Engine, and Pub/Sub API を有効にします。

    API を有効にする

  8. Cloud SDK をインストールし、初期化します
  9. gcloud コンポーネントを更新します。
  10. gcloud components update

    コマンド プロンプトが必要な場合は、Google Cloud Shell を使用できます。Google Cloud Shell は、Google Cloud SDK がすでに含まれているコマンドライン環境であるため、インストールする必要はありません。Google Cloud SDK も Google Compute Engine の仮想マシンにプリインストールされています。

  11. 開発環境を準備します。

前提条件

  1. Google Cloud コンソールで [IAM と管理] > [監査ログ] ページを開きます。

    [IAM と管理] > [監査ログ] ページに移動

  2. Compute Engine API で、Cloud Audit Logs の管理読み取りデータ読み取りデータ書き込みの各ログタイプを有効にします。

    Compute Engine の監査ログを有効にするスクリーンショット

  3. Compute Engine サービス アカウントに Editor ロールがあるかどうかを確認します。このサービス アカウントは、Cloud Functions のサービス ID として使用されます。

    [IAM と管理] > [IAM] ページに移動します

    テーブルで PROJECT_NUMBER-compute@developer.gserviceaccount.com エントリを見つけて、Roles 列を確認します。列に Editor が含まれている場合は、次の手順をスキップできます。それ以外の場合は、次の手順に進んで、必要なロールをサービス アカウントに割り当てます。

  4. プロジェクトの Compute Engine サービス アカウントに eventarc.eventReceiver ロールを付与します。

    PROJECT_ID=$(gcloud config get-value project)
    PROJECT_NUMBER=$(gcloud projects list --filter="project_id:$PROJECT_ID" --format='value(project_number)')
    
    # Allow service account token creation
    gcloud projects add-iam-policy-binding $PROJECT_ID \
     --member serviceAccount:$PROJECT_NUMBER-compute@developer.gserviceaccount.com \
     --role roles/eventarc.eventReceiver
    
  5. Pub/Sub トリガーが関数を実行できるように、プロジェクトの Compute Engine サービス アカウントに run.invoker ロールを付与します。

    PROJECT_ID=$(gcloud config get-value project)
    PROJECT_NUMBER=$(gcloud projects list --filter="project_id:$PROJECT_ID" --format='value(project_number)')
    
    # Allow service account token creation
    gcloud projects add-iam-policy-binding $PROJECT_ID \
     --member serviceAccount:$PROJECT_NUMBER-compute@developer.gserviceaccount.com \
     --role roles/run.invoker
    
  6. プロジェクトの Compute Engine サービス アカウントに compute.instanceAdmin ロールを付与して、VM インスタンスを取得してラベルを設定するために必要な権限を関数コードに付与します。

    PROJECT_ID=$(gcloud config get-value project)
    PROJECT_NUMBER=$(gcloud projects list --filter="project_id:$PROJECT_ID" --format='value(project_number)')
    
    # Allow service account token creation
    gcloud projects add-iam-policy-binding $PROJECT_ID \
     --member serviceAccount:$PROJECT_NUMBER-compute@developer.gserviceaccount.com \
     --role roles/compute.instanceAdmin
    

アプリケーションの準備

  1. ローカルマシンにサンプルアプリのリポジトリのクローンを作成します。

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    また、zip 形式のサンプルをダウンロードしてファイルを抽出してもかまいません。

    Python

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git

    また、zip 形式のサンプルをダウンロードしてファイルを抽出してもかまいません。

    Go

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git

    また、zip 形式のサンプルをダウンロードしてファイルを抽出してもかまいません。

    Java

    git clone https://github.com/GoogleCloudPlatform/java-docs-samples.git

    または、zip 形式のサンプルをダウンロードし、ファイルを抽出してもかまいません。

  2. Cloud Audit Logs にアクセスするための Cloud Functions サンプルコードがあるディレクトリに移動します。

    Node.js

    cd nodejs-docs-samples/functions/v2/autoLabelInstance/

    Python

    cd python-docs-samples/functions/v2/label_gce_instance/

    Go

    cd golang-samples/functions/functionsv2/label_gce_instance/

    Java

    cd java-docs-samples/functions/v2/label-compute-instance/

  3. サンプルコードを見てみましょう。

    Node.js

    const functions = require('@google-cloud/functions-framework');
    
    const compute = require('@google-cloud/compute');
    const instancesClient = new compute.InstancesClient();
    
    // Register a CloudEvent callback with the Functions Framework that labels
    // newly-created GCE instances with the entity (person or service account)
    // that created them.
    functions.cloudEvent('autoLabelInstance', async cloudEvent => {
      // Extract parameters from the CloudEvent + Cloud Audit Log data
      const payload = cloudEvent.data && cloudEvent.data.protoPayload;
      const authInfo = payload && payload.authenticationInfo;
      let creator = authInfo && authInfo.principalEmail;
    
      // Get relevant VM instance details from the CloudEvent's `subject` property
      // Example value:
      //   compute.googleapis.com/projects/<PROJECT>/zones/<ZONE>/instances/<INSTANCE>
      const params = cloudEvent.subject && cloudEvent.subject.split('/');
    
      // Validate data
      if (!creator || !params || params.length !== 7) {
        throw new Error('Invalid event structure');
      }
    
      // Format the 'creator' parameter to match GCE label validation requirements
      creator = creator.toLowerCase().replace(/\W/g, '_');
    
      // Get the newly-created VM instance's label fingerprint
      // This is required by the Compute Engine API to prevent duplicate labels
      const getInstanceRequest = {
        project: params[2],
        zone: params[4],
        instance: params[6],
      };
      const [instance] = await instancesClient.get(getInstanceRequest);
    
      // Label the instance with its creator
      const setLabelsRequest = Object.assign(
        {
          instancesSetLabelsRequestResource: {
            labels: {creator},
            labelFingerprint: instance.labelFingerprint,
          },
        },
        getInstanceRequest
      );
    
      return instancesClient.setLabels(setLabelsRequest);
    });

    Python

    import re
    
    from google.api_core.exceptions import GoogleAPIError
    from google.cloud import compute_v1
    from google.cloud.compute_v1.types import compute
    
    instances_client = compute_v1.InstancesClient()
    
    # CloudEvent function that labels newly-created GCE instances
    # with the entity (user or service account) that created them.
    #
    # @param {object} cloudevent A CloudEvent containing the Cloud Audit Log entry.
    # @param {object} cloudevent.data.protoPayload The Cloud Audit Log entry.
    def label_gce_instance(cloudevent):
        # Extract parameters from the CloudEvent + Cloud Audit Log data
        payload = cloudevent.data.get("protoPayload", dict())
        auth_info = payload.get("authenticationInfo", dict())
        creator = auth_info.get("principalEmail")
    
        # Get relevant VM instance details from the cloudevent's `subject` property
        # Example value:
        #   compute.googleapis.com/projects/<PROJECT_ID>/zones/<ZONE_ID>/instances/<INSTANCE_NAME>
        instance_params = cloudevent["subject"].split("/")
    
        # Validate data
        if not creator or not instance_params or len(instance_params) != 7:
            # This is not something retries will fix, so don't throw an Exception
            # (Thrown exceptions trigger retries *if* you enable retries in GCF.)
            print("ERROR: Invalid `principalEmail` and/or CloudEvent `subject`.")
            return
    
        instance_project = instance_params[2]
        instance_zone = instance_params[4]
        instance_name = instance_params[6]
    
        # Format the 'creator' parameter to match GCE label validation requirements
        creator = re.sub("\\W", "_", creator.lower())
    
        # Get the newly-created VM instance's label fingerprint
        # This is required by the Compute Engine API to prevent duplicate labels
        instance = instances_client.get(
            project=instance_project, zone=instance_zone, instance=instance_name
        )
    
        # Construct API call to label the VM instance with its creator
        request_init = {
            "project": instance_project,
            "zone": instance_zone,
            "instance": instance_name,
        }
        request_init[
            "instances_set_labels_request_resource"
        ] = compute.InstancesSetLabelsRequest(
            label_fingerprint=instance.label_fingerprint, labels={"creator": creator}
        )
        request = compute.SetLabelsInstanceRequest(request_init)
    
        # Perform instance-labeling API call
        try:
            instances_client.set_labels_unary(request)
            print(f"Labelled VM instance {instance_name} with creator: {creator}")
        except GoogleAPIError as e:
            # Swallowing the exception means failed invocations WON'T be retried
            print("Label operation failed", e)
    
            # Uncomment the line below to retry failed invocations.
            # (You'll also have to enable retries in Cloud Functions itself.)
            # raise e
    
        return
    
    

    Go

    
    // Package helloworld provides a set of Cloud Functions samples.
    package helloworld
    
    import (
    	"context"
    	"fmt"
    	"log"
    	"regexp"
    	"strings"
    
    	compute "cloud.google.com/go/compute/apiv1"
    	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
    	"github.com/cloudevents/sdk-go/v2/event"
    	computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
    	"google.golang.org/protobuf/proto"
    )
    
    // AuditLogEntry represents a LogEntry as described at
    // https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
    type AuditLogEntry struct {
    	ProtoPayload *AuditLogProtoPayload `json:"protoPayload"`
    }
    
    // AuditLogProtoPayload represents AuditLog within the LogEntry.protoPayload
    // See https://cloud.google.com/logging/docs/reference/audit/auditlog/rest/Shared.Types/AuditLog
    type AuditLogProtoPayload struct {
    	MethodName         string                 `json:"methodName"`
    	ResourceName       string                 `json:"resourceName"`
    	AuthenticationInfo map[string]interface{} `json:"authenticationInfo"`
    }
    
    var client *compute.InstancesClient
    
    func init() {
    	// Create an Instances Client
    	var err error
    	client, err = compute.NewInstancesRESTClient(context.Background())
    	if err != nil {
    		log.Fatalf("Failed to create instances client: %s", err)
    	}
    
    	functions.CloudEvent("label-gce-instance", labelGceInstance)
    }
    
    // Cloud Function that receives GCE instance creation Audit Logs, and adds a
    // `creator` label to the instance.
    func labelGceInstance(ctx context.Context, ev event.Event) error {
    	// Extract parameters from the Cloud Event and Cloud Audit Log data
    	logentry := &AuditLogEntry{}
    	if err := ev.DataAs(logentry); err != nil {
    		err = fmt.Errorf("event.DataAs() : %w", err)
    		log.Printf("Error parsing proto payload: %s", err)
    		return err
    	}
    	payload := logentry.ProtoPayload
    	creator, ok := payload.AuthenticationInfo["principalEmail"]
    	if !ok {
    		err := fmt.Errorf("principalEmail not found in cloud event payload: %v", payload)
    		log.Printf("creator email not found: %s", err)
    		return err
    	}
    
    	// Get relevant VM instance details from the event's `subject` property
    	// Subject format:
    	// compute.googleapis.com/projects/<PROJECT>/zones/<ZONE>/instances/<INSTANCE>
    	paths := strings.Split(ev.Subject(), "/")
    	if len(paths) < 6 {
    		return fmt.Errorf("invalid event subject: %s", ev.Subject())
    	}
    	project := paths[2]
    	zone := paths[4]
    	instance := paths[6]
    
    	// Sanitize the `creator` label value to match GCE label requirements
    	// See https://cloud.google.com/compute/docs/labeling-resources#requirements
    	labelSanitizer := regexp.MustCompile("[^a-z0-9_-]+")
    	creatorstring := labelSanitizer.ReplaceAllString(strings.ToLower(creator.(string)), "_")
    
    	// Get the newly-created VM instance's label fingerprint
    	// This is a requirement of the Compute Engine API and avoids duplicate labels
    	inst, err := client.Get(ctx, &computepb.GetInstanceRequest{
    		Project:  project,
    		Zone:     zone,
    		Instance: instance,
    	})
    	if err != nil {
    		err = fmt.Errorf("could not retrieve GCE instance: %s", err)
    		log.Print(err)
    		return err
    	}
    	if v, ok := inst.Labels["creator"]; ok {
    		// Instance already has a creator label.
    		log.Printf("instance %s already labeled with creator: %s", instance, v)
    		return nil
    	}
    
    	// Add the creator label to the instance
    	op, err := client.SetLabels(ctx, &computepb.SetLabelsInstanceRequest{
    		Project:  project,
    		Zone:     zone,
    		Instance: instance,
    		InstancesSetLabelsRequestResource: &computepb.InstancesSetLabelsRequest{
    			LabelFingerprint: proto.String(inst.GetLabelFingerprint()),
    			Labels: map[string]string{
    				"creator": creatorstring,
    			},
    		},
    	})
    	if err != nil {
    		log.Fatalf("Could not label GCE instance: %s", err)
    	}
    	log.Printf("Creator label added to %s in operation %v", instance, op)
    	return nil
    }
    

    Java

    import com.google.cloud.compute.v1.GetInstanceRequest;
    import com.google.cloud.compute.v1.Instance;
    import com.google.cloud.compute.v1.InstancesClient;
    import com.google.cloud.compute.v1.InstancesSetLabelsRequest;
    import com.google.cloud.compute.v1.SetLabelsInstanceRequest;
    import com.google.cloud.functions.CloudEventsFunction;
    import com.google.gson.Gson;
    import com.google.gson.JsonObject;
    import com.google.gson.JsonSyntaxException;
    import io.cloudevents.CloudEvent;
    import java.nio.charset.StandardCharsets;
    import java.util.logging.Logger;
    
    public class AutoLabelInstance implements CloudEventsFunction {
      private static final Logger logger = Logger.getLogger(AutoLabelInstance.class.getName());
    
      @Override
      public void accept(CloudEvent event) throws Exception {
        // Extract CloudEvent data
        if (event.getData() != null) {
          String cloudEventData = new String(event.getData().toBytes(), StandardCharsets.UTF_8);
    
          // Convert data to JSON
          JsonObject eventData;
          try {
            Gson gson = new Gson();
            eventData = gson.fromJson(cloudEventData, JsonObject.class);
          } catch (JsonSyntaxException error) {
            throw new RuntimeException("CloudEvent data is not valid JSON: " + error.getMessage());
          }
    
          // Extract the Cloud Audit Logging entry from the data's protoPayload
          JsonObject payload = eventData.getAsJsonObject("protoPayload");
          JsonObject auth = payload.getAsJsonObject("authenticationInfo");
    
          // Extract the email address of the authenticated user
          // (or service account on behalf of third party principal) making the request
          String creator = auth.get("principalEmail").getAsString();
          if (creator == null) {
            throw new RuntimeException("`principalEmail` not found in protoPayload.");
          }
          // Format the 'creator' parameter to match GCE label validation requirements
          creator = creator.toLowerCase().replaceAll("\\W", "-");
    
          // Get relevant VM instance details from the CloudEvent `subject` property
          // Example: compute.googleapis.com/projects/<PROJECT>/zones/<ZONE>/instances/<INSTANCE>
          String subject = event.getSubject();
          if (subject == null || subject == "") {
            throw new RuntimeException("Missing CloudEvent `subject`.");
          }
          String[] params = subject.split("/");
    
          // Validate data
          if (params.length < 7) {
            throw new RuntimeException("Can not parse resource from CloudEvent `subject`: " + subject);
          }
          String project = params[2];
          String zone = params[4];
          String instanceName = params[6];
    
          // Instantiate the Compute Instances client
          try (InstancesClient instancesClient = InstancesClient.create()) {
            // Get the newly-created VM instance's label fingerprint
            // This is required by the Compute Engine API to prevent duplicate labels
            GetInstanceRequest getInstanceRequest =
                GetInstanceRequest.newBuilder()
                    .setInstance(instanceName)
                    .setProject(project)
                    .setZone(zone)
                    .build();
            Instance instance = instancesClient.get(getInstanceRequest);
            String fingerPrint = instance.getLabelFingerprint();
    
            // Label the instance with its creator
            SetLabelsInstanceRequest setLabelRequest =
                SetLabelsInstanceRequest.newBuilder()
                    .setInstance(instanceName)
                    .setProject(project)
                    .setZone(zone)
                    .setInstancesSetLabelsRequestResource(
                        InstancesSetLabelsRequest.newBuilder()
                            .putLabels("creator", creator)
                            .setLabelFingerprint(fingerPrint)
                            .build())
                    .build();
    
            instancesClient.setLabelsAsync(setLabelRequest);
            logger.info(
                String.format(
                    "Adding label, \"{'creator': '%s'}\", to instance, \"%s\".",
                    creator, instanceName));
          } catch (Exception error) {
            throw new RuntimeException(
                String.format(
                    "Error trying to label VM instance, %s: %s", instanceName, error.toString()));
          }
        }
      }
    }

関数のデプロイ

Cloud Audit Logs トリガーを使用して関数をデプロイするには、サンプルコード(Java の場合は pom.xml ファイル)を含むディレクトリで次のコマンドを実行します。

Node.js

gcloud functions deploy nodejs-cal-function \
--gen2 \
--runtime=nodejs20 \
--region=REGION \
--source=. \
--entry-point=autoLabelInstance \
--trigger-location=REGION \
--trigger-event-filters="type=google.cloud.audit.log.v1.written" \
--trigger-event-filters="serviceName=compute.googleapis.com" \
--trigger-event-filters="methodName=v1.compute.instances.insert"

サポートされている Node.js バージョンのランタイム ID を指定して関数を実行するには、--runtime フラグを使用します。

Python

gcloud functions deploy python-cal-function \
--gen2 \
--runtime=python312 \
--region=REGION \
--source=. \
--entry-point=label_gce_instance \
--trigger-location=REGION \
--trigger-event-filters="type=google.cloud.audit.log.v1.written" \
--trigger-event-filters="serviceName=compute.googleapis.com" \
--trigger-event-filters="methodName=v1.compute.instances.insert"

サポートされている Python バージョンのランタイム ID を指定して関数を実行するには、--runtime フラグを使用します。

Go

gcloud functions deploy go-cal-function \
--gen2 \
--runtime=go121 \
--region=REGION \
--source=. \
--entry-point=label-gce-instance \
--trigger-location=REGION \
--trigger-event-filters="type=google.cloud.audit.log.v1.written" \
--trigger-event-filters="serviceName=compute.googleapis.com" \
--trigger-event-filters="methodName=v1.compute.instances.insert"

サポートされている Go バージョンのランタイム ID を指定して関数を実行するには、--runtime フラグを使用します。

Java

gcloud functions deploy java-cal-function \
--gen2 \
--runtime=java17 \
--region=REGION \
--source=. \
--entry-point=functions.AutoLabelInstance \
--memory=512MB \
--trigger-location=REGION \
--trigger-event-filters="type=google.cloud.audit.log.v1.written" \
--trigger-event-filters="serviceName=compute.googleapis.com" \
--trigger-event-filters="methodName=v1.compute.instances.insert"

サポートされている Java バージョンのランタイム ID を指定して関数を実行するには、--runtime フラグを使用します。

上記のデプロイ コマンドでは、VM の作成に対応する次のイベント フィルタ パラメータを指定します。

  • type: Cloud Audit Logs イベントタイプ(google.cloud.audit.log.v1.written)。
  • serviceName: ログエントリを生成した Google Cloud サービスの名前。この場合は compute.googleapis.com です。
  • methodName: ログエントリを生成した API メソッドの名前。この場合は v1.compute.instances.insert です。

関数のトリガー

関数がデプロイされたら、動作を確認します。

  1. Compute Engine VM インスタンスを作成します。

    gcloud compute instances create YOUR_INSTANCE_NAME --zone YOUR_ZONE
    

    または、Google Cloud コンソールに移動して、[VM を作成] をクリックします。

  2. 次のコマンドを実行して、インスタンスに適切なラベルが設定されていることを確認します。

    gcloud compute instances describe YOUR_INSTANCE_NAME \
        --zone YOUR_ZONE \
        --format 'value(labels)'
    

    creator=YOURNAMEYOUR_DOMAIN 形式のラベルが付いているはずです。

クリーンアップ

このチュートリアルで使用したリソースについて、Google Cloud アカウントに課金されないようにするには、リソースを含むプロジェクトを削除するか、プロジェクトを維持して個々のリソースを削除します。

プロジェクトの削除

課金をなくす最も簡単な方法は、チュートリアル用に作成したプロジェクトを削除することです。

プロジェクトを削除するには:

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

Cloud Functions の関数の削除

Cloud Functions を削除しても、Cloud Storage に保存されたリソースは削除されません。

このチュートリアルで作成した Cloud Functions の関数を削除するには、次のコマンドを実行します。

Node.js

gcloud functions delete nodejs-cal-function --gen2 --region REGION 

Python

gcloud functions delete python-cal-function --gen2 --region REGION 

Go

gcloud functions delete go-cal-function --gen2 --region REGION 

Java

gcloud functions delete java-cal-function --gen2 --region REGION 

Google Cloud コンソールから Cloud Functions を削除することもできます。

Compute Engine VM インスタンスを削除する

このチュートリアルで作成した Compute Engine VM インスタンスを削除するには、次のコマンドを実行します。

gcloud compute instances delete YOUR_INSTANCE_NAME --zone YOUR_ZONE