GitHub Actions as CI component for CI/CD : Store Docker image in GCP Artifacts Registry

This is the CI (continues integration) part of common CI/CD pipelines.

Task: Set up a new CI pipeline using GitHub Actions that should be triggered for any pull request.

1. The GitHub Actions file structure in your repo:

IMAGE-BUILDER repo:
.
├── .git
├── .github
│   └── workflows
│       ├── flask-app
│       │   ├── Dockerfile
│       │   ├── app.py
│       │   └── requirements.txt
│       └── g-registry-build.yaml    
├── .gitignore
├── README.md
└── sonar-project.properties

image-builder/.github/workflows/g-registry-build.yaml


# This is a basic workflow to help you get started with Actions

name: Docker Build and Push Release

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
    types: [opened, synchronize, reopened]

jobs:

  sonarcloud:
    name: SonarCloud
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # Shallow clones should be disabled for a better relevancy of analysis
      - name: SonarCloud static code Scan
        uses: SonarSource/sonarcloud-github-action@master
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}  # Needed to get PR information, if any
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

  app-build:
    name: Tagged Docker release to Google Artifact Registry
    runs-on: ubuntu-latest
    needs: sonarcloud
    permissions:
      contents: 'read'
      id-token: 'write'

    steps:
    - id: checkout
      name: Checkout
      uses: 'actions/checkout@v3'
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2 

    - name: Get tag for the image
      id: get-tag
      run: echo ::set-output name=short_ref::${GITHUB_REF#refs/*/}
  
    - id: 'auth'
      name: 'Authenticate to Google Cloud'
      uses: 'google-github-actions/auth@v1'
      with:
        token_format: access_token
        workload_identity_provider: 'projects/12344123412311/locations/global/workloadIdentityPools/my-pool/providers/provider'
        service_account: 'my-service-account@my-project-name.iam.gserviceaccount.com'
        access_token_lifetime: 300s

    - name: Login to Artifact Registry
      uses: docker/login-action@v1
      with:
        registry: us-west2-docker.pkg.dev  #us-west2-docker.pkg.dev/my-project-name/flask
        username: oauth2accesstoken
        password: ${{ steps.auth.outputs.access_token }}

    - id: docker-push-tagged
      name: Tag Docker image and push to Google Artifact Registry
      uses: 'docker/build-push-action@v2'
      with:
        context: .github/workflows/flask-app
        file: .github/workflows/flask-app/Dockerfile
        push: true
        tags: |
          us-west2-docker.pkg.dev/my-project-name.github/workflows/g-registry-build.yaml/flask-docker/application:${{ steps.get-tag.outputs.short_ref }}

To setup the Sonar Cloud code scanner put the sonar-project.properties file in the root of the repo :

image-builder/sonar-project.properties

sonar.projectKey=organization-name_image-builder
sonar.organization=organization-name

# This is the name and version displayed in the SonarCloud UI.
#sonar.projectName=image-builder
#sonar.projectVersion=1.0


# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
#sonar.sources=.

# Encoding of the source code. Default is the 
default system encoding
#sonar.sourceEncoding=UTF-8

GCP Artifact Registry Setup (

Pre-setup:

It can be done from GCP console (embeded shell CLI)

#Configuring gcloud in GitHub Actions

This GitHub Action to configure authentication for the gcloud CLI tool.

Warning! Workload Identity Federation requires Cloud SDK (gcloud) version 363.0.0 or later.

account_name@cloudshell:~ (my-project-name)$ 
export SERVICE_ACCOUNT="my-project-name"                                                   
export REPO="repo-owner/organization-name/image-builder"                              
export PROJECT_ID="my-project-name"                                                   
export WORKLOAD_IDENTITY_POOL="my-pool-iam"                                             
export WORKLOAD_PROVIDER="my-provider"
gcloud iam workload-identity-pools describe "my-pool-iam" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --format="value(name)"

OUTPUT:

projects/12341234134134/locations/global/workloadIdentityPools/my-pool-iam

Save this value as an environment variable:

export WORKLOAD_IDENTITY_POOL_ID="..." # value from above

Create a Workload Identity Provider in that pool:

gcloud iam workload-identity-pools providers create-oidc "provider" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="my-pool-iam" \
  --display-name="my provider" \
  --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
  --issuer-uri="https://token.actions.githubusercontent.com"

OUTPUT: Created workload identity pool provider [provider].

Allow authentications from the Workload Identity Provider originating from your repository to impersonate the Service Account created above:

TODO(developer): Update this value to your GitHub repository.

export REPO="username(or organization-name)/repo-name" # e.g. "google/chrome"


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

If you want to admit all repos of an owner (user or organization), map on attribute.repository_owner:

--member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository_owner/${OWNER}"
For this to work, you need to make sure that attribute.repository_owner is mapped in your attribute mapping (see previous step).

OUTPUT:

Updated IAM policy for serviceAccount [my-serviceaccount-name@my-project-name.iam.gserviceaccount.com].
bindings:
- members:
  - principalSet://iam.googleapis.com/projects/12341234134134/locations/global/workloadIdentityPools/my-pool-iam/attribute.repository/repo-owner/organization/image-builder
  - principalSet://iam.googleapis.com/projects/12341234134134/locations/global/workloadIdentityPools/my-pool-iam/attribute.repository/repo-owner/image-builder
  role: roles/iam.workloadIdentityUser
etag: BwYCmO5NM20=
version: 1

#Authenticating to Container Registry and Artifact Registry This example demonstrates authenticating to Google Container Registry (GCR) or Google Artifact Registry (GAR). The most common way to authenticate to these services is via a gcloud docker proxy. However, you can authenticate to these registries directly using the auth action:

Username: oauth2accesstoken
Password: ${{ steps.auth.outputs.access_token }}

You must set token_format: access_token in your Action YAML. Here are a few examples:

jobs:
  job_id:
    steps:
    - uses: 'actions/checkout@v3'

    - id: 'auth'
      name: 'Authenticate to Google Cloud'
      uses: 'google-github-actions/auth@v1'
      with:
        token_format: 'access_token'
        # Either user Workload Identity Federation or Service Account Keys. See
        # above more more examples

    # This example uses the docker login action
    - uses: 'docker/login-action@v1'
      with:
        registry: 'gcr.io' # or REGION-docker.pkg.dev
        username: 'oauth2accesstoken'
        password: '${{ steps.auth.outputs.access_token }}'

    # This example runs "docker login" directly to Artifact Registry.
    - run: |-
        echo '${{ steps.auth.outputs.access_token }}' | docker login -u oauth2accesstoken --password-stdin https://REGION-docker.pkg.dev

    # This example runs "docker login" directly to Container Registry.
    - run: |-
        echo '${{ steps.auth.outputs.access_token }}' | docker login -u oauth2accesstoken --password-stdin https://gcr.io

flask-app/Dockerfile

```dockerfile
FROM python:3.8
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD [ "python", "app.py" ] # command to run on container start
```

flask-app/requirements.txt

```pip-requirements
Flask==2.1.0
werkzeug==2.0.0
```

flask-app/app.py

```python
from flask import Flask
from flask import json
import logging

app = Flask(__name__)

@app.route('/status')
def healthcheck():
    response = app.response_class(
            response=json.dumps({"result":"OK - healthy"}),
            status=200,
            mimetype='application/json'
    )
    app.logger.info('Status request successfull')
    app.logger.debug('DEBUG message')
    return response

@app.route('/metrics')
def metrics():
    response = app.response_class(
            response=json.dumps({"status":"success","code":0,"data":{"UserCount":140,"UserCountActive":23}}),
            status=200,
            mimetype='application/json'
    )
    app.logger.info('Metrics request successfull')
    return response

@app.route("/")
def hello():
    app.logger.info('Main request successfull')

    return "Hello World!"

if __name__ == "__main__":
    ## stream logs to a file
    logging.basicConfig(filename='app.log',level=logging.DEBUG)
    
    app.run(host='0.0.0.0')
```

Last updated