Workflow Reference: leftsize.yml, Inputs and Outputs
LeftSize runs as a step in a GitHub Actions workflow in your repository, usually .github/workflows/leftsize.yml. The onboarding page at leftsize.com generates this file for you; this page shows it in full, explains each part, and lists every input and output of the leftsize/leftsize-action step in scan mode. For the stats mode inputs and outputs, see Reports.
Terms used on this page
- Workflow: a YAML file in
.github/workflows/that GitHub Actions runs on a schedule or on demand. - Matrix: a list of values that runs the same job once per value. LeftSize uses it to scan several subscriptions or accounts from one workflow, one job each.
- Job summary: the report a workflow run shows on its summary page in the Actions tab. LeftSize writes its findings per rule, estimated savings and any problems there.
- Scope: how LeftSize groups findings into issues: an Azure resource group, an AWS account and region, or a Cloudflare account. Each rule gets one issue per scope.
Azure workflow
name: LeftSize Cost Optimization Scan
on:
schedule:
- cron: '17 6 * * *'
workflow_dispatch:
permissions:
id-token: write
contents: read
jobs:
leftsize-scan:
runs-on: ubuntu-latest
name: Scan ${{ matrix.environment }}
strategy:
fail-fast: false
matrix:
environment: [MAIN]
steps:
- name: Azure Login (OIDC)
uses: azure/login@v2
with:
client-id: ${{ secrets[format('AZURE_CLIENT_ID_{0}', matrix.environment)] }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets[format('AZURE_SUBSCRIPTION_ID_{0}', matrix.environment)] }}
- name: Run LeftSize Scan
id: leftsize
uses: leftsize/leftsize-action@v1
with:
installation-id: ${{ secrets.LEFTSIZE_INSTALLATION_ID }}
repository-token: ${{ secrets.LEFTSIZE_REPOSITORY_TOKEN }}
cloud-provider: azure
environment-name: ${{ matrix.environment }}
verbose: false
- name: View Results Summary
if: always()
run: |
echo "LeftSize scan complete for ${{ matrix.environment }}!"
echo "Found ${{ steps.leftsize.outputs.findings-count }} optimization opportunities"
echo "Results submitted: ${{ steps.leftsize.outputs.findings-submitted }}"
Secrets: AZURE_CLIENT_ID_MAIN, AZURE_SUBSCRIPTION_ID_MAIN, AZURE_TENANT_ID, LEFTSIZE_INSTALLATION_ID and LEFTSIZE_REPOSITORY_TOKEN. See Azure Setup.
AWS workflow
name: LeftSize Cost Optimization Scan
on:
schedule:
- cron: '17 6 * * *'
workflow_dispatch:
permissions:
id-token: write
contents: read
jobs:
leftsize-scan:
runs-on: ubuntu-latest
name: Scan ${{ matrix.environment }}
strategy:
fail-fast: false
matrix:
environment: [MAIN]
steps:
- name: AWS Login (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets[format('AWS_ROLE_ARN_{0}', matrix.environment)] }}
aws-region: us-east-1
- name: Run LeftSize Scan
id: leftsize
uses: leftsize/leftsize-action@v1
with:
installation-id: ${{ secrets.LEFTSIZE_INSTALLATION_ID }}
repository-token: ${{ secrets.LEFTSIZE_REPOSITORY_TOKEN }}
cloud-provider: aws
aws-regions: ${{ vars.AWS_REGIONS }}
environment-name: ${{ matrix.environment }}
verbose: false
- name: View Results Summary
if: always()
run: |
echo "LeftSize scan complete for ${{ matrix.environment }}!"
echo "Found ${{ steps.leftsize.outputs.findings-count }} optimization opportunities"
echo "Results submitted: ${{ steps.leftsize.outputs.findings-submitted }}"
Secrets: AWS_ROLE_ARN_MAIN, LEFTSIZE_INSTALLATION_ID and LEFTSIZE_REPOSITORY_TOKEN. Variable: AWS_REGIONS. See AWS Setup.
Cloudflare workflow
name: LeftSize Cloudflare Scan
on:
schedule:
- cron: '17 6 * * *'
workflow_dispatch:
permissions:
contents: read
jobs:
leftsize-cloudflare:
runs-on: ubuntu-latest
steps:
- name: Run LeftSize Scan
id: leftsize
uses: leftsize/leftsize-action@v1
with:
installation-id: ${{ secrets.LEFTSIZE_INSTALLATION_ID }}
repository-token: ${{ secrets.LEFTSIZE_REPOSITORY_TOKEN }}
cloud-provider: cloudflare
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare-account-ids: ${{ vars.CLOUDFLARE_ACCOUNT_IDS }}
verbose: false
- name: View Results Summary
if: always()
run: |
echo "LeftSize Cloudflare scan complete!"
echo "Found ${{ steps.leftsize.outputs.findings-count }} findings"
echo "Results submitted: ${{ steps.leftsize.outputs.findings-submitted }}"
Secrets: CLOUDFLARE_API_TOKEN, LEFTSIZE_INSTALLATION_ID and LEFTSIZE_REPOSITORY_TOKEN. Variable (optional): CLOUDFLARE_ACCOUNT_IDS. Cloudflare has no OIDC for API tokens, so there is no login step and no id-token: write; the action signs in with the read-only token. See Cloudflare Setup.
Scanning several clouds from one workflow
Each run of the action scans one cloud. To scan AWS, Azure and Cloudflare from one workflow, give each cloud its own job:
name: LeftSize
on:
schedule:
- cron: '17 6 * * *'
workflow_dispatch:
permissions:
contents: read
jobs:
aws:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: AWS Login (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN_MAIN }}
aws-region: us-east-1
- name: Run LeftSize Scan
uses: leftsize/leftsize-action@v1
with:
installation-id: ${{ secrets.LEFTSIZE_INSTALLATION_ID }}
repository-token: ${{ secrets.LEFTSIZE_REPOSITORY_TOKEN }}
cloud-provider: aws
aws-regions: ${{ vars.AWS_REGIONS }}
azure:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Azure Login (OIDC)
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID_MAIN }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID_MAIN }}
- name: Run LeftSize Scan
uses: leftsize/leftsize-action@v1
with:
installation-id: ${{ secrets.LEFTSIZE_INSTALLATION_ID }}
repository-token: ${{ secrets.LEFTSIZE_REPOSITORY_TOKEN }}
cloud-provider: azure
cloudflare:
runs-on: ubuntu-latest
steps:
- name: Run LeftSize Scan
uses: leftsize/leftsize-action@v1
with:
installation-id: ${{ secrets.LEFTSIZE_INSTALLATION_ID }}
repository-token: ${{ secrets.LEFTSIZE_REPOSITORY_TOKEN }}
cloud-provider: cloudflare
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare-account-ids: ${{ vars.CLOUDFLARE_ACCOUNT_IDS }}
Leave out the jobs for clouds you don’t use. To scan several AWS accounts or Azure subscriptions, add the matrix from the workflows above to that job.
- The jobs run in parallel and each submits its own findings. All of them count as one repository for your plan.
- Permissions per job. Only the AWS and Azure jobs need
id-token: writefor OIDC. The Cloudflare job inheritscontents: readfrom the workflow and gets no OIDC token. - One cloud never closes another’s findings. A scan only adds and updates findings. A Cloudflare scan doesn’t resolve AWS or Azure findings because they’re missing from it, and the other way around.
- Daily submission limit. An installation can submit findings 20 times per day (UTC), across all its repositories. Each job, and each matrix entry, is one submission, so a three-cloud workflow that runs once a day uses 3. See Findings weren’t submitted.
The same, as a matrix
If you’d rather keep one list of steps, use a matrix over the clouds. Each entry runs as its own job, in parallel, like the separate jobs above:
name: LeftSize
on:
schedule:
- cron: '17 6 * * *'
workflow_dispatch:
permissions:
contents: read
jobs:
leftsize:
name: Scan ${{ matrix.cloud }} ${{ matrix.environment }}
runs-on: ubuntu-latest
permissions:
id-token: write # for the AWS and Azure logins; a matrix shares one permissions block
contents: read
strategy:
fail-fast: false # a cloud that fails doesn't cancel the others
matrix:
include:
- cloud: aws
environment: MAIN
- cloud: azure
environment: MAIN
- cloud: cloudflare
environment: MAIN
steps:
- name: AWS Login (OIDC)
if: matrix.cloud == 'aws'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets[format('AWS_ROLE_ARN_{0}', matrix.environment)] }}
aws-region: us-east-1
- name: Azure Login (OIDC)
if: matrix.cloud == 'azure'
uses: azure/login@v2
with:
client-id: ${{ secrets[format('AZURE_CLIENT_ID_{0}', matrix.environment)] }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets[format('AZURE_SUBSCRIPTION_ID_{0}', matrix.environment)] }}
- name: Run LeftSize Scan
uses: leftsize/leftsize-action@v1
with:
installation-id: ${{ secrets.LEFTSIZE_INSTALLATION_ID }}
repository-token: ${{ secrets.LEFTSIZE_REPOSITORY_TOKEN }}
cloud-provider: ${{ matrix.cloud }}
environment-name: ${{ matrix.environment }}
aws-regions: ${{ vars.AWS_REGIONS }}
cloudflare-api-token: ${{ matrix.cloud == 'cloudflare' && secrets.CLOUDFLARE_API_TOKEN || '' }}
cloudflare-account-ids: ${{ vars.CLOUDFLARE_ACCOUNT_IDS }}
Remove the entries of clouds you don’t use, and their login step. To scan another AWS account or Azure subscription, add an entry such as cloud: aws with environment: PROD and the secrets ending in _PROD, as in the single-cloud workflows above. The action ignores the inputs of the other clouds (aws-regions on an Azure entry, for example), and the Cloudflare token is passed only to the Cloudflare entry.
How the matrix differs from separate jobs:
- One permissions block. A matrix can’t set permissions per entry, so the Cloudflare entry also gets
id-token: write. It doesn’t use it. With separate jobs, only the AWS and Azure jobs have it. fail-fast: falsematters. A scan that can’t sign in fails its job. Withoutfail-fast: false, GitHub cancels the other entries when one fails.- Same limits. Each entry is one submission against the daily limit, and all entries count as one repository for your plan.
The workflow explained
Schedule
schedule runs the scan once a day. The onboarding page picks a fixed time that differs per repository (here 17 6 * * *, 06:17 UTC) to spread the load. workflow_dispatch adds a Run workflow button in the Actions tab for manual scans. GitHub runs scheduled workflows on the default branch.
Permissions
id-token: write lets the job request the OIDC token that the login step exchanges for cloud credentials. Only the AWS and Azure workflows need it; the Cloudflare workflow signs in with an API token and has only contents: read. contents: read is the minimum GitHub needs to run the workflow. The scan doesn’t need any other GitHub permission: LeftSize opens issues through its GitHub App, not through the workflow’s token.
Matrix and the MAIN label
The AWS and Azure workflows use a matrix that runs the job once per environment label. The generated workflow has one label, MAIN, and reads the secrets with that suffix: AZURE_CLIENT_ID_MAIN, AZURE_SUBSCRIPTION_ID_MAIN or AWS_ROLE_ARN_MAIN. To scan more subscriptions or accounts, add labels (environment: [MAIN, PROD, DEV]) and create the matching secrets, such as AWS_ROLE_ARN_PROD. Each label becomes its own job and its own scan; all of them count as one repository for your plan. The Cloudflare workflow has no matrix: one job scans every account the token can read, or the accounts in cloudflare-account-ids.
The label is also passed as environment-name. Labels that aren’t an environment name, such as MAIN, are ignored, so LeftSize detects production and non-production per resource. A label such as PROD or DEV sets the environment for that whole scan. See Production vs non-production.
Login step
On AWS and Azure, the login step signs in to your cloud before LeftSize runs. On AWS, aws-region: us-east-1 is only the region used to sign in; the regions to scan come from aws-regions. The action uses whatever credentials this step sets up, so you can replace it with another sign-in method; see Other ways to authenticate. On Cloudflare, the action signs in itself with cloudflare-api-token.
Results summary
The last step prints the number of findings and whether they were submitted, also when the scan step fails (if: always()). The job summary has the full report.
Inputs
| Input | Default | Description |
|---|---|---|
installation-id |
(required) | Your GitHub App installation ID, from onboarding. Store it as the LEFTSIZE_INSTALLATION_ID secret. |
repository-token |
(required) | The repository token from onboarding. Store it as the LEFTSIZE_REPOSITORY_TOKEN secret. |
mode |
scan |
scan scans your cloud; stats reports statistics (see Reports). |
cloud-provider |
azure |
azure, aws or cloudflare (any case). One cloud per step; see Scanning several clouds from one workflow. |
aws-regions |
the login region | Comma-separated AWS regions to scan, or all for every enabled region. |
cloudflare-api-token |
(required for Cloudflare) | The read-only Cloudflare API token. Pass it from a secret: ${{ secrets.CLOUDFLARE_API_TOKEN }}. See Cloudflare Setup for the permissions. |
cloudflare-account-ids |
every account the token can read | Comma-separated Cloudflare account IDs to scan (32 hexadecimal characters each). |
azure-subscription-ids |
the login subscription | Subscription ID used for scoping and billing-currency detection. The scan covers the subscription the login step selected. |
environment-name |
detected per resource | One environment for the whole scan (Production), or name=environment pairs matched against resource group and resource names (Shop-NG-A=acceptance, Shop-NG-P=production, *-dev-*=development). Values that aren’t an environment, such as MAIN, are ignored. |
include-policies |
all | Rule categories to run: cost-optimization, deprecation, governance, security (also cost, deprecations). |
exclude-policies |
none | Rule IDs to skip, with or without the leftsize- prefix, such as leftsize-idle-vm,unused-public-ip. |
currency |
detected | Currency code recorded with the findings. Detected from Azure billing; USD on AWS and Cloudflare. Savings amounts in issues are always in US dollars. |
verbose |
false |
Debug logging for LeftSize itself. |
backend-url |
https://api.leftsize.com |
Leave unset. Only https URLs on a LeftSize domain are accepted. |
To also see Cloud Custodian’s output and the API response, set the environment variable LEFTSIZE_DEBUG: true on the step. See Troubleshooting.
Outputs
| Output | Description |
|---|---|
findings-count |
Number of findings in this scan. |
findings-submitted |
true when the findings were sent to LeftSize. |
findings-json |
All findings as a JSON string; an empty array when that would exceed 512 KB (GitHub limits step outputs to 1 MB). |
findings-file |
Path of a JSON file on the runner with all findings, whatever the size. |
failed-policies |
JSON array of checks that didn’t run, for example because of a missing read permission; [] when the scan was complete. |
limit-exceeded |
true when the findings weren’t submitted because the plan’s repository limit was reached. |
Use them in later steps as ${{ steps.leftsize.outputs.<name> }}. For example, fail the job when checks are missing:
- name: Require a complete scan
if: steps.leftsize.outputs.failed-policies != '[]'
run: |
echo "Checks that didn't run: ${{ steps.leftsize.outputs.failed-policies }}"
exit 1
When the step fails
The step fails (exit code 1) when no policy file could run, for example because the cloud credentials are missing or expired, when AWS credentials don’t work, when the Cloudflare token can’t list zones and isn’t confirmed active, when an input is invalid, or when the findings can’t be submitted after three attempts. Reaching the repository limit or the daily submission limit keeps the step green. See Troubleshooting.
Related
- Getting Started: install the app and generate this workflow
- AWS Setup, Azure Setup and Cloudflare Setup: the cloud side of the sign-in
- Plans and Pricing: how repositories are counted
Was this article helpful?