Search code examples
google-app-enginegithub-actionsopenid-connectgcloudcicd

Deploy GCP App Engine wtih GitHub Actions using (Preferred) Direct Workload Identity Federation


I have a an app that I have successfully deployed from my local workstation using gcloud app deploy.

I am now attempting to setup a GitHub Action to do this on push.

The reference material for what I'm doing is the following two GitHub actions: https://github.com/google-github-actions/deploy-appengine https://github.com/google-github-actions/auth

I am looking to follow the (Preferred) Direct Workload Identity Federation section for authorization: https://github.com/google-github-actions/auth?tab=readme-ov-file#preferred-direct-workload-identity-federation

Setting up the workload identity pool and provider seems relatively straightforward.

The piece where I think I'm failing is configuring the "As needed, allow authentications from the Workload Identity Pool to Google Cloud resources."

Setting up the workload identity pools and providers I've done with:

gcloud iam workload-identity-pools create "${WIF_POOL_NAME}" \
  --project="${GCP_PROJECT_ID}" \
  --location="global" \
  --display-name="GitHub Actions Pool"

gcloud iam workload-identity-pools providers create-oidc "${WIF_PROVIDER_NAME}" \
  --project="${GCP_PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="${WIF_POOL_NAME}" \
  --display-name="My GitHub repo Provider" \
  --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
  --attribute-condition="assertion.repository_owner=='${GITHUB_ORG}' && assertion.ref=='refs/heads/main'" \
  --issuer-uri="https://token.actions.githubusercontent.com"

Setting the iam policy bindings I've done with:

export WORKLOAD_IDENTITY_POOL_ID=$(gcloud iam workload-identity-pools describe "${WIF_POOL_NAME}" \
  --project="${GCP_PROJECT_ID}" \
  --location="global" \
  --format="value(name)" 2>&1)

export WORKLOAD_IDENTITY_PROVIDER=$(gcloud iam workload-identity-pools providers describe "${WIF_PROVIDER_NAME}" \
  --project="${GCP_PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="$WIF_POOL_NAME" \
  --format="value(name)" 2>&1)

export GCP_PROJECT_NUMBER=$(gcloud projects describe \
    $(gcloud config get-value core/project) \
    --format="value(projectNumber)" 2>&1)


echo "project_id: '${GCP_PROJECT_ID}'"
echo "workload_identity_provider: '${WORKLOAD_IDENTITY_PROVIDER}'"

gcloud iam service-accounts add-iam-policy-binding "${GCP_PROJECT_ID}@appspot.gserviceaccount.com" \
    --member=serviceAccount:SERVICE_AGENT_EMAIL \
    --role=roles/iam.serviceAccountTokenCreator

gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
  --project="${PROJECT_ID}" \
  --role="roles/appengine.deployer" \
  --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"

gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
  --project="${PROJECT_ID}" \
  --role="roles/appengine.deployer" \
  --member="serviceAccount:${GCP_PROJECT_ID}@appspot.gserviceaccount.com"

gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
  --project="${PROJECT_ID}" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"

gcloud iam service-accounts add-iam-policy-binding "${GCP_PROJECT_ID}@appspot.gserviceaccount.com" \
  --project="${GCP_PROJECT_ID}" \
  --role="roles/iam.serviceAccountUser" \
  --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"

The error that GitHub actions is report is:

Running: gcloud app deploy --quiet --format json app.yaml --promote
Error: google-github-actions/deploy-appengine failed with: failed to execute gcloud command `gcloud app deploy --quiet --format json app.yaml --promote`: 
ERROR: (gcloud.app.deploy) Permissions error fetching application [apps/appspot]. 
Please make sure that you have permission to view applications on the project and that *** has the App Engine Deployer (roles/appengine.deployer) role.

This is the GitHub actions file I'm using:

name: Deployment

on:
  push:
    branches:
    - main

jobs:
  job_id:
    name: Deploy to App Engine
    runs-on: ubuntu-latest
    permissions:
      contents: 'read'
      id-token: 'write'

    steps:
      - id: 'checkout'
        uses: 'actions/checkout@v4'

      - id: 'auth'
        uses: 'google-github-actions/auth@v2'
        with:
          service_account: '${{ secrets.GCP_SA }}'
          workload_identity_provider: '${{ secrets.WIF_PROVIDER }}'

      - id: 'deploy'
        uses: 'google-github-actions/deploy-appengine@v2'

Solution

  • I have this working now. I changed the strategy slightly by creating a new service account that does the deploy. The workload identity provider then uses that service account to do the the deploy.

    The advantage of this is that it allowed me to test the the service account with my regular user account locally before switching to the workload identity provider from GitHub Actions.

    First thing was to create the new service account:

    gcloud iam service-accounts create $GCP_SVC_ACC \
      --description="For deploying app engine" \
      --display-name="app deploy bot"
    

    Then give that service account the roles required to deploy the app engine:

    gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
      --member="serviceAccount:${GCP_SVC_ACC}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
      --role="roles/appengine.deployer"
    
    gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
      --member="serviceAccount:${GCP_SVC_ACC}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
      --role="roles/cloudbuild.builds.editor"
    
    gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
      --member="serviceAccount:${GCP_SVC_ACC}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
      --role="roles/storage.objectAdmin"
    
    gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
      --member="serviceAccount:${GCP_SVC_ACC}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
      --role="roles/appengine.serviceAdmin"
    

    Then give the deploy service account access to app engine default service account.

    gcloud iam service-accounts add-iam-policy-binding "${GCP_PROJECT_ID}@appspot.gserviceaccount.com" \
      --member="serviceAccount:${GCP_SVC_ACC}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
      --role="roles/iam.serviceAccountUser"
    

    To test that the service account could be impersonated and used to deploy app engine, I gave my user access to creating token for service account and then using the service account:

    gcloud iam service-accounts add-iam-policy-binding "${GCP_SVC_ACC}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
      --member="user:${GCP_USER_EMAIL}" \
      --role="roles/iam.serviceAccountTokenCreator"
    
    gcloud iam service-accounts add-iam-policy-binding "${GCP_SVC_ACC}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
      --member="user:${GCP_USER_EMAIL}" \
      --role="roles/iam.serviceAccountUser"
    

    I could then test the deploy service account from my local machine with:

    gcloud app deploy --impersonate-service-account="${GCP_SVC_ACC}@${GCP_PROJECT_ID}.iam.gserviceaccount.com"
    

    Once this was working, create the workload identity pool:

    gcloud iam workload-identity-pools create "${WIF_POOL_NAME}" \
      --project="${GCP_PROJECT_ID}" \
      --location="global" \
      --display-name="GitHub Actions Pool"
    

    Next, create the workload identity provider:

    gcloud iam workload-identity-pools providers create-oidc "${GCP_PROVIDER_NAME}" \
      --project="${GCP_PROJECT_ID}" \
      --location="global" \
      --workload-identity-pool="${WIF_POOL_NAME}" \
      --display-name="My GitHub repo Provider" \
      --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
      --attribute-condition="assertion.repository_owner=='${GITHUB_ORG}' && assertion.ref=='refs/heads/main'" \
      --issuer-uri="https://token.actions.githubusercontent.com"
    

    Allow workload provider to create token for impersonating deploy service account and use it:

    gcloud iam service-accounts add-iam-policy-binding "${GCP_SVC_ACC}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
      --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${GITHUB_ORGREPO}" \
      --role="roles/iam.serviceAccountTokenCreator"
    
    gcloud iam service-accounts add-iam-policy-binding "${GCP_SVC_ACC}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
        --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${GITHUB_ORGREPO}" \
        --role="roles/iam.serviceAccountUser"
    

    Next, get the information required for the GitHub Actions YAML file:

    export WORKLOAD_IDENTITY_POOL_ID=$(gcloud iam workload-identity-pools describe "${WIF_POOL_NAME}" \
      --project="${GCP_PROJECT_ID}" \
      --location="global" \
      --format="value(name)" 2>&1)
    
    export WORKLOAD_IDENTITY_PROVIDER=$(gcloud iam workload-identity-pools providers describe "${GCP_PROVIDER_NAME}" \
      --project="${GCP_PROJECT_ID}" \
      --location="global" \
      --workload-identity-pool="$WIF_POOL_NAME" \
      --format="value(name)" 2>&1)
    
    # Information for GitHub Actions yaml file
    echo "workload_identity_provider: '${WORKLOAD_IDENTITY_PROVIDER}'"
    echo "service_account: '${GCP_SVC_ACC}@${GCP_PROJECT_ID}.iam.gserviceaccount.com'"
    

    My final Workflow file was:

    name: Deployment
    
    on:
      workflow_dispatch:
      push:
        branches:
        - main
    
    jobs:
      job_id:
        name: Deploy to App Engine
        runs-on: ubuntu-latest
        permissions:
          contents: 'read'
          id-token: 'write'
    
        steps:
          - uses: 'actions/checkout@v4'
    
          - id: 'auth'
            uses: 'google-github-actions/auth@v2'
            with:
              workload_identity_provider: '${{ secrets.GCP_IDENTITY_PROVIDER }}'
              service_account: '${{ secrets.GCP_SVC_ACC }}'
    
          - id: 'deploy'
            uses: 'google-github-actions/deploy-appengine@v2'