Promotion workflow
Description
To implement the continuous delivery pattern, we need to be able to deploy our new application version to a validation phase (test for this demo), run some tests and if everything is fine, promote this version to the next phase (prod for this demo).
We can easily implement that combining ArgoCD (and its notifications), Argo Events and Argo Workflows.
As soon as a new version is pushed by the CI to this GitOps repository, Argo CD detects it and applies the new version to the test namespace.
Argo CD sends then a notification when the synchronization is completed, and the status of the application is Healthy
.
This notification reaches an Argo Events' webhook EventSource, triggering as action an Argo Workflow.
This Workflow is composed of two steps:
- Run tests
- Promote application to the next phase (commit the version in git, in the next phase's values)
This loop can be repeated for as many software phases as you may have (dev, test, staging, prod, etc.).
Implementation
We'll start by the end of the promotion loop and go back step by step up the trigger chain.
Configuring Argo Workflows
The first step is to define the workflow to execute to validate that the new application deployed doesn't break any functional flow, and promote it to the next phase if successful.
We define one WorkflowTemplate
per action (test, promote), and combine them in another workflow to chain both actions. We are defining them at cluster scope (ClusterWorkflowTemplate
), to be able to use them in different namespaces.
Those templates will be instantiated by Argo Events upon the reception of specific notifications.
Tests workflow
apiVersion: argoproj.io/v1alpha1
kind: ClusterWorkflowTemplate
metadata:
name: workflow-template-run-integration-tests
spec:
entrypoint: integration-tests
podGC:
strategy: OnPodSuccess
templates:
- name: integration-tests
inputs:
parameters:
- name: test_target
- name: test_docker_image
- name: test_docker_command
container:
image: "localhost:32000/{{inputs.parameters.test_docker_image}}"
resources:
requests:
memory: 1Gi
limits:
memory: 2Gi
command:
- "{{inputs.parameters.test_docker_command}}"
args:
- "{{inputs.parameters.test_target}}"
This workflow executes a robot framework test campaign located here, validating that the application returns a valid Pet on its endpoint.
Git promotion workflow:
apiVersion: argoproj.io/v1alpha1
kind: ClusterWorkflowTemplate
metadata:
name: workflow-template-promote-to-next-phase
spec:
entrypoint: promotion
podGC:
strategy: OnPodSuccess
templates:
- name: promotion
inputs:
parameters:
- name: git_repo
- name: source_commit
- name: source_phase
- name: target_phase
- name: git_credentials_secret
container:
image: localhost:32000/argo-projects/git-promotion:latest
command:
- "python3"
- "promote.py"
args:
- "--git-repo"
- "{{inputs.parameters.git_repo}}"
- "--source-commit"
- "{{inputs.parameters.source_commit}}"
- "--source-phase"
- "{{inputs.parameters.source_phase}}"
- "--target-phase"
- "{{inputs.parameters.target_phase}}"
env:
- name: GIT_USER
valueFrom:
secretKeyRef:
name: "{{inputs.parameters.git_credentials_secret}}"
key: username
- name: GIT_PASSWORD
valueFrom:
secretKeyRef:
name: "{{inputs.parameters.git_credentials_secret}}"
key: password
- name: GIT_EMAIL
valueFrom:
secretKeyRef:
name: "{{inputs.parameters.git_credentials_secret}}"
key: email
The workflow executes a small python script pushing the application version from the "source_folder" on the commit hash "source_commit" to the "target_folder" on the "main" branch.
In more pratical terms, it'll align the version from the test phase defined here, to the production phase defined here.
The fact to take the synchronization commit hash as parameter ensures you are promoting the application version that was tested before, in case a new commit would bump it in test during the process.
The workflow combining both
apiVersion: argoproj.io/v1alpha1
kind: ClusterWorkflowTemplate
metadata:
name: workflow-template-validate-then-promote
spec:
entrypoint: validate-then-promote
podGC:
strategy: OnPodSuccess
arguments:
parameters:
- name: test_target
- name: test_docker_image
- name: test_docker_command
- name: git_repo
- name: source_commit
- name: source_phase
- name: target_phase
- name: git_credentials_secret
templates:
- name: validate-then-promote
dag:
tasks:
- name: promote-to-next-phase
depends: "run-integration-tests"
templateRef:
name: workflow-template-promote-to-next-phase
clusterScope: true
template: promotion
arguments:
parameters:
- name: git_repo
value: "{{workflow.parameters.git_repo}}"
- name: source_commit
value: "{{workflow.parameters.source_commit}}"
- name: source_phase
value: "{{workflow.parameters.source_phase}}"
- name: target_phase
value: "{{workflow.parameters.target_phase}}"
- name: git_credentials_secret
value: "{{workflow.parameters.git_credentials_secret}}"
- name: run-integration-tests
templateRef:
name: workflow-template-run-integration-tests
clusterScope: true
template: integration-tests
arguments:
parameters:
- name: test_target
value: "{{workflow.parameters.test_target}}"
- name: test_docker_image
value: "{{workflow.parameters.test_docker_image}}"
- name: test_docker_command
value: "{{workflow.parameters.test_docker_command}}"
Sources from workflow-templates.yaml.
Configuring Argo Events
Now that we have the workflows defined as templates, we need a trigger to instantiate them.
We'll use an Argo Events webhook as event source.
apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
name: webhook
spec:
webhook:
appSyncSuccess:
# port to run HTTP server on
port: "12001"
# endpoint to listen to
endpoint: /appSyncSuccess
# HTTP request method to allow. In this case, only POST requests are accepted
method: POST
And an associated trigger action: a Sensor triggering an Argo Workflow.
apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
name: webhook-app-promotion
spec:
template:
serviceAccountName: argo
dependencies:
- name: webhook-dep
eventSourceName: webhook # Reference to the webhook `EventSource`.
eventName: appSyncSuccess # Reference to the event inside the webhook.
triggers:
- template:
name: webhook-app-promotion
k8s:
operation: create # Create the Workflow resource
source:
resource:
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: app-
labels:
scenario: syncSuccess
phase: ""
spec:
serviceAccountName: argo
ttlStrategy:
secondsAfterCompletion: 43200 # Time to live in seconds after workflow is completed / 12h
arguments:
parameters:
- name: test_target
- name: source_commit
- name: source_phase
- name: target_phase
- name: test_docker_image
value: "argo-projects/test-campaign:latest"
- name: test_docker_command
value: "./launchTests.sh"
- name: git_repo
value: "https://github.com/AmadeusITGroup/argo-projects-demo.git"
- name: git_credentials_secret
value: "github-credentials"
workflowTemplateRef:
name: workflow-template-validate-then-promote # Reference to the workflow to instantiate.
clusterScope: true
parameters: # Workflow parameters read from webhook JSON body.
- src:
dependencyName: webhook-dep
dataTemplate: "{{ .Input.body.test_target }}"
dest: spec.arguments.parameters.0.value
- src:
dependencyName: webhook-dep
dataTemplate: "{{ .Input.body.source_commit }}"
dest: spec.arguments.parameters.1.value
- src:
dependencyName: webhook-dep
dataTemplate: "{{ .Input.body.source_phase }}"
dest: spec.arguments.parameters.2.value
- src:
dependencyName: webhook-dep
dataTemplate: "{{ .Input.body.target_phase }}"
dest: spec.arguments.parameters.3.value
- src:
dependencyName: webhook-dep
dataTemplate: "{{ .Input.body.source_phase }}"
dest: metadata.generateName
operation: append
- src:
dependencyName: webhook-dep
dataTemplate: "{{ .Input.body.source_phase }}"
dest: metadata.labels.phase
operation: append
With this setup, an HTTP POST to the webhook-service:12001/appSyncSuccess
will trigger the creation of a Workflow, following the template workflow-template-validate-then-promote
.
Sources from Argo Events resources.
You can see here that the github credentials are read from the Secret
called github-credentials
.
You would usually have that created securely with something like the External Secret Operator.
For the sake of simplicity for this use case, we'll create it manually:
apiVersion: v1
kind: Secret
metadata:
name: github-credentials
namespace: argo
type: Opaque
data:
username: <myUser/base64 encoded>
password: <myPassword/base64 encoded>
email: <myEmail/base64 encoded>
kubectl apply -f github-credentials.yaml -n argo
Configuring Argo CD notifications
To finish the setup, we need to plug events occuring during Argo CD deployments to trigger the webhook we just defined.
To achieve that, we will use Argo CD notifications.
We can configure it by patching the ConfigMap argocd-notifications-cm
.
apiVersion: apps/v1
kind: ConfigMap
metadata:
name: argocd-notifications-cm
data:
# generic webhook data
service.webhook.sync-webhook: | # service.<notificiation-type>.<webhook-name>
url: http://webhook-eventsource.argo.svc.cluster.local:12001 # ref to the webhook service
headers:
- name: "Content-Type"
value: "application/json"
# Webhook template that you can differentiate for each event inside the webhook, or each phase where you want to run the workflows
template.sync-success-test: |
webhook:
sync-webhook:
method: POST
path: /appSyncSuccess
body: |
{
"test_target": "http://app-test-my-app-svc.app-test.svc.cluster.local",
"source_phase": "test",
"target_phase": "prod",
"source_commit": "{{.app.status.operationState.operation.sync.revision}}"
}
trigger.on-sync-success: |
- when: app.status.operationState.phase in ['Succeeded'] and app.status.health.status == 'Healthy' and app.metadata.name == 'app-test'
oncePer: app.status.operationState.operation.sync.revision
send: [sync-success-test]
Source from notification-cm.yaml.
In the sync-sucess-test notification, we are sending a HTTP POST to our Argo Event webhook, sending in the body the parameters required for the workflow to run: the application endpoint to run the tests, the source phase, the target phase, and the commit hash having triggered the Argo CD synchronization.
This commit hash will be used to promote the changes from source_phase
folder in the hash source_commit
to the target_phase
folder in main
branch.
We trigger the notification only if:
- The synchronization is successful (
Succeeded
) - The application is
Healthy
Now we only need to activate the notification itself. We can do it for all Argo CD Applications
or for a subset.
We'll activate it only for our PetStore Applications
using an annotation.
metadata:
annotations:
notifications.argoproj.io/subscribe.on-sync-success.sync-webhook: ""
Source here.