CUDly is a self-hosted tool. Two CLI subcommands handle the one-time credential bootstrap for Azure and GCP: configure-azure and configure-gcp. Both write credentials to AWS Secrets Manager, where the CUDly server reads them at runtime.
These are one-time setup operations, not part of the regular analysis/purchase workflow.
For the full Terraform deployment guide (supported runtimes, quick-deploy script, manual steps, tfvars reference, accessing the dashboard after deploy), see the Deployment (self-hosted via Terraform) section in the README.
The ci-cd-permissions/ module within each environment provisions the CI/CD deploy identity and is applied once, manually, by a privileged operator. The main deploy workflow (.github/workflows/deploy-*.yml) then runs terraform init / plan / apply against the environment directory using OIDC keyless authentication.
- An AWS profile with
secretsmanager:ListSecretsandsecretsmanager:UpdateSecretpermissions on the secrets created by the Terraform deployment. - The target Secrets Manager secret must already exist (created by the Terraform deployment). Both commands locate the secret by listing secrets with a name prefix (
<stack-name>-AzureCredentialsor<stack-name>-GCPCredentials) and updating the first match.
Store Azure Service Principal credentials in Secrets Manager.
cudly configure-azure [flags]
| Flag | Default | Description |
|---|---|---|
--stack-name |
cudly |
Deployment stack name prefix. Used to locate the Secrets Manager secret (<stack-name>-AzureCredentials). Must match the name used when the Terraform deployment created the secret. |
--profile |
(AWS default chain) | AWS profile to use when writing to Secrets Manager. |
--tenant-id |
Azure AD Tenant ID (UUID format). | |
--client-id |
Azure Service Principal Client ID (UUID format). | |
--client-secret |
Azure Service Principal Client Secret. Read securely from stdin if omitted. | |
--subscription-id |
Azure Subscription ID (UUID format). | |
--interactive / -i |
false |
Prompt for all credential fields interactively, even if some are provided as flags. |
--skip-setup |
false |
Skip the guided Azure CLI steps (az login, az account list, az ad sp create-for-rbac). Use when you already have a Service Principal and just want to store the credentials. |
When --skip-setup is not set, the command runs an interactive guided flow:
az login- opens a browser window for Azure authentication (can be skipped at the prompt).az account list --output table- lists subscriptions so you can identify the Subscription ID.az ad sp create-for-rbac --name CUDly --role "Reservations Administrator" --scopes /subscriptions/<id>- creates a Service Principal with the correct role (can be skipped).
After the guided steps (or immediately with --skip-setup), the command prompts for or accepts any missing credential fields and writes them as JSON to the <stack-name>-AzureCredentials secret.
# Provide all credentials as flags (--client-secret is read from a variable to avoid shell history)
AZURE_SECRET="$(cat /run/secrets/azure-client-secret)"
cudly configure-azure \
--stack-name prod-cudly \
--profile cudly-admin \
--tenant-id 12345678-1234-1234-1234-123456789012 \
--client-id 87654321-4321-4321-4321-210987654321 \
--client-secret "$AZURE_SECRET" \
--subscription-id aaaabbbb-cccc-dddd-eeee-ffffaaaabbbb \
--skip-setup# Let the command guide you through Azure CLI setup and credential collection
cudly configure-azure --stack-name prod-cudly --profile cudly-adminThe Service Principal must have the Reservations Administrator role at the subscription scope. This is the minimum role needed to create reservation purchases.
# Example: grant the role manually if the guided step was skipped
az role assignment create \
--assignee "<client-id>" \
--role "Reservations Administrator" \
--scope "/subscriptions/<subscription-id>"Store GCP Service Account credentials in Secrets Manager.
cudly configure-gcp [flags]
| Flag | Short | Default | Description |
|---|---|---|---|
--stack-name |
cudly |
Deployment stack name prefix. Used to locate the secret (<stack-name>-GCPCredentials). Must match the name used when the Terraform deployment created the secret. |
|
--profile |
(AWS default chain) | AWS profile to use when writing to Secrets Manager. | |
--credentials-file |
-f |
Path to a GCP Service Account JSON key file. Supports ~ expansion. |
|
--project-id |
GCP Project ID. Overrides the value embedded in the credentials file if set. | ||
--interactive / -i |
false |
Prompt for the credentials file path interactively. | |
--skip-setup |
false |
Skip the guided gcloud steps (gcloud auth login, create service account, grant roles, create key). Use when you already have a JSON key file. |
When --skip-setup is not set, the command runs an interactive guided flow:
gcloud auth login- opens a browser window for GCP authentication.gcloud projects list- lists projects so you can identify your Project ID.gcloud config set project <id>- sets the active project.- Creates a
cudly-service-accountService Account with the display name "CUDly Service Account". - Grant IAM roles via SDK - creates or validates the project custom role
cudlyCommitmentPurchaser, then grants it androles/compute.viewerto the Service Account. gcloud iam service-accounts keys create ~/cudly-gcp-key.json- downloads a JSON key to your home directory.
After the guided steps (or with --skip-setup --credentials-file <path>), the command reads and validates the JSON file and writes it to the <stack-name>-GCPCredentials secret.
# Store an existing key file
cudly configure-gcp \
--stack-name prod-cudly \
--profile cudly-admin \
--credentials-file ~/cudly-gcp-key.json \
--skip-setup# Let the command guide you through gcloud setup
cudly configure-gcp --stack-name prod-cudly --profile cudly-adminThe Service Account needs the following roles:
| Role | Purpose |
|---|---|
roles/compute.viewer |
Read Compute Engine resources and commitment operations |
projects/PROJECT_ID/roles/cudlyCommitmentPurchaser |
Purchase commitments with only compute.commitments.create |
projects/PROJECT_ID/roles/cudlyRecommendationReader |
List Compute commitment recommendations with only recommender.usageCommitmentRecommendations.list |
The setup operator needs iam.roles.get, iam.roles.create,
resourcemanager.projects.getIamPolicy, and resourcemanager.projects.setIamPolicy
for this step. These setup permissions are not granted to the Service Account.
An existing custom role must have exactly its one permission and be enabled (not
disabled and not soft-deleted); the wizard refuses incompatible roles rather
than changing them. It also refuses
to widen an existing conditional-only grant for this Service Account.
Rerunning the wizard does not remove broad grants from older installations.
Compute Engine commitment recommendations are read from the project-scoped
resource projects/PROJECT_ID/locations/REGION/recommenders/google.compute.commitment.UsageCommitmentRecommender.
That list call needs recommender.usageCommitmentRecommendations.list on the
project, which the wizard grants through cudlyRecommendationReader. The
Recommender API (recommender.googleapis.com) must be enabled in the Service
Account's project; the wizard prints this prerequisite but does not enable it.
Scope notes:
- Project scope: the grant above is a project binding. Nothing is granted on a
billing account, folder or organization, because the pinned client never
queries those parents. Billing-account roles such as
roles/billing.viewerare not needed for this call. - Other services: the GCP provider also queries Cloud SQL and Memorystore
recommenders (
google.cloudsql.instance.PerformanceRecommender,google.memorystore.redis.PerformanceRecommender). The wizard provisions nothing for them; those calls return a permission error, which is reported as a warning for that service while other services continue.
Rerunning the wizard does not remove broad grants from older installations.
Earlier setup wizard versions granted
roles/compute.admin to the cudly-service-account Service Account before
minting its key. Rerunning the current wizard adds the narrow roles but
preserves existing bindings, so every older installation needs a one-time
operator review. Work through this checklist with the project owner's
authorization:
-
Inventory broad grants. List the project IAM policy and identify CUDly identities that still hold Compute Admin:
gcloud projects get-iam-policy PROJECT_ID \ --flatten=bindings[].members \ --filter=bindings.role:roles/compute.admin \ --format='table(bindings.role, bindings.members, bindings.condition)'The project policy does not include inherited bindings, so check folder and organization policies too:
gcloud projects get-ancestors-iam-policy PROJECT_ID \ --flatten=policy.bindings[].members \ --filter=policy.bindings.role:roles/compute.admin \ --format='table(type, id, policy.bindings.role, policy.bindings.members, policy.bindings.condition)'Also save the full JSON output of both commands with
--format=jsonand without--flattenor--filter, so all bindings and conditions remain available for review. Record each exact Service Account email, resource type and ID, role, and condition. Do not change live IAM during inventory. -
Establish the narrow permissions. Rerun
cudly configure-gcp(or grant manually) so the Service Account holdsroles/compute.viewerandprojects/PROJECT_ID/roles/cudlyCommitmentPurchaseras described above. An existing custom role must contain exactlycompute.commitments.createand be enabled; the wizard refuses incompatible roles rather than changing them.Before removing anything, inspect the full project policy for both exact role names and the exact Service Account member, including conditions:
gcloud projects get-iam-policy PROJECT_ID --format=json gcloud iam roles describe cudlyCommitmentPurchaser \ --project=PROJECT_ID --format=json
Confirm the custom role is not deleted, its stage is not
DISABLED, andincludedPermissionscontains onlycompute.commitments.create. Confirm the replacement bindings' conditions permit the intended workflow. A successful permission check while Compute Admin remains granted cannot prove the narrow roles are sufficient: the broad grant masks missing access. -
Revoke each approved binding at its recorded scope. Obtain owner authorization for each exact resource, member, role, and condition. Record the approved rollback (restoring that exact binding and condition) before removal. As the operator, use the command matching the resource that owns the binding. These examples remove only unconditional grants:
gcloud projects remove-iam-policy-binding PROJECT_ID \ --member=serviceAccount:cudly-service-account@PROJECT_ID.iam.gserviceaccount.com \ --role=roles/compute.admin --condition=None gcloud resource-manager folders remove-iam-policy-binding FOLDER_ID \ --member=serviceAccount:cudly-service-account@PROJECT_ID.iam.gserviceaccount.com \ --role=roles/compute.admin --condition=None gcloud organizations remove-iam-policy-binding ORGANIZATION_ID \ --member=serviceAccount:cudly-service-account@PROJECT_ID.iam.gserviceaccount.com \ --role=roles/compute.admin --condition=None
Substitute the inventoried Service Account email, which may belong to a different project. For a conditional binding, replace
--condition=Nonewith the exact owner-approved condition using--conditionor--condition-from-file; preserve its expression, title, and description. Do not use--allor remove other bindings. Recheck the owning policy after each removal and confirm only the authorized binding changed. -
Verify after removal without purchasing. Authenticate with the installation's existing Service Account key. A missing key requires separate authorization for credential recovery, not automatic key creation. Obtain a token as that Service Account and call Cloud Resource Manager's
projects.testIamPermissionsREST endpoint:Before authenticating, ensure the
auth/impersonate_service_accountgcloud configuration property andCLOUDSDK_AUTH_IMPERSONATE_SERVICE_ACCOUNTenvironment variable are unset. If either is set, stop and select an owner-approved configuration and environment without impersonation; do not automatically change existing settings. Substitute the inventoried Service Account email in the token command below.gcloud auth activate-service-account --key-file=~/cudly-gcp-key.json CUDLY_GCP_TOKEN="$(gcloud auth print-access-token --account=cudly-service-account@PROJECT_ID.iam.gserviceaccount.com)" curl --fail-with-body --request POST \ "https://cloudresourcemanager.googleapis.com/v1/projects/PROJECT_ID:testIamPermissions" \ --header "Authorization: Bearer ${CUDLY_GCP_TOKEN}" \ --header 'Content-Type: application/json' \ --data '{"permissions":["compute.commitments.create","compute.commitments.list"]}' unset CUDLY_GCP_TOKEN
The response omits permissions the caller lacks. Confirm both requested permissions appear, then run the installation's read-only CUDly analysis workflow with the same credentials and project and confirm it completes without permission errors. Do not purchase a commitment as a verification step. If validation fails, stop, switch back to the authorized operator identity with
gcloud auth login, then follow the authorized rollback; do not broaden roles without owner approval. Also switch back to your operator identity before any further operator actions after successful validation.Record the inventory result and any deployment-specific follow-up. Never delete bindings or rotate keys without per-resource authorization.
This documentation change was verified with offline fixtures and a local HTTP endpoint, not a live cloud account. Those checks cover command data shape and request construction; they do not prove deployment-specific IAM propagation or the live read-only workflow. Record those coverage gaps honestly when reviewing or applying the migration.
If you manage Cloud SQL or Memorystore commitments, you may need additional roles. Check the GCP documentation for the minimum required permissions per commitment type.
The command expects a standard GCP Service Account JSON key file (type service_account) with at minimum:
{
"type": "service_account",
"project_id": "your-project-id",
"client_email": "cudly-service-account@your-project-id.iam.gserviceaccount.com",
"private_key": "<YOUR_SERVICE_ACCOUNT_PRIVATE_KEY>"
}Any missing required field causes the command to exit with an error before writing to Secrets Manager.
| Scenario | Notes |
|---|---|
| First-time setup | Deploy with Terraform first (terraform/environments/<cloud>/). The secrets are created as part of that deployment; these commands only update them. |
| Rotating credentials | Both commands update (not create) the secret, so they can be run again to rotate credentials without redeploying. |