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.

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 Least-Privilege S3 Permissions Policy

Attach an IAM policy granting only the minimal actions required by cloud-cache-action:

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowBucketListing",
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket"
      ],
      "Resource": "arn:aws:s3:::my-actions-cache-bucket"
    },
    {
      "Sid": "AllowObjectOperations",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:AbortMultipartUpload"
      ],
      "Resource": "arn:aws:s3:::my-actions-cache-bucket/*"
    }
  ]
}

NOTE

cloud-cache-action does not require s3:DeleteObject. Lifecycle cleanup is handled by bucket lifecycle rules.

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 the least-privilege policy shown above, 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]
  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
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - name: Setup Node.js
        uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
        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 /tmp/aws-cache-test
          echo "AWS S3 test artifact $(date +%s%N) for run ${{ github.run_id }}" > /tmp/aws-cache-test/sample.txt
          sha256sum /tmp/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: /tmp/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 /tmp/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: /tmp/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 /tmp/aws-cache-test/sample.txt ]; then
            echo "::error::Restored file /tmp/aws-cache-test/sample.txt not found!"
            exit 1
          fi

          ACTUAL_SHA=$(sha256sum /tmp/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"