CloudPlans Docs

GCP

CloudPlans does not use service account keys. It uses Workload Identity Federation: your project trusts the CloudPlans AWS identity (account 605134468875, role cloudplansbatch), which impersonates a service account of your project that only has the roles of the services you want to manage.

The console generates it for you: in Credentials → Add Credential → GCP, click Helper: Google Cloud Shell setup, tick only the services you use and run the script in Google Cloud Shell. Then paste the Project ID in the credential (Create).

Permissions per service

The roles are granted at project level, only for the services you select.

Service Predefined role API that must be enabled
GCP Compute Engine VM (Stoppable) roles/compute.instanceAdmin.v1 compute.googleapis.com
GCP Compute Engine VM (Vertical Resize) roles/compute.instanceAdmin.v1 compute.googleapis.com
GCP GKE Cluster (Scalable) roles/container.clusterAdmin container.googleapis.com
GCP Cloud SQL (Stoppable) roles/cloudsql.editor sqladmin.googleapis.com
GCP Memorystore Redis (Stoppable) roles/redis.editor redis.googleapis.com
GCP Dataproc Cluster (Stoppable) roles/dataproc.editor dataproc.googleapis.com
GCP Vertex AI Endpoint (Scalable) roles/aiplatform.user aiplatform.googleapis.com

Always required: the role roles/browser for the CloudPlans service account cloudsschedulerfromaws@cloudsscheduler.iam.gserviceaccount.com, so CloudPlans can resolve your numeric project number when you save the credential (read only, it cannot start, stop or delete anything).

The script enables the APIs for Workload Identity Federation and Compute Engine. If you use any other service in the table, make sure its API is enabled in the project (Terraform below enables them all).

Google Cloud Shell script

# Run this in Google Cloud Shell, in the project you want CloudPlans to manage.
PROJECT_ID="<your-gcp-project-id>"
POOL_ID="cloudplans-pool"
PROVIDER_ID="cloudplans-aws"
SA_NAME="cloudplans-scheduler"
AWS_ACCOUNT="605134468875"
AWS_ROLE_NAME="cloudplansbatch"

gcloud config set project "$PROJECT_ID"

# 1. Enable required APIs
gcloud services enable iam.googleapis.com sts.googleapis.com \
  iamcredentials.googleapis.com compute.googleapis.com cloudresourcemanager.googleapis.com

# 2. Create the Workload Identity Pool + AWS provider that trusts our account
gcloud iam workload-identity-pools create "$POOL_ID" \
  --location=global --display-name="CloudPlans pool"

gcloud iam workload-identity-pools providers create-aws "$PROVIDER_ID" \
  --location=global --workload-identity-pool="$POOL_ID" \
  --account-id="$AWS_ACCOUNT" \
  --attribute-mapping="google.subject=assertion.arn,attribute.aws_role=assertion.arn"

# 3. Create the service account that will perform start/stop/resize
gcloud iam service-accounts create "$SA_NAME" \
  --display-name="CloudPlans Scheduler"

SA_EMAIL="$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com"
PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')
PRINCIPAL="principalSet://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/attribute.aws_role/arn:aws:sts::$AWS_ACCOUNT:assumed-role/$AWS_ROLE_NAME/$AWS_ROLE_NAME"

# 4. Allow the federated AWS principal to impersonate the SA
gcloud iam service-accounts add-iam-policy-binding "$SA_EMAIL" \
  --role="roles/iam.workloadIdentityUser" \
  --member="$PRINCIPAL"

# 5. Grant the SA permissions for the resource types you selected:
#    GCP Compute Engine VM (Stoppable), GCP Compute Engine VM (Vertical Resize), GCP GKE Cluster (Scalable), GCP Cloud SQL (Stoppable), GCP Memorystore Redis (Stoppable), GCP Dataproc Cluster (Stoppable), GCP Vertex AI Endpoint (Scalable)
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:$SA_EMAIL" \
  --role="roles/compute.instanceAdmin.v1"

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:$SA_EMAIL" \
  --role="roles/container.clusterAdmin"

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:$SA_EMAIL" \
  --role="roles/cloudsql.editor"

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:$SA_EMAIL" \
  --role="roles/redis.editor"

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:$SA_EMAIL" \
  --role="roles/dataproc.editor"

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:$SA_EMAIL" \
  --role="roles/aiplatform.user"

# 6. Let CloudPlans resolve your numeric project number when you save the
#    credential (read-only, cannot start/stop/delete anything by itself).
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:cloudsschedulerfromaws@cloudsscheduler.iam.gserviceaccount.com" \
  --role="roles/browser"

echo "----- Paste the following in CloudPlans -----"
echo "GCP Project ID  : $PROJECT_ID"
echo "Service Account : $SA_EMAIL"

Terraform

variable "project_id" {
  type = string
}

data "google_project" "this" {
  project_id = var.project_id
}

resource "google_project_service" "apis" {
  for_each = toset([
    "aiplatform.googleapis.com",
    "cloudresourcemanager.googleapis.com",
    "compute.googleapis.com",
    "container.googleapis.com",
    "dataproc.googleapis.com",
    "iam.googleapis.com",
    "iamcredentials.googleapis.com",
    "redis.googleapis.com",
    "sqladmin.googleapis.com",
    "sts.googleapis.com"
  ])
  project            = var.project_id
  service            = each.key
  disable_on_destroy = false
}

resource "google_iam_workload_identity_pool" "cloudplans" {
  project                   = var.project_id
  workload_identity_pool_id = "cloudplans-pool"
  display_name              = "CloudPlans pool"
  depends_on                = [google_project_service.apis]
}

resource "google_iam_workload_identity_pool_provider" "aws" {
  project                            = var.project_id
  workload_identity_pool_id          = google_iam_workload_identity_pool.cloudplans.workload_identity_pool_id
  workload_identity_pool_provider_id = "cloudplans-aws"

  aws {
    account_id = "605134468875"
  }

  attribute_mapping = {
    "google.subject"     = "assertion.arn"
    "attribute.aws_role" = "assertion.arn"
  }
}

resource "google_service_account" "cloudplans" {
  project      = var.project_id
  account_id   = "cloudplans-scheduler"
  display_name = "CloudPlans Scheduler"
}

resource "google_service_account_iam_member" "federation" {
  service_account_id = google_service_account.cloudplans.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.cloudplans.name}/attribute.aws_role/arn:aws:sts::605134468875:assumed-role/cloudplansbatch/cloudplansbatch"
}

resource "google_project_iam_member" "roles" {
  for_each = toset([
    "roles/aiplatform.user",
    "roles/cloudsql.editor",
    "roles/compute.instanceAdmin.v1",
    "roles/container.clusterAdmin",
    "roles/dataproc.editor",
    "roles/redis.editor"
  ])
  project = var.project_id
  role    = each.key
  member  = "serviceAccount:${google_service_account.cloudplans.email}"
}

resource "google_project_iam_member" "project_number_lookup" {
  project = var.project_id
  role    = "roles/browser"
  member  = "serviceAccount:cloudsschedulerfromaws@cloudsscheduler.iam.gserviceaccount.com"
}

output "service_account" {
  value = google_service_account.cloudplans.email
}