moonrepo remote cache: how do I configure Google Cloud Storage (service account, permissions) for CI?
Developer Productivity Tooling

moonrepo remote cache: how do I configure Google Cloud Storage (service account, permissions) for CI?

9 min read

Configuring moonrepo’s remote cache with Google Cloud Storage (GCS) in CI involves three main pieces: creating and securing a bucket, setting up a service account with the right permissions, and wiring credentials into your CI environment so moonrepo can read and write cache artifacts. This guide walks through each step in detail and highlights common pitfalls when using GCS for remote caching.


Why use Google Cloud Storage for moonrepo remote cache?

Using GCS as a remote cache for moonrepo gives you:

  • Shared build artifacts across developers and CI
  • Faster CI builds by avoiding repeated work
  • Durable, centralized storage that works across regions and runners
  • Fine‑grained IAM control over cache access

For CI, the usual pattern is:

  1. A dedicated GCS bucket holds moonrepo cache artifacts.
  2. A least‑privilege service account has access to that bucket.
  3. CI jobs authenticate using that service account and point moonrepo to the bucket.

Prerequisites

Before you start configuring the moonrepo remote cache, make sure you have:

  • A Google Cloud project (with billing enabled).
  • gcloud CLI installed and authenticated (optional but helpful).
  • A working moonrepo setup with a .moon configuration directory.
  • Administrative access to your CI system (GitHub Actions, GitLab CI, CircleCI, etc.).

Step 1: Create a GCS bucket for moonrepo cache

You can create the bucket via console or CLI. Naming and location are important for performance and organization.

1.1 Choose a bucket name and location

Use a globally unique name, often including project and environment, for example:

  • my-company-moon-cache-prod
  • my-company-moon-cache-ci

Recommended choices:

  • Location type: regional or multi-region close to your CI runners.
  • Storage class: STANDARD is typical for active cache usage.

1.2 Create the bucket with gcloud (optional)

PROJECT_ID="my-gcp-project-id"
BUCKET_NAME="my-company-moon-cache-ci"

gcloud config set project "$PROJECT_ID"

gcloud storage buckets create "gs://${BUCKET_NAME}" \
  --location="us-central1" \
  --default-storage-class="STANDARD"

Or create it in the Google Cloud Console under Cloud Storage → Buckets → Create.


Step 2: Create a service account for CI

You’ll want a dedicated service account used only by CI for moonrepo remote cache access, following least‑privilege security practices.

2.1 Create the service account

SERVICE_ACCOUNT_NAME="moonrepo-cache-ci"
SERVICE_ACCOUNT_ID="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"

gcloud iam service-accounts create "$SERVICE_ACCOUNT_NAME" \
  --display-name="Moonrepo CI Remote Cache"

You can also create it via Google Cloud Console under IAM & Admin → Service Accounts.


Step 3: Grant minimum required permissions on the bucket

The service account only needs the ability to read and write objects in the cache bucket (and list them). Avoid giving it broad project‑wide roles when possible.

3.1 Recommended minimal roles

At the bucket level (not the whole project), grant:

  • Storage Object Admin (roles/storage.objectAdmin)
    This allows:
    • Read (storage.objects.get, storage.objects.list)
    • Write (storage.objects.create, storage.objects.delete, storage.objects.update)

This is usually sufficient and commonly used for build caches.

Assign the role to the service account:

gcloud storage buckets add-iam-policy-binding "gs://${BUCKET_NAME}" \
  --member="serviceAccount:${SERVICE_ACCOUNT_ID}" \
  --role="roles/storage.objectAdmin"

If you want stricter separation of duties, you can fine‑tune with:

  • roles/storage.objectCreator + roles/storage.objectViewer
  • Combined with careful bucket policies

But in practice, roles/storage.objectAdmin for the bucket is simplest and safe when scoped only to this cache bucket.


Step 4: Create service account credentials for CI

Your CI jobs need a way to authenticate as this service account. The common method is a JSON key file, but some CI providers support more secure, keyless options (Workload Identity, OIDC). Below covers the JSON key approach; adapt if you’re using a more advanced setup.

4.1 Generate a JSON key

gcloud iam service-accounts keys create "moonrepo-cache-ci-key.json" \
  --iam-account="${SERVICE_ACCOUNT_ID}"

This produces a JSON file like:

{
  "type": "service_account",
  "project_id": "my-gcp-project-id",
  "private_key_id": "…",
  "private_key": "-----BEGIN PRIVATE KEY-----\n…\n-----END PRIVATE KEY-----\n",
  "client_email": "moonrepo-cache-ci@my-gcp-project-id.iam.gserviceaccount.com",
  "client_id": "…",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "…"
}

Store it somewhere secure locally (it should not be committed to the repository).

4.2 Store the key securely in CI

Each CI platform has its own secrets mechanism:

  • GitHub Actions:
    • Encode the JSON content as a single secret, e.g. GCP_SERVICE_ACCOUNT_KEY.
  • GitLab CI:
    • Add a variable like GCP_SERVICE_ACCOUNT_KEY (masked & protected).
  • CircleCI / others:
    • Similar secrets or environment variable configuration.

Do not commit the key to your repo or echo it in logs.


Step 5: Configure authentication in CI

Your CI job must:

  1. Write the JSON key content into a file.
  2. Set GOOGLE_APPLICATION_CREDENTIALS to point to that file.
  3. Make sure the GCP SDK or client that moonrepo uses picks this up.

5.1 Example: GitHub Actions

name: CI

on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest

    env:
      GCP_PROJECT_ID: my-gcp-project-id
      GCS_BUCKET_NAME: my-company-moon-cache-ci

    steps:
      - uses: actions/checkout@v4

      - name: Set up Node (if needed)
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Configure GCP credentials
        run: |
          echo "${{ secrets.GCP_SERVICE_ACCOUNT_KEY }}" > "${HOME}/gcp-key.json"
          export GOOGLE_APPLICATION_CREDENTIALS="${HOME}/gcp-key.json"
          gcloud auth activate-service-account \
            --key-file="${HOME}/gcp-key.json" \
            --project="${GCP_PROJECT_ID}"

      - name: Install moonrepo (example)
        run: |
          npm install -g @moonrepo/cli

      - name: Run moon with remote cache
        env:
          GOOGLE_APPLICATION_CREDENTIALS: "${HOME}/gcp-key.json"
        run: |
          moon run :build

Depending on your moonrepo setup, you may not need to explicitly run gcloud auth if the underlying client library respects GOOGLE_APPLICATION_CREDENTIALS. But setting it up as shown is a safe pattern.

5.2 Example: GitLab CI

stages:
  - build

variables:
  GCP_PROJECT_ID: "my-gcp-project-id"
  GCS_BUCKET_NAME: "my-company-moon-cache-ci"

build:
  stage: build
  image: node:20
  script:
    - echo "$GCP_SERVICE_ACCOUNT_KEY" > "$CI_PROJECT_DIR/gcp-key.json"
    - export GOOGLE_APPLICATION_CREDENTIALS="$CI_PROJECT_DIR/gcp-key.json"
    - npm install -g @moonrepo/cli
    - moon run :build
  only:
    - main

Here, GCP_SERVICE_ACCOUNT_KEY is defined as a masked, protected variable in GitLab settings.


Step 6: Configure moonrepo to use Google Cloud Storage remote cache

Moonrepo’s remote cache configuration is typically stored in a .moon/workspace.yml or via environment variables, depending on your version and setup. The key elements:

  • Enable remote cache.
  • Set the GCS bucket URL.
  • Ensure the environment provides valid credentials.

6.1 Example moonrepo configuration (GCS)

Assuming a config structure similar to:

# .moon/workspace.yml

remoteCache:
  enabled: true
  provider: "gcs"
  bucket: "my-company-moon-cache-ci"
  # Optional: prefix to separate environments or repos
  prefix: "moon-cache/"
  # Optional: limit concurrency, timeouts, etc. depending on moonrepo version

In some setups, the bucket may also be specified as a full URI:

remoteCache:
  enabled: true
  provider: "gcs"
  bucket: "gs://my-company-moon-cache-ci"

Check the moonrepo version you’re using and its documentation for exact property names; the pattern above is representative but may vary slightly.

6.2 Using environment variables instead of hard‑coding

To keep configs generic, you can rely on environment variables in CI:

export MOON_REMOTE_CACHE_PROVIDER="gcs"
export MOON_REMOTE_CACHE_BUCKET="my-company-moon-cache-ci"
export MOON_REMOTE_CACHE_PREFIX="moon-cache/"

Some versions of moonrepo support environment variable overrides for config fields. This is useful when:

  • Using different buckets for PRs vs main branch.
  • Separating environments (dev, staging, prod).
  • Avoiding repository‑specific configuration for shared CI templates.

Step 7: Testing your moonrepo remote cache setup in CI

Before relying on the cache at scale, verify that:

  1. CI can authenticate to GCS.
  2. Moonrepo successfully uploads and downloads cache entries.
  3. Permissions are scoped correctly and not overly broad.

7.1 Local test with the same credentials

You can simulate CI locally:

export GOOGLE_APPLICATION_CREDENTIALS="/path/to/moonrepo-cache-ci-key.json"
moon run :build

Then check the GCS bucket in the console to confirm new objects are created under any configured prefix.

7.2 CI test strategy

  1. Run the CI pipeline twice with no code changes:
    • The first run should populate the remote cache (takes normal time).
    • The second run should show cache hits (noticeably faster).
  2. Inspect moonrepo logs (often via verbose or debug flags) for messages indicating remote cache hits and misses.

If moonrepo is not using the remote cache, it typically logs warnings if it fails to connect to the remote provider.


Common issues and how to fix them

1. 403 PermissionDenied or AccessDenied

Cause: The service account does not have sufficient bucket permissions.

Fix:

  • Confirm bucket‑level IAM binding:

    gcloud storage buckets get-iam-policy "gs://${BUCKET_NAME}"
    
  • Ensure your service account is listed with roles/storage.objectAdmin (or a combined viewer/creator role set).

  • Confirm you’re not accidentally using a different project or bucket name.

2. 401 Unauthorized or credentials ignored

Cause: GOOGLE_APPLICATION_CREDENTIALS not set, or the file is malformed.

Fix:

  • Ensure your CI job writes the full JSON content into a file.

  • Confirm the path:

    echo "$GOOGLE_APPLICATION_CREDENTIALS"
    cat "$GOOGLE_APPLICATION_CREDENTIALS" | jq '.' >/dev/null
    
  • Make sure your secrets manager is not escaping quotes incorrectly (for example, multiline issues).

3. Cache not being used (no errors, but no hits)

Cause: Misconfigured moonrepo remote cache settings or non‑deterministic tasks.

Fix:

  • Confirm remoteCache.enabled: true and provider: gcs in your moonrepo config.
  • Ensure the bucket name or prefix matches what you expect in both local and CI environments.
  • Check that your tasks are cacheable in moonrepo configuration (e.g., cache: true on tasks).

4. Security concerns about service account keys

Cause: Long‑lived keys stored as CI secrets.

Fix / mitigation:

  • Restrict the service account to the single cache bucket only.
  • Rotate keys regularly.
  • Where possible, switch to keyless auth:
    • Workload Identity Federation (GitHub Actions → GCP).
    • GCP Workload Identity on GKE or Cloud Build.

Best practices for a secure and efficient moonrepo remote cache on GCS

To keep your moonrepo remote cache reliable and secure in CI, follow these practices:

  • Use dedicated buckets per environment
    e.g., my-company-moon-cache-dev, -staging, -prod. This avoids cross‑environment contamination.

  • Separate CI and developer access
    CI can use a service account; developers can rely on their own GCP credentials or a different service account with read‑only permissions.

  • Keep IAM minimal
    Give your moonrepo CI service account only bucket‑level object permissions on the cache bucket.

  • Avoid leakage of cache artifacts
    Cache may contain compiled code or internal logic; treat it as private and avoid making the bucket public.

  • Monitor bucket size and lifecycle
    Set lifecycle rules to delete older objects and control storage costs:

    gcloud storage buckets update "gs://${BUCKET_NAME}" \
      --lifecycle-file="lifecycle.json"
    

    Example lifecycle.json:

    {
      "rule": [
        {
          "action": { "type": "Delete" },
          "condition": { "age": 30 }
        }
      ]
    }
    
  • Document your setup
    Add a brief docs/ci-remote-cache.md in your repo that explains:

    • Bucket name
    • Service account
    • CI job snippet
    • How to rotate keys

By following these steps—creating a dedicated GCS bucket, adding a least‑privilege service account, wiring in credentials for your CI provider, and configuring moonrepo’s remote cache to point at that bucket—you’ll have a secure and effective moonrepo remote cache backed by Google Cloud Storage. This setup significantly speeds up CI pipelines while keeping access controlled via GCP IAM.