A set of tools to support the implementation of GitOps workflows on GitHub.
Currently implemented is a GitHub Action intended for use in an application or service repository to trigger updates in a GitOps config repository.
Include this action in your CI pipeline towards the end of the workflow. It will manage the opening and merging of release PRs in the GitOps config repository. It accepts the following options:
steps:
# other test, build, etc steps
# ...
- name: Deploy
uses: docker://ghcr.io/geode-io/gitops-tools:latest
env:
APP_NAME: # Name of the application (optional if app config is provided)
APP_CONFIG: # Path to the application gitops config (optional if global config is provided)
TARGET_STACK: # If specified, will only run deployments for the specified stack (optional)
GLOBAL_CONFIG: # Path to the global gitops config (optional if app config is provided)
VALUE: # Value to update the files in the config repository (required)
GH_TOKEN: # Github PAT with proper permissions (optional if GH_APP_KEY is provided)
GH_APP_KEY: # Github App private key (optional if GH_TOKEN is provided)
GH_APP_ID: # Github App ID (optional if GH_TOKEN is provided)
GH_APP_INSTALLATION_ID: # Github App Installation ID (optional if GH_TOKEN is provided)
GIT_COMMIT_AUTHOR_NAME: # Name of the commit author (optional)
GIT_COMMIT_AUTHOR_EMAIL: # Email of the commit author (optional)
PR_TITLE: # Title of the PR in the config repository (optional)
PR_BODY: # Body of the PR in the config repository (optional)
This action will read a configuration file in your app repo to determine how it should update the config repository to deploy changes. The schema looks like this:
apiVersion: infrastructure.geode.io/v1alpha1
kind: GitOpsConfig
spec:
configRepo:
owner: string
repo: string
appPathPrefix: string
app: string
targetFiles:
- path: string
replacer: string
key: string
regex:
pattern: string
tmpl: string
deployments:
- sourceBranch: string
targetStack: string
autoDeploy: boolean
configRepo
is used to provide information about the config repository where the changes should be pushed.
owner
: Owner of the config repositoryrepo
: Name of the config repositoryappPathPrefix
: Prefix of the path where the configuration files are stored in the config repositoryapp
: Name of the application. It will be used with the combination ofappPathPrefix
to find the path where the configuration files are stored.
targetFiles
is used to define the files that should be updated with the provided value.
path
: Path of the file in the config repository. It should be relative to the combination ofappPathPrefix
andapp
from theconfigRepo
.replacer
: Replacer to be used to update the file. Currently,yaml
andregex
are supported.key
: Key to be updated in the file. It is used with theyaml
replacer.regex
: Regex pattern to be used to update the file. It is used with theregex
replacer.regex.pattern
: Pattern to be used to find the value to be replaced.regex.tmpl
: Template to be used to replace the value.
deployments
is used to define the deployments strategies for the changes.
sourceBranch
: Base branch in the config repository where the changes should be pushed.targetStack
: Stack where the changes should be deployed. It is used with the combination ofappPathPrefix
andapp
from theconfigRepo
.autoDeploy
: Flag to enable/disable the auto merge of the PR created by this action.
Let's say you have a mono repo where you have multiple services source code and you want to update the GitOps configuration after building images and pushing them to the registry. Here is an example of how you can use this action in the mono repo:
Application Mono Repository:
├── .github
│ └── workflows
│ └── release.yaml
├── gitops-actions.yaml
└── services
├── app-1
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── Dockerfile
│ ├── gitops-actions.yaml
│ └── src
└── app-2
├── Cargo.lock
├── Cargo.toml
├── Dockerfile
└── src
You can have a global configuration file in the root of the mono repository (or any other path) that defines the configuration for all the services. Here is an example of how the global configuration file can look like:
apiVersion: infrastructure.geode.io/v1alpha1
kind: GitOpsConfig
spec:
configRepo:
owner: geode-io
repo: gitops-config
appPathPrefix: services
targetFiles:
- path: config.yaml
replacer: yaml
key: tag
- path: main.tf
replacer: regex
regex:
pattern: '(ref=)([^\"]+)'
tmpl: '${1}'
deployments:
- sourceBranch: main
targetStack: dev
autoDeploy: true
- sourceBranch: main
targetStack: stage
autoDeploy: true
- sourceBranch: main
targetStack: prod
autoDeploy: false
In the above example, the targetFiles
defines the files that should be updated with the provided value.
- The first file is a
yaml
file where thetag
key should be updated with the provided value using theyaml
replacer.
image: 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-company/app-1
tag: 8d9bdc8e05bada480c0011d564910902a812a43a
# other configurations
- The second file is a
tf
file where theref
value should be updated with the provided value using theregex
replacer.
module "app-1" {
source = "git@github.com:geode-io/gitar-apps.git//terraform/modules/lambda?ref=8d9bdc8e05bada480c0011d564910902a812a43a"
... other configurations
}
The deployments
defines the deployment strategies for the changes. In the above example, pull requests will be created based on the main
branch in the config repository and auto-merge will be enabled for the dev
and stage
stacks.
if you have a separate configuration for a specific service, you can have a configuration file in the service directory. Here is configuration file for app-1
service which only has dev
and stage
stacks:
apiVersion: infrastructure.geode.io/v1alpha1
kind: GitOpsConfig
spec:
configRepo:
app: app-1
deployments:
- sourceBranch: main
targetStack: dev
autoDeploy: true
- sourceBranch: main
targetStack: stage
autoDeploy: true
Note
the spec.configRepo.app
has to be defined at the service level or can be set by the APP_NAME
environment variable in the action.
GitOps Configuration Repository: With the above example, the configuration repository will look like this:
├── .github
│ └── workflows
│ └── required-checks.yaml
└── services
├── app-1
│ ├── dev
│ │ ├── config.yaml
│ │ └── main.tf
│ └── stage
│ ├── config.yaml
│ └── main.tf
└── app-2
├── dev
│ ├── config.yaml
│ └── main.tf
├── prod
│ ├── config.yaml
│ └── main.tf
└── stage
├── config.yaml
└── main.tf
Tip
It is recommended to have a workflow in the config repository to run some required checks, like terraform plan
, ArgoCD Diff
, etc. This action expect the PR has some checks to be passed before auto-merging the PR.
Here is an example of how you can use this action in the mono repository workflow:
name: release
on:
push:
branches:
- main
pull_request:
branches:
- main
env:
AWS_ACCOUNT_ID: 123456789012
AWS_REGION: us-east-1
jobs:
affected:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.changed-files.outputs.all_changed_files}}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v44
with:
matrix: true
dir_names: true
dir_names_max_depth: 2
dir_names_exclude_current_dir: true
files: services/**
build-docker:
runs-on: ubuntu-latest
needs: [affected]
if: ${{ needs.affected.outputs.matrix != '[]' }}
strategy:
fail-fast: false
matrix:
service-path: ${{fromJson(needs.affected.outputs.matrix)}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to ECR
uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
registry: ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com
username: ${{ secrets.AWS_ACCESS_KEY_ID }}
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Get service name
id: service-name
run: |
echo "service=$(echo ${{ matrix.service-path }} | sed 's/services\///g')" >> "${GITHUB_OUTPUT}"
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/my-company/${{ steps.service-name.outputs.service }}
tags: |
type=raw,value=${{ github.event.pull_request.head.sha || github.sha }}
type=ref,event=branch
type=ref,event=pr
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push the image
uses: docker/build-push-action@v5
with:
context: ${{ matrix.service-path }}
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
APP_NAME=${{ steps.service-name.outputs.service }}
deploy:
runs-on: ubuntu-latest
needs: [affected, build-docker]
if: ${{ needs.affected.outputs.matrix != '[]' && github.event_name != 'pull_request' }}
strategy:
fail-fast: false
matrix:
service-path: ${{fromJson(needs.affected.outputs.matrix)}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: get service name drop services/ from path
id: service-name
run: |
echo "service=$(echo ${{ matrix.service-path }} | sed 's/services\///g')" >> "${GITHUB_OUTPUT}"
- id: create_token
uses: tibdex/github-app-token@v2
with:
app_id: 12345
private_key: ${{ secrets.PRIVATE_KEY }}
- name: Deploy
uses: docker://ghcr.io/geode-io/gitops-tools:latest
env:
GH_TOKEN: ${{ steps.create_token.outputs.token }}
APP_NAME: ${{ steps.service-name.outputs.service }}
APP_CONFIG: ${{ matrix.service-path }}/gitops-actions.yaml # load the service specific config if exists
TARGET_STACK: dev # if you want to deploy to a specific stack
VALUE: ${{ github.sha }}
GLOBAL_CONFIG: gitops-actions.yaml
GIT_COMMIT_AUTHOR_NAME: "geode-actions-bot"
Tip
It is recommended to use a Github App to authenticate with Github API. You can use the tibdex/github-app-token
action to create a token for the Github App and use it in the action.