Skip to content

AWS S3 Setup ​

Cloud Cache Action supports AWS S3 out of the box using both GitHub Actions OIDC (OpenID Connect) with IAM Roles and static IAM credentials.


Best Practices: Bucket Setup & Configuration ​

Follow these recommendations when creating and configuring your AWS S3 cache bucket:

1. Bucket Region & Colocation ​

  • GitHub-hosted runners: Default ubuntu-latest and windows-latest runners typically execute in AWS US regions (us-east-1 or us-east-2). Creating your bucket in us-east-1 minimizes cross-region latency and lowers data transfer fees.
  • Self-hosted EC2 runners: Always create the bucket in the same AWS region and VPC as your runners to achieve maximum throughput (line-rate VPC speeds) with zero data transfer costs.

2. Security & Access Control ​

  • Block All Public Access: Ensure all 4 settings under Block Public Access are enabled. Cache bundles contain compiled binaries, source artifacts, and dependency manifests that must never be publicly readable.
  • Object Ownership: Enable Bucket owner enforced (disable ACLs) to guarantee consistent ownership of all uploaded archives.
  • Default Encryption: Use server-side encryption with Amazon S3 managed keys (SSE-S3 / AES256) or AWS KMS (SSE-KMS). SSE-S3 is included at no additional cost.

3. Lifecycle Rules & Cost Optimization ​

Without lifecycle management, older cache revisions accumulate and increase storage costs. Configure two lifecycle rules under Bucket Management > Lifecycle Rules:

  1. Expire Current Objects:
    • Filter: Apply to all objects in bucket (or prefix ${GITHUB_REPOSITORY}/).
    • Action: Expire current versions of objects after 30 or 60 days.
  2. Abort Incomplete Multipart Uploads:
    • Action: Delete expired object delete markers and incomplete multipart uploads after 7 days. This prevents lingering chunks from failed or interrupted uploads from consuming storage.

4. Object Lock ​

Do not use a bucket with an Object Lock default retention for caches. Prune cannot delete locked objects. Also, Object Lock requires a checksum on each upload. From v1.9, the action does not send the AWS SDK CRC32 checksums, because its own sha256 check covers the archive. A save to such a bucket then fails with 400 InvalidRequest. If you must use such a bucket, set this environment variable on the job:

yaml
env:
  CLOUD_CACHE_SDK_CHECKSUMS: 'true'

Credentials & Least-Privilege IAM Policies ​

Using GitHub Actions OpenID Connect (OIDC) eliminates the need to store long-lived AWS Access Keys in repository secrets.

1. Configure the IAM Role Trust Policy ​

Create an IAM Role with a trust policy allowing GitHub Actions to assume it:

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:<OWNER>/<REPO>:*"
        }
      }
    }
  ]
}

2. Attach a Least-Privilege S3 Permissions Policy ​

Attach a policy that grants only the actions the action sends, scoped to the bucket and the repository prefix. The Security guide holds the full read-write, read-only and prune policies, and the list of S3 calls that each feature sends.

3. Workflow Example (OIDC) ​

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      id-token: write # Required for requesting the OIDC JWT
      contents: read
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - name: Configure AWS Credentials via OIDC
        uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsCacheRole
          aws-region: us-east-1

      - name: Cache dependencies
        uses: xSAVIKx/cloud-cache-action@v1
        with:
          bucket: my-actions-cache-bucket
          key: ${{ runner.os }}-build-${{ hashFiles('**/lock') }}
          path: node_modules

Option B: Static IAM Credentials ​

If you prefer static credentials, create a dedicated IAM user (never use your root AWS account) with a least-privilege policy from the Security guide, generate an Access Key ID and Secret Access Key, and save them in your repository's GitHub Secrets.

yaml
- name: Cache dependencies
  uses: xSAVIKx/cloud-cache-action@v1
  with:
    bucket: my-actions-cache-bucket
    region: us-east-1
    access-key: ${{ secrets.AWS_ACCESS_KEY_ID }}
    secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    key: ${{ runner.os }}-build-${{ hashFiles('**/lock') }}
    path: node_modules

Live CI Verification Workflow ​

This action is tested continuously against a real Amazon S3 bucket. You can inspect the live GitHub Actions workflow file in the repository: .github/workflows/provider-aws-s3.yml.

.github/workflows/provider-aws-s3.yml (Click to view full workflow)
yaml
name: Provider - Amazon S3

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened, labeled]
  workflow_dispatch:
  schedule:
    - cron: '0 4 * * *'

permissions:
  id-token: write
  contents: read

jobs:
  test-aws-s3:
    name: Amazon S3 Live Integration
    runs-on: ubuntu-latest
    if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'full-ci')
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - name: Setup Node.js
        uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
        with:
          node-version: 24

      - name: Check AWS credentials
        id: check-secrets
        run: |
          BUCKET="${{ secrets.AWS_S3_BUCKET || secrets.AWS_BUCKET }}"
          ROLE="${{ secrets.AWS_ROLE_TO_ASSUME }}"
          ACCESS_KEY="${{ secrets.AWS_ACCESS_KEY_ID }}"
          SECRET_KEY="${{ secrets.AWS_SECRET_ACCESS_KEY }}"

          if [ -z "$BUCKET" ]; then
            echo "has_secrets=false" >> "$GITHUB_OUTPUT"
            echo "::notice::AWS S3 bucket secret (AWS_S3_BUCKET or AWS_BUCKET) is not configured. Skipping live AWS S3 integration test."
          elif [ -n "$ROLE" ]; then
            echo "has_secrets=true" >> "$GITHUB_OUTPUT"
            echo "auth_mode=oidc" >> "$GITHUB_OUTPUT"
          elif [ -n "$ACCESS_KEY" ] && [ -n "$SECRET_KEY" ]; then
            echo "has_secrets=true" >> "$GITHUB_OUTPUT"
            echo "auth_mode=static" >> "$GITHUB_OUTPUT"
          else
            echo "has_secrets=false" >> "$GITHUB_OUTPUT"
            echo "::notice::AWS credentials (AWS_ACCESS_KEY_ID & AWS_SECRET_ACCESS_KEY, or AWS_ROLE_TO_ASSUME) are not configured. Skipping live AWS S3 integration test."
          fi

      - name: Configure AWS Credentials via OIDC
        if: steps.check-secrets.outputs.has_secrets == 'true' && steps.check-secrets.outputs.auth_mode == 'oidc'
        uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          aws-region: ${{ secrets.AWS_REGION || 'us-east-1' }}

      - name: Generate synthetic test payload
        if: steps.check-secrets.outputs.has_secrets == 'true'
        run: |
          mkdir -p ~/aws-cache-test
          echo "AWS S3 test artifact $(date +%s%N) for run ${{ github.run_id }}" > ~/aws-cache-test/sample.txt
          sha256sum ~/aws-cache-test/sample.txt > /tmp/aws-expected-sha.txt
          cat /tmp/aws-expected-sha.txt

      - name: Save cache to Amazon S3 (save-only)
        if: steps.check-secrets.outputs.has_secrets == 'true'
        uses: ./save
        with:
          path: ~/aws-cache-test
          key: aws-s3-live-test-${{ github.run_id }}-${{ github.run_attempt }}
          bucket: ${{ secrets.AWS_S3_BUCKET || secrets.AWS_BUCKET }}
          region: ${{ secrets.AWS_REGION || 'us-east-1' }}
          access-key: ${{ secrets.AWS_ACCESS_KEY_ID || '' }}
          secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY || '' }}

      - name: Purge local test payload
        if: steps.check-secrets.outputs.has_secrets == 'true'
        run: rm -rf ~/aws-cache-test

      - name: Restore cache from Amazon S3 (restore-only)
        if: steps.check-secrets.outputs.has_secrets == 'true'
        id: restore-aws
        uses: ./restore
        with:
          path: ~/aws-cache-test
          key: aws-s3-live-test-${{ github.run_id }}-${{ github.run_attempt }}
          bucket: ${{ secrets.AWS_S3_BUCKET || secrets.AWS_BUCKET }}
          region: ${{ secrets.AWS_REGION || 'us-east-1' }}
          access-key: ${{ secrets.AWS_ACCESS_KEY_ID || '' }}
          secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY || '' }}
          fail-on-cache-miss: true

      - name: Verify restored content and checksum
        if: steps.check-secrets.outputs.has_secrets == 'true'
        run: |
          echo "Asserting cache hit..."
          if [ "${{ steps.restore-aws.outputs.cache-hit }}" != "true" ]; then
            echo "::error::Expected cache-hit to be 'true', got '${{ steps.restore-aws.outputs.cache-hit }}'"
            exit 1
          fi

          if [ ! -f ~/aws-cache-test/sample.txt ]; then
            echo "::error::Restored file ~/aws-cache-test/sample.txt not found!"
            exit 1
          fi

          ACTUAL_SHA=$(sha256sum ~/aws-cache-test/sample.txt | awk '{print $1}')
          EXPECTED_SHA=$(awk '{print $1}' /tmp/aws-expected-sha.txt)
          if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
            echo "::error::Checksum mismatch! Expected $EXPECTED_SHA, got $ACTUAL_SHA"
            exit 1
          fi

          echo "Live Amazon S3 cache save & restore test succeeded! Checksum: $ACTUAL_SHA"

      - name: Look up by restore-keys
        if: steps.check-secrets.outputs.has_secrets == 'true'
        id: restore-keys-aws
        uses: ./restore
        with:
          path: ~/aws-cache-test
          key: aws-s3-live-test-${{ github.run_id }}-${{ github.run_attempt }}-missing
          restore-keys: aws-s3-live-test-${{ github.run_id }}-
          lookup-only: true
          bucket: ${{ secrets.AWS_S3_BUCKET || secrets.AWS_BUCKET }}
          region: ${{ secrets.AWS_REGION || 'us-east-1' }}
          access-key: ${{ secrets.AWS_ACCESS_KEY_ID || '' }}
          secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY || '' }}

      - name: Verify the restore-keys match
        if: steps.check-secrets.outputs.has_secrets == 'true'
        run: |
          test "${{ steps.restore-keys-aws.outputs.cache-hit }}" = "false"
          test "${{ steps.restore-keys-aws.outputs.cache-matched-key }}" = "aws-s3-live-test-${{ github.run_id }}-${{ github.run_attempt }}"