What are Github Actions?
Up until recently I hadn’t had a ton of opportunities to really dig my fingers into all that Github had to offer. I had
only been hosting some of my random and test repos there. Essentially just a remote location to push
| pull
to/from.
My general workflow would often be some form of development in a dev branch, maybe opening up a pull request and eventuanlly merging my changes into main.
The “basics”. Any downstream CI/CD steps (if what I was working on required any) would often be handled outside of Github. For instance,
I used to have an entire Jenkins server setup to handle testing and linting of a repo. Although not THAT much of a hassle,
working with Jenkins did add additional server maintenance steps that I often didn’t have time for (read: I just plain didn’t want to do that). 😭
So, when I began to hear about what was possible with Github Actions my interest was peaked. I had seen the “Actions” tab in all of my repositories, but I never took the time to really get to the bottom of what was inside there.
Github defines Actions as the following:
GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows you to automate your build, test, and deployment pipeline. You can create workflows that build and test every pull request to your repository, or deploy merged pull requests to production.
To overly simplify things. Github Actions allows you, the developer, to write your own custom “workflows” where you define step-by-step what actions should happen after your workflow has been triggered. Even better, is that you can leverage already built workflow steps by looking through the Github Marketplace. I’m going to take the next few posts to break down into more detail one particular Github Actions Workflow I put together and use a lot. Brush up on working in YAML if you want to follow along!
name: Sample GH Action Workflow
on:
workflow_dispatch:
inputs:
SUB_DIR:
required: true
type: string
ENVIRONMENT:
required: true
type: string
PROJECT:
required: true
type: string
REGION:
required: true
type: string
ZONE:
required: true
type: string
GCP_AR_REPO:
required: true
type: string
env:
SUB_DIR: ${{ inputs.SUB_DIR }}
WORKINGDIR: ints/${{ inputs.SUB_DIR }}
WORK_QUEUE: ${{ inputs.ENVIRONMENT }}
ENVIRONMENT: ${{ inputs.ENVIRONMENT }}
PROJECT: ${{ inputs.PROJECT }}
REGION: ${{ inputs.REGION }}
ZONE: ${{ inputs.ZONE }}
GCP_AR_REPO: ${{ inputs.GCP_AR_REPO }}
GCE_INSTANCE: prefect-docker-${{ inputs.ENVIRONMENT }}-vm
GCE_INSTANCE_ZONE: ${{ inputs.REGION }}-${{ inputs.ZONE }}
jobs:
changes:
name: Code & dependency changes
runs-on: ubuntu-latest
outputs:
prefect_flows: ${{ steps.filter.outputs.flows_files }}
prefect_flows_changed: ${{ steps.filter.outputs.flows }}
code_dependencies_changed: ${{ steps.filter.outputs.docker_code }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Generate Markdown Summary
run: echo "Starting CI/CD for flows and dependencies added/modified with commit $GITHUB_SHA" >> $GITHUB_STEP_SUMMARY
- uses: dorny/paths-filter@v2
id: filter
with:
list-files: json
filters: |
flows:
- added|modified: "${{ env.WORKINGDIR }}/flows/**/*"
- added|modified: "${{ env.WORKINGDIR }}/blocks.py"
- added|modified: "${{ env.WORKINGDIR }}/deployments.py"
docker_code:
- added|modified: "${{ env.WORKINGDIR }}/flows/**/*"
- added|modified: "${{ env.WORKINGDIR }}/requirements.txt"
- added|modified: "${{ env.WORKINGDIR }}/Dockerfile"
- added|modified: "ints/Dockerfile.agent"
- name: Generate Markdown Summary
run: |
echo Flows: ${{ steps.filter.outputs.flows_files }} >> $GITHUB_STEP_SUMMARY
echo Code dependency changes: ${{ steps.filter.outputs.code_files }} >> $GITHUB_STEP_SUMMARY
prefect_deploy:
needs: changes
if: needs.changes.outputs.prefect_flows_changed == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: '3.10.4'
architecture: 'x64'
- name: Python dependencies
working-directory: ${{ env.WORKINGDIR }}
run: |
pip install .
- name: Prefect Cloud login
run: |
prefect config set PREFECT_API_KEY=${{ secrets.PREFECT_API_KEY }}
prefect config set PREFECT_API_URL=https://FAKE_URL.COM
- name: Deploy Blocks to Prefect Cloud
id: build-blocks
working-directory: ${{ env.WORKINGDIR }}
run: |
cat <<EOF > gcp_cred_block.py
from prefect_gcp import GcpCredentials
service_account_info = ${{ secrets.GCP_CREDENTIALS }}
gcp_creds = GcpCredentials(service_account_info=service_account_info)
gcp_creds.save("my-srvc-usr", overwrite=True)
EOF
python gcp_cred_block.py
python blocks.py --env $ENVIRONMENT
- name: Deploy Flows to Prefect Cloud
id: build-deployments
working-directory: ${{ env.WORKINGDIR }}
run: |
python deployments.py --deployments-ver $GITHUB_SHA --env $ENVIRONMENT
- name: Upload YAML deployment manifest as artifact
uses: actions/upload-artifact@v3
with:
name: Deployment YAML manifests
path: ./ints/**/*.yaml
gcp_deploy:
needs: changes
if: needs.changes.outputs.code_dependencies_changed == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
# id-token: write is used by Google auth to request an OpenID Connect JWT Token https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings
outputs:
image: ${{ steps.build-image.outputs.image }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Login to GAR
uses: docker/login-action@v2
with:
registry: "${{ env.REGION }}-docker.pkg.dev"
username: _json_key
password: ${{ secrets.GCP_CREDENTIALS }}
- name: Set docker image URI
run: |
echo "AGENT_IMG_URI=$REGION-docker.pkg.dev/$PROJECT/$GCP_AR_REPO/my-project-prefect-agent-$ENVIRONMENT:latest" >> $GITHUB_ENV
echo "FLOWS_IMG_URI=$REGION-docker.pkg.dev/$PROJECT/$GCP_AR_REPO/$SUB_DIR-flows-$ENVIRONMENT:latest" >> $GITHUB_ENV
- name: Build and Push Agent Docker Image
id: build-agent-image
working-directory: ints
run: |
docker build \
--build-arg PREFECT_API_KEY=${{ secrets.PREFECT_API_KEY }} \
--build-arg PREFECT_API_URL=https://FAKE_URL.COM \
--build-arg PREFECT_WORK_QUEUE="$WORK_QUEUE" \
-t "${{ env.AGENT_IMG_URI }}" \
-f Dockerfile.agent .
docker push "${{ env.AGENT_IMG_URI }}"
- name: Build and Push Flows Docker Image
id: build-flows-image
working-directory: ${{ env.WORKINGDIR }}
run: |
docker build --build-arg ENVIRONMENT=$ENVIRONMENT -t "${{ env.FLOWS_IMG_URI }}" .
docker push "${{ env.FLOWS_IMG_URI }}"
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v1
with:
credentials_json: "${{ secrets.GCP_CREDENTIALS }}"
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v1
- name: Delete VM if exists
continue-on-error: true
run: gcloud compute instances delete "$GCE_INSTANCE" --zone "$GCE_INSTANCE_ZONE" --quiet
# Scopes defines which services VM needs, SA sets permissions for those
- name: Deploy Prefect Agent VM
run: |
gcloud compute instances create-with-container "$GCE_INSTANCE" \
--zone "$GCE_INSTANCE_ZONE" \
--machine-type "e2-micro" \
--scopes "cloud-platform" \
--service-account "[email protected]" \
--container-image "${{ env.AGENT_IMG_URI }}"
I’d like to just quickly summarize what it actually does. When triggered, the workflow looks for modifications made to specific files in the repo. If any of those files were, indeed, touched the workflow proceeds to deploy changes to two targets: Prefect Cloud, and Google Cloud Platform. This workflow handles all of the CI/CD needed to add and maintain mission critical ELT pipelines. All without ever leaving Github.