Hay mucha teoría sobre pipelines de CI/CD, en artículos y libros pero no me ha resultado sencillo encontrar un ejemplo de implementación con todas esas buenas prácticas lo suficientemente genérico para que se adapte en gran medida a cualquier organización. En este artículo muestro un ejemplo de pipeline con workflows reutilizables usando Github Actions, Task y semantic-release. También comento algunos aspecto de contexto por el que surge la necesidad y los pasos hasta llegar a esta nueva implementación.
A lo largo del tiempo las empresas generan inevitablemente software heredado (o cantidades ingentes de software heredado) u obsoleto que por falta de tiempo para el mantenimiento, conocimiento o personas que ya no están en la empresa.
Para algunas empresas fundamentalmente tecnológicas es un problema ya que su valor y competitividad se asienta en la tecnología, a día de hoy la totalidad de las empresas son en gran medida empresas de tecnología y dependen de ella.
No poseer una buena tecnología con la que ofrecer sus servicios puede significar no generar beneficios y no ser competitiva que mantenido en el tiempo de una forma u otra el fracaso como compañía seguramente después de momentos dolorosos con varios procesos de despidos. También puede significar la pérdida de personas y su talento o ser incapaz de atraerlo, profesionalmente es más difícil que alguien esté interesado en una empresa si no la considera un ámbito atractivo profesionalmente en la que pueda aprender y crecer, para muchas personas trabajar con herramientas actuales es un requisito. Trabajar con código heredado puede tener su atractivo siempre y cuando haya oportunidades y voluntad de modernizarlo.
Y dicho esto una de las oportunidades en las que he podido cambiar en un contexto de mucho código heredado ha sido el pipeline de CI/CD, al menos para los proyectos modernos o en los que los cambios son posibles. Y después de leer el artículo si quieres comentar, ¿como es el el pipeline de CI/CD que usas en el trabajo? ¿que herramientas usas? ¿está completamente automatizado o hay pasos manuales? ¿hacéis teses funcionales una vez desplegado? ¿si haces algo diferente en tu empresa, que podría mejorar en este? Deja un comentario estaré encantado de leerlo y de aprender.
El pipeline de integración continua y despliegue continuo
A día de hoy un pipeline de integración continua es indispensable, su función es integrar los cambios de los desarrolladores y con pruebas unitarias validar que los cambios de código no introducen errores. Hay mucha teoría y libros explicando las importantes ventajas que proporciona esto. Además de descubrir errores, un pipeline de CI permite hacerlo rápido y de forma automatizada, lo que mejora la eficiencia y rapidez en el desarrollo de software.
El siguiente paso en la automatización es el despliegue continuo con el que el despliegue en el entorno de producción queda también automatizado. Automatizar el despliegue permite reducir el tiempo en introducir cambios, de forma más eficiente con menos esfuerzo y más fiable con menos errores. Quizá requiera una aprobación o despliegue en el momento deseado pero mayormente el despliegue está automatizado.
El último paso es la entrega continua con la que los cambios se despliegan en producción si todas las pruebas automatizadas validan el software correctamente, el software se despliega en producción totalmente automatizada sin aprobaciones. Esto requiere de pruebas automatizadas adicionales como teses de aceptación, funcionales, de seguridad, rendimiento.
Con todas las ventajas de la integración continua y el despliegue continuo el desarrollo de software actual se hace empleando estas técnicas de ingeniería de software.
Contexto
Uno de los puntos en el que había mucho legacy en la empresa en la que trabajo era el CI/CD, más que él es los varios que había, bueno ahora hay uno más pero este más moderno que dedicando tiempo tal vez podría reemplazar alguno de los antiguos.
El inicial era un Jenkins y luego algunos proyecto más modernizados pasaron a Concourse con ambos pipelines de integración continua funcionando. Los despliegues con Jenkins requería de varios pasos manuales con mucho margen de mejora en la automatización, toma una o dos horas hacer un despliegue. Esos Jenkins no eran un servicio administrado que había que mantener y dedicar tiempo a que sus instancias de computación funcionasen correctamente, además los pipelines no están bajo el control de los servicios que los hace poco flexibles, impone limitaciones en los nuevos servicio o requiere seguir las convenciones. Su coste era fijo independientemente de si se usaba o no.
Con Concourse las cosas son un poco mejores pero no usa algunas buenas prácticas, los pipelines de despliegue están separados de los proyectos y cada grupo de aplicaciones tiene su propio repositorio de pipeline de CI/CD, que se ha convertido en código heredado difícil de mantener o que requiere una buena cantidad de tiempo para conocerlos que habiendo alternativas es preferible esas alternativas. Es un servicio administrado pero las alternativas son mejores ya no solo porque otras están mejor documentadas.
Primera solución con Github Actions
Hace un tiempo Github añadió como complemento natural denominado Github Actions que en esencia es una herramienta de CI/CD más moderna. Uno de sus atractivos es que es un servicio administrado con lo que no requiere dedicar tiempo ni personas a administrarlo, por otro lado cualquiera con cuenta en Github puede usarlo con lo que muchos profesionales ya tienen conocimiento avanzado de como usarlo, tiene un coste superado un límite de uso pero es un coste por uso y no fijo independientemente de si se usa o no.
En un primer momento el uso que le dábamos era para pasar los teses unitarios con integración continua para cada push a un pull request y en el merge a la rama main. Luego para hacer el despliegue de algunos servicios en GKE. Luego también para enviar notificaciones a Slack en determinados canales para advertir de un nuevo despliegue y diversa información.
Github Actions tiene un marketplace de acciones en el que hay integraciones para la totalidad de herramientas populares y muchas de las menos conocidas, con lo que usar algo es sencillo, rápido y da mucha flexibilidad en caso de surgir una nueva necesidad.
Nuevo contexto
Usar Github Actions para la integración continua con teses unitarios y ser un servicio administrado ya era una mejora pero con una base de cientos de repositorios no es escalable ir replicando en cada repositorio el pipeline de CI/CD. De forma que ahora surge la necesidad de construir un pipeline reusable y suficientemente genérico para que cubra la necesidad como pipeline de los servicios.
Esto significa separar los repositorios del código de los pipeline nuevamente pero es una contrapartida opcional y preferible que copiar y pegar en la multitud de repositorios de código. Quizá con un monorepo sería otra la herramienta a utilizar pero en el contexto actual de multitud de repositorios es la opción viable, por tiempo, garantía de éxito y esfuerzo.
Utilizando Google Cloud el nuevo pipeline ha de soportar varios artefactos de construcción como Dockerfiles, librerías java y desplegar en GCP, Google App Engine (GAE), Google Cloud Functions o publicar las librerías en repositorios de Maven, de tal forma que el pipeline lo permita y suficientemente flexible para que el código no sea demasiado complicado además se ser escalable y permitir en el futuro añadir nuevos lenguajes, artefactos y entornos de ejecución.
Nueva solución con GitHub Actions
Así que toca buscar herramientas e implementar una solución usando Github Actions de tal forma que el nuevo pipeline sea reutilizable, suficientemente flexible para que cubra las diferentes necesidades y que sea suficientemente sencillo para que no sea un problema de mantenimiento.
Además de tener que construir diferentes artefactos, de poder desplegar en diferentes runtimes ha de soportar repositorios en diferentes lenguajes como Javascript, Typescript, Node y Java en diferentes versiones cada uno.
Para soportar esta diversidad de lenguajes y versiones el pipeline ha de independizarse y no acoplarse a cada lenguaje actual y los hipotéticos futuros, por ejemplo el comando de las tareas para los teses unitarios y análisis estático de código son diferentes para un proyecto de Node y otro de Java, GNU Make es una herramienta que proporciona esa abstracción pero finalmente opto por Task que es una equivalente que se define más simple y fácil de usar.
Otra de la necesidades es de realizar versionado semántico y crear tags en el repositorio de git con cada nueva versión, para los proyectos Java estábamos usando el plugin de Gradle Axion pero ahora necesitando soportar varios lenguajes para resolver este problema está semantic-release que además genera la nueva versión en función de los mensajes de commit.
También me ha sido necesario buscar algo de información de buenas prácticas en un pipeline. Que sea sencillo y que sea rápido por ejemplo en el paso de integración continua, en los siguientes artículos y libros hay más información. Para las fases de este pipeline me he basado en Hashicorp Waypoint que define tres build, deploy y release, separando el deploy del release con el cambio de tráfico a la nueva versión y ahí entrarían diferentes estrategias de release con canary o blue-green además de como realizar las actualizaciones de las instancias o el rollout.
Semantic release.
Enlaces.
Libros.
También he necesitado leer algunas secciones de documentación de Github Actions para aprender, por ejemplo para ver como configurar las reglas de protección de las ramas y diferentes opciones de merge.
Implementación de pipeline reutilizable con Github Actions
El pipeline como el anterior consta de dos fases principales la de build y la de deploy. La de build se divide a su vez en una primera base de comprobaciones en la que están incluidos los teses unitarios y el análisis estático de código en un proyecto Java con Gradle, JUnit, PMD, Checkstyle y Spotbugs. Además de si las comprobaciones son correctas generar la release.
Como es una buena práctica que la fase de integración continua sea rápida para obtener feedback lo más rápido posible cada una de esas comprobaciones se lanza en paralelo, esto tiene más coste pero el beneficio de la inmediatez es preferible (y estar esperando también tiene un coste). En los proyectos de Node las tareas de teses unitarios y análisis estático de código serán otros. Como he mencionado para esta abstracción he utilizado Task combinado con el concepto de matrix de Github Actions para lanzarlos en paralelo y permitir configurar el caller workflow cuales son las tareas de check.
Una vez la fase de check es correcta se lanza la fase de construcción del artefacto y de generación de la release con varias tareas específicas para cada artefacto que se activan con condiciones if en función de cómo ha de construirse el artefacto. Ya sea con un Dockerfile o una librería Java, o más que se añadan posteriormente con Buildpacks por ejemplo.
Construido el artefacto hay que generar la release y publicarlo en el repositorio de artefactos, para una imagen de contenedor en un repositorio de Docker y para una librería en un repositorio de Maven. Para etiquetar el repositorio de Git se utiliza semantic-release que en función de los mensajes de commit genera el siguiente identificador de versión en función de la versión anterior, de los mensajes de commit y de si es una rama de release (main) o no (otro branch y trabajando con Github un pull request).
El workflow de la fase de build
Hay varios archivos de configuración importantes, el de las tareas de Task que proporcionará el repositorio que haga uso de los workflows, el de la configuración de semantic-release que tiene una buena cantidad de opciones de configuración y el del propio workflow de build.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
version: '3'
dotenv: ['.env']
tasks:
build:
cmds:
- ./gradlew $GRADLE_OPTIONS build
build:check:
deps:
- task: build:test
- task: build:pmd
- task: build:checkstyle
- task: build:spotbugs
build:test:
cmds:
- ./gradlew $GRADLE_OPTIONS test
build:pmd:
cmds:
- ./gradlew $GRADLE_OPTIONS pmdMain pmdTest
build:checkstyle:
cmds:
- ./gradlew $GRADLE_OPTIONS checkstyleMain checkstyleTest
build:spotbugs:
cmds:
- ./gradlew $GRADLE_OPTIONS spotBugMain spotBugTest
build:release:
- npx semantic-release --no-ci --dry-run
build:publish:
- echo "publish"
run:
cmds:
- ./gradlew $GRADLE_OPTIONS run
noop:
- echo "noop"
|
taskfile.yml
1
2
3
4
5
6
7
8
|
#!/usr/bin/env bash
# Parameters
# * $1: ${nextRelease.version}
# * $2: ${lastRelease.version}
echo "$1" > .semantic-release-next-version
echo "$2" > .semantic-release-last-version
|
miscellaneous/task/publish.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
{
"plugins": [
[
"@semantic-release/commit-analyzer", {
"preset": "angular",
"releaseRules": [{
"message": "**", "release": "patch"
}]
}
],
[
"@semantic-release/release-notes-generator", {
"presetConfig": {
"issuePrefixes": ["ISSUE-"],
"issueUrlFormat": "https://organization.atlassian.net/browse//{{id}}"
}
}
[
"@semantic-release/changelog", {
"changelogFile": "CHANGELOG.md"
}
],
[
"@semantic-release/git", {
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}],
[
"@semantic-release/exec", {
"publishCmd": "./miscellaneous/task/publish.sh \"${nextRelease.version}\" \"${lastRelease.version}\""
}
]
],
"branches": [
{ "name": "main" },
{ "name": "*", "prerelease": true }
]
}
|
.releaserc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
|
name: build
on:
workflow_call:
inputs:
ref:
description: Reference to checkout
required: true
type: string
default: 'main'
java-version:
description: Java version
type: string
node-version:
description: Node version
type: string
setup-gradle:
description: Setup Gradle
type: boolean
check-tasks:
description: Tasks to execute for code check
type: string
required: true
artifact:
description: Artifact type
type: string
required: true
artifact-name:
description: Artifact name
type: string
required: true
artifact-context:
description: Artifact context
type: string
required: true
repository:
description: Artifact repository
type: string
required: true
outputs:
version:
description: Next release version
value: ${{ jobs.release.outputs.version }}
last-version:
description: Last release version
value: ${{ jobs.release.outputs.last-version }}
next-version:
description: Next release version
value: ${{ jobs.release.outputs.next-version }}
sha:
description: Release sha
value: ${{ jobs.release.outputs.sha }}
branch:
description: Release branch
value: ${{ jobs.release.outputs.branch }}
build-number:
description: Release build number
value: ${{ jobs.release.outputs.build-number }}
build-date:
description: Release build date
value: ${{ jobs.release.outputs.build-date }}
image-id:
description: Release image-id
value: ${{ jobs.release.outputs.image-id }}
secrets:
GITHUB_PACKAGES_REPOSITORY_DOWNLOAD_USERNAME:
required: true
GITHUB_PACKAGES_REPOSITORY_DOWNLOAD_PASSWORD:
required: true
env:
GOOGLE_CLOUD_WORKLOAD_IDENTITY_PROVIDER: projects/118532964247/locations/global/workloadIdentityPools/runners/providers/github
GOOGLE_CLOUD_SERVICE_ACCOUNT: gar-mgm-repository@common-1234.iam.gserviceaccount.com
GOOGLE_ARTIFACT_REGISTRY_HOST: us-east4-docker.pkg.dev
GOOGLE_ARTIFACT_REGISTRY_PROJECT: common-1234
GOOGLE_ARTIFACT_REGISTRY_REPOSITORY: repository
jobs:
check:
runs-on: ubuntu-22.04
strategy:
matrix:
task: ${{ fromJSON(inputs.check-tasks) }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup Java
uses: ./.github/actions/setup-java
if: ${{ inputs.java-version != null }}
with:
java-version: ${{ inputs.java-version }}
- name: Setup Node
uses: ./.github/actions/setup-node
if: ${{ inputs.node-version != null }}
with:
node-version: ${{ inputs.node-version }}
- name: Setup Gradle
uses: ./.github/actions/setup-gradle
if: ${{ inputs.setup-gradle }}
with:
github-packages-repository-download-username: ${{ secrets.GITHUB_PACKAGES_REPOSITORY_DOWNLOAD_USERNAME }}
github-packages-repository-download-password: ${{ secrets.GITHUB_PACKAGES_REPOSITORY_DOWNLOAD_PASSWORD }}
- name: Setup Task
uses: ./.github/actions/setup-task
- name: Run task ${{ matrix.task }}
run: task ${{ matrix.task }}
release:
needs: [check]
runs-on: ubuntu-22.04
outputs:
last-version: ${{ steps.build-information.outputs.last-version }}
next-version: ${{ steps.build-information.outputs.next-version }}
version: ${{ steps.build-information.outputs.version }}
sha: ${{ steps.build-information.outputs.sha }}
branch: ${{ steps.build-information.outputs.branch }}
build-number: ${{ steps.build-information.outputs.build-number }}
build-date: ${{ steps.build-information.outputs.build-date }}
image-id: ${{ steps.docker-build-push.outputs.imageid }}
permissions:
contents: 'write'
id-token: 'write'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Setup Semantic release
uses: ./.github/actions/setup-semantic-release
- name: Semantic release
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |-
unset GITHUB_ACTIONS && npx semantic-release --no-ci
- name: Build information
id: build-information
shell: bash
run: |-
LAST_VERSION="$(cat .semantic-release-last-version)"
NEXT_VERSION="$(cat .semantic-release-next-version)"
VERSION="${NEXT_VERSION}"
SHA="$(git rev-parse --short HEAD)"
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}"
BUILD_NUMBER="${{ github.run_number }}"
BUILD_DATE="$(date +"%Y-%m-%dT%H-%M-%S")"
echo "Last version: ${LAST_VERSION}"
echo "Next version: ${NEXT_VERSION}"
echo "Version: ${VERSION}"
echo "SHA: ${SHA}"
echo "Branch: ${BRANCH}"
echo "Build number: ${BUILD_NUMBER}"
echo "Build date: ${BUILD_DATE}"
echo "last-version=${LAST_VERSION}" >> $GITHUB_OUTPUT
echo "next-version=${NEXT_VERSION}" >> $GITHUB_OUTPUT
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "sha=${SHA}" >> $GITHUB_OUTPUT
echo "branch=${BRANCH}" >> $GITHUB_OUTPUT
echo "build-number=${BUILD_NUMBER}" >> $GITHUB_OUTPUT
echo "build-date=${BUILD_DATE}" >> $GITHUB_OUTPUT
- name: Authenticate to Google Cloud
id: google-cloud-auth
uses: google-github-actions/auth@v2
if: ${{ inputs.artifact == 'dockerfile' && inputs.repository == 'google-artifact-registry' }}
with:
token_format: access_token
workload_identity_provider: ${{ env.GOOGLE_CLOUD_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.GOOGLE_CLOUD_SERVICE_ACCOUNT }}
- name: Release library
if: ${{ inputs.artifact == 'jar-library' && github.ref_name == 'main' && github.event_name == 'push' }}
shell: bash
run: |-
task release
- name: Publish library
if: ${{ inputs.artifact == 'jar-library' && github.ref_name == 'main' && github.event_name == 'push' }}
run: |-
task publish
- name: Login to Google Artifact Registry
uses: docker/login-action@v3
if: ${{ inputs.artifact == 'dockerfile' && inputs.repository == 'google-artifact-registry' }}
with:
registry: ${{ env.GOOGLE_ARTIFACT_REGISTRY_HOST }}
username: oauth2accesstoken
password: ${{ steps.google-cloud-auth.outputs.access_token }}
- name: Build and push docker image to Google Artifact Registry
id: docker-build-push
uses: docker/build-push-action@v5
if: ${{ inputs.artifact == 'dockerfile' && inputs.repository == 'google-artifact-registry' }}
with:
push: true
context: ${{ inputs.artifact-context }}
tags: |
${{ env.GOOGLE_ARTIFACT_REGISTRY_HOST }}/${{ env.GOOGLE_ARTIFACT_REGISTRY_PROJECT }}/${{ env.GOOGLE_ARTIFACT_REGISTRY_REPOSITORY }}/${{ inputs.artifact-name }}:latest
${{ env.GOOGLE_ARTIFACT_REGISTRY_HOST }}/${{ env.GOOGLE_ARTIFACT_REGISTRY_PROJECT }}/${{ env.GOOGLE_ARTIFACT_REGISTRY_REPOSITORY }}/${{ inputs.artifact-name }}:${{ steps.build-information.outputs.sha }}
${{ env.GOOGLE_ARTIFACT_REGISTRY_HOST }}/${{ env.GOOGLE_ARTIFACT_REGISTRY_PROJECT }}/${{ env.GOOGLE_ARTIFACT_REGISTRY_REPOSITORY }}/${{ inputs.artifact-name }}:${{ steps.build-information.outputs.branch }}
${{ env.GOOGLE_ARTIFACT_REGISTRY_HOST }}/${{ env.GOOGLE_ARTIFACT_REGISTRY_PROJECT }}/${{ env.GOOGLE_ARTIFACT_REGISTRY_REPOSITORY }}/${{ inputs.artifact-name }}:${{ steps.build-information.outputs.version }}
build-args: |
ARTIFACT_NAME=${{ inputs.artifact-name }}
GITHUB_PACKAGES_REPOSITORY_DOWNLOAD_USERNAME=${{ secrets.GITHUB_PACKAGES_REPOSITORY_DOWNLOAD_USERNAME }}
GITHUB_PACKAGES_REPOSITORY_DOWNLOAD_PASSWORD=${{ secrets.GITHUB_PACKAGES_REPOSITORY_DOWNLOAD_PASSWORD }}
VERSION=${{ steps.build-information.outputs.version }}
SHA=${{ steps.build-information.outputs.sha }}
BUILD_NUMBER=${{ steps.build-information.outputs.build-number }}
BUILD_DATE=${{ steps.build-information.outputs.build-date }}
|
.github/workflows/build.yml
El workflow de la fase de deploy
La fase de deploy ha de desplegar el artefacto en el entorno de ejecución que necesite el servicio, puede ser un servicio que hace uso de GKE, de GAE o un Google Function. Hay dos entornos el de desarrollo que es un entorno de pruebas y el de producción, con reglas para requerir aprobaciones para hacer el despliegue que se han de configurar en cada repositorio.
Varios de los siguientes steps del workflow tienen if condicionales para que se activen los steps que corresponden. Añadir un nuevo entorno de ejecución sería añadir una nueva clave para identificar el entorno y nuevos steps para hacer su despliegue como corresponde. Tanto el workflow de build como el de deploy reciben una buena cantidad de parámetros con la que configurar el comportamiento de los workflows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
|
name: deploy
on:
workflow_call:
inputs:
ref:
description: Reference to checkout
type: string
required: true
default: 'main'
dry-run:
description: Dry run (no deploy)
type: boolean
required: true
default: false
environment:
description: Environment to deploy
type: string
required: true
default: 'development'
runtime:
description: Runtime type
type: string
required: true
gke-directory:
description: GKE deployment directory
type: string
required: false
gke-docker-image-name:
description: Docker image name to deploy
type: string
required: false
gae-deliverables:
description: Google App Engine deliverables
type: string
required: false
gae-project-id:
description: Google App Engine project id
type: string
required: false
gae-image-id:
description: Google App Engine image id
type: string
required: false
version:
description: Version to deploy
type: string
required: true
sha:
description: Sha to deploy
type: string
required: true
branch:
description: Branch to deploy
type: string
required: true
build-number:
description: Build number
type: string
required: true
build-date:
description: Build date
type: string
required: true
secrets:
slack-webhook-url:
required: true
concurrency:
group: deploy
cancel-in-progress: true
env:
GOOGLE_CLOUD_WORKLOAD_IDENTITY_PROVIDER: projects/118532964247/locations/global/workloadIdentityPools/runners/providers/github
GOOGLE_CLOUD_SERVICE_ACCOUNT: gar-mgm-repository@common-1234.iam.gserviceaccount.com
GOOGLE_ARTIFACT_REGISTRY_HOST: us-east4-docker.pkg.dev
GOOGLE_ARTIFACT_REGISTRY_PROJECT: common-1234
GOOGLE_ARTIFACT_REGISTRY_REPOSITORY: repository
GKE_PROJECT_DEVELOPMENT: infrastructure-devevelopment-1234
GKE_PROJECT_PRODUCTION: infrastructure-production-1234
jobs:
deployment:
runs-on: ubuntu-22.04-4core
environment: ${{ inputs.environment }}
permissions:
contents: 'read'
id-token: 'write'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Set deployment variables
shell: bash
run: |-
echo "DEPLOYMENT_DIRECTORY=${{ inputs.gke-directory }}" >> $GITHUB_ENV
if [ '${{ inputs.environment }}' == 'development' ]; then
echo "GKE_PROJECT=${{ env.GKE_PROJECT_DEVELOPMENT }}" >> $GITHUB_ENV
fi
if [ '${{ inputs.environment }}' == 'production' ]; then
echo "GKE_PROJECT=${{ env.GKE_PROJECT_PRODUCTION }}" >> $GITHUB_ENV
fi
- name: Authenticate to Google Cloud
id: google-cloud-auth
uses: google-github-actions/auth@v2
if: ${{ !inputs.dry-run && inputs.runtime == 'gke' }}
with:
token_format: access_token
workload_identity_provider: ${{ env.GOOGLE_CLOUD_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.GOOGLE_CLOUD_SERVICE_ACCOUNT }}
- name: Setup GKE credentials
uses: ./.github/actions/setup-gke-credentials
if: ${{ !inputs.dry-run && inputs.runtime == 'gke' }}
with:
gke-project: ${{ env.GKE_PROJECT }}
- name: Setup Kustomize
shell: bash
if: ${{ !inputs.dry-run && inputs.runtime == 'gke' }}
run: |-
cd ${{ env.DEPLOYMENT_DIRECTORY }}
curl -sfLo kustomize https://github.com/kubernetes-sigs/kustomize/releases/download/v3.1.0/kustomize_3.1.0_linux_amd64
chmod u+x ./kustomize
- name: Deploy to GKE
shell: bash
if: ${{ !inputs.dry-run && inputs.runtime == 'gke' }}
env:
GOOGLE_ARTIFACT_REGISTRY_HOST: ${{ env.GOOGLE_ARTIFACT_REGISTRY_HOST }}
GOOGLE_ARTIFACT_REGISTRY_PROJECT: ${{ env.GOOGLE_ARTIFACT_REGISTRY_PROJECT }}
GOOGLE_ARTIFACT_REGISTRY_REPOSITORY: ${{ env.GOOGLE_ARTIFACT_REGISTRY_REPOSITORY }}
DOCKER_IMAGE_NAME: ${{ inputs.gke-docker-image-name }}
run: |-
cd ${{ env.DEPLOYMENT_DIRECTORY }}
./kustomize edit set image ${GOOGLE_ARTIFACT_REGISTRY_HOST}/${GOOGLE_ARTIFACT_REGISTRY_PROJECT}/${GOOGLE_ARTIFACT_REGISTRY_REPOSITORY}/${DOCKER_IMAGE_NAME}:latest=${GOOGLE_ARTIFACT_REGISTRY_HOST}/${GOOGLE_ARTIFACT_REGISTRY_PROJECT}/${GOOGLE_ARTIFACT_REGISTRY_REPOSITORY}/${DOCKER_IMAGE_NAME}:${{ inputs.version }}
./kustomize build . | kubectl apply -f -
- name: Deploy to GAE
uses: google-github-actions/deploy-appengine@v2
if: ${{ !inputs.dry-run && inputs.runtime == 'gae' }}
with:
project_id: ${{ inputs.gae-project-id }}
deliverables: ${{ inputs.gae-deliverables }}
image_url: ${{ env.GOOGLE_ARTIFACT_REGISTRY_HOST }}/${{ env.GOOGLE_ARTIFACT_REGISTRY_PROJECT }}/${{ env.GOOGLE_ARTIFACT_REGISTRY_REPOSITORY }}/${{ inputs.gke-docker-image-name }}:${{ inputs.version }}
- name: Slack notification deploy
uses: ./.github/actions/slack-notification-deploy
with:
ref: ${{ inputs.ref }}
slack-webhook-url: ${{ secrets.slack-webhook-url }}
environment: ${{ inputs.environment }}
runtime: ${{ inputs.runtime }}
version: ${{ inputs.version }}
sha: ${{ inputs.sha }}
branch: ${{ inputs.branch }}
build-number: ${{ inputs.build-number }}
|
.github/workflows/deploy.yml
El workflow reutilizable del CI/CD
Estos son los workflows reutilizables del pipeline, cada repositorio tiene que definir un workflow que los invoque y proporcione los argumentos necesarios. Este puede ser uno de ejemplo para un proyecto de Java que construye una imagen de Docker con un Dockerfile, lo publica en Google Artifact Repository y lo despliega en GKE.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
name: main
on:
push:
branches:
- main
- master
pull_request:
types: ['opened', 'synchronize']
merge_group:
workflow_dispatch:
inputs:
ref:
description: 'Reference to checkout'
required: true
type: string
jobs:
build:
name: build
uses: organization/platform-github-actions-workflows/.github/workflows/build.yml@main
with:
ref: ${{ inputs.ref || github.head_ref || github.ref_name }}
java-version: '21'
node-version: '20'
setup-gradle: true
check-tasks: "['build:test', 'build:pmd', 'build:checkstyle', 'build:spotbugs']"
artifact: dummy
artifact-name: platform-github-actions-workflows
artifact-context: miscellaneous/docker/
repository: google-artifact-registry
secrets:
GITHUB_PACKAGES_REPOSITORY_DOWNLOAD_USERNAME: ${{ secrets.GH_USER_NAME }}
GITHUB_PACKAGES_REPOSITORY_DOWNLOAD_PASSWORD: ${{ secrets.GH_PACKAGES_DOWNLOAD_TOKEN }}
deploy-development:
name: deploy-development
needs: [build]
uses: organization/platform-github-actions-workflows/.github/workflows/deploy.yml@main
with:
ref: ${{ inputs.ref || github.head_ref || github.ref_name }}
dry-run: true
environment: development
runtime: gke
gke-directory: miscellaneous/gke/development
gke-docker-image-name: platform-github-actions-workflows
version: ${{ needs.build.outputs.version }}
sha: ${{ needs.build.outputs.sha }}
branch: ${{ needs.build.outputs.branch }}
build-number: ${{ needs.build.outputs.build-number }}
build-date: ${{ needs.build.outputs.build-date }}
secrets:
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
deploy-production:
name: deploy-production
if: contains('["main", "master"]', github.ref_name) && github.event_name == 'push'
needs: [build, deploy-development]
uses: organization/platform-github-actions-workflows/.github/workflows/deploy.yml@main
with:
ref: ${{ inputs.ref || github.head_ref || github.ref_name }}
dry-run: true
environment: production
runtime: gke
gke-directory: miscellaneous/gke/production
gke-docker-image-name: platform-github-actions-workflows
version: ${{ needs.build.outputs.version }}
sha: ${{ needs.build.outputs.sha }}
branch: ${{ needs.build.outputs.branch }}
build-number: ${{ needs.build.outputs.build-number }}
build-date: ${{ needs.build.outputs.build-date }}
secrets:
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
.github/workflows/main.yml
Para reutilizar acciones comunes en varios jobs el pipeline incluye algunas acciones también reutilizables, para configurar Gradle, Node, Task, Google Cloud y notificaciones de Slack.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
name: setup-gradle
description: 'Setup Gradle'
inputs:
java-version:
type: string
default: '21'
java-distribution:
type: string
default: 'temurin'
runs:
using: "composite"
steps:
- name: Setup JDK ${{ inputs.java-version }}-${{ inputs.java-distribution }}
uses: actions/setup-java@v4
with:
java-version: ${{ inputs.java-version }}
distribution: ${{ inputs.java-distribution }}
|
.github/actions/setup-java/action.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
name: setup-gradle
description: 'Setup Node'
inputs:
node-version:
type: string
default: '20'
runs:
using: "composite"
steps:
- name: Setup Node ${{ inputs.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
- name: Install npm packages
shell: bash
run: |-
npm install
|
.github/actions/setup-node/action.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
name: setup-gradle
description: 'Setup Gradle'
inputs:
github-packages-repository-download-username:
required: true
github-packages-repository-download-password:
required: true
runs:
using: "composite"
steps:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Configure Task
shell: bash
run: |-
echo "GRADLE_OPTIONS=-console plain" >> .env
echo "GITHUB_PACKAGES_REPOSITORY_DOWNLOAD_USERNAME=${{ inputs.github-packages-repository-download-username }}" >> .env
echo "GITHUB_PACKAGES_REPOSITORY_DOWNLOAD_PASSWORD=${{ inputs.github-packages-repository-download-password }}" >> .env
|
.github/actions/setup-gradle/action.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
name: setup-task
description: 'Setup Task'
inputs:
task-version:
default: '3.x'
runs:
using: "composite"
steps:
- name: Setup Task
uses: arduino/setup-task@v2
with:
version: 3.x
|
.github/actions/setup-task/action.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
name: setup-gradle
description: 'Setup Semantic Release'
inputs:
node-version:
type: string
default: '20'
runs:
using: "composite"
steps:
- name: Setup semantic release
uses: ./.github/actions/setup-node
with:
node-version: ${{ inputs.node-version }}
|
.github/actions/setup-semantic-release/action.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
name: slack-notification-deploy
description: 'Slack notification deploy'
inputs:
ref:
description: Reference to checkout
type: string
required: false
slack-webhook-url:
description: Reference to slack webhook url
type: string
required: true
environment:
description: Environment to deploy
type: string
required: true
runtime:
description: Runtime to deploy
type: string
required: true
version:
description: Version to deploy
type: string
required: true
sha:
description: Commit SHA to deploy
type: string
required: true
branch:
description: Branch to deploy
type: string
required: true
build-number:
description: Build number
type: string
required: true
build-date:
description: Build date
type: string
required: true
runs:
using: "composite"
steps:
- name: Get information
id: info
shell: bash
run: |-
REPOSITORY=$(echo "${{ github.repository }}" | sed -e 's/organization\///g')
if [ -n "${{ github.event.pull_request.number }}" ]; then
PR="${{ github.event.pull_request.number }}"
else
PR=$(echo "${{ github.event.head_commit.message }}" | head -1 | sed -r 's/^Merge pull request \#([0-9]+) from ([a-z]+)\/([A-Z0-9\-]+)$|^(.*)()()$/\1/')
fi
echo "repository=${REPOSITORY}" >> $GITHUB_OUTPUT
echo "pr=${PR}" >> $GITHUB_OUTPUT
echo "status_emoji=$(case "${{ job.status }}" in success) echo ":large_green_circle:" ;; failure) echo ":red_circle:" ;; *) echo ":large_yellow_circle:" ;; esac)" >> $GITHUB_OUTPUT
- name: Send job notification to Slack
id: slack
uses: slackapi/slack-github-action@v1.25.0
env:
SLACK_WEBHOOK_URL: ${{ inputs.slack-webhook-url }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
with:
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${{ steps.info.outputs.status_emoji }} Deploy for *${{ steps.info.outputs.repository }}* in *${{ inputs.environment }}*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":github: Repository: <https://github.com/${{ github.repository }}|${{ steps.info.outputs.repository }}>"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":github: Pull request: <https://github.com/${{ github.repository }}/pull/${{ steps.info.outputs.pr }}|${{ steps.info.outputs.pr }}>"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":hammer_and_wrench: Environment: ${{ inputs.environment }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":gear: Runtime: ${{ inputs.runtime }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":git: Branch: <https://github.com/${{ github.repository }}/tree/${{ inputs.branch || inputs.ref }}|${{ inputs.branch || inputs.ref }}>"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":git: Commit: <https://github.com/${{ github.repository }}/commit/${{ inputs.sha }}|${{ inputs.sha }}>"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":package: Version: ${{ inputs.version }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":dart: Build: <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|${{ github.run_number }}>"
}
}
]
}
|
.github/actions/slack-notification-deploy/action.yml
Este es el aspecto de la notificación en Slack.
Cambios a futuro
Los pipelines probablemente son trajes a medida según la organización cada una es diferente aunque todas comparten muchas necesidades. Este ejemplo tampoco es completo, por ejemplo podría añadirse la parte de pruebas una vez desplegado el artefacto en un entorno, pruebas funcionales, de rendimiento, seguridad, etc.
Es interesante también archivar los informes generados en caso de que un paso de check falle en la integración continua. O crear algún workflow periódico que no es útil ejecutar en cada push a una rama como comprobaciones de seguridad en proyectos Java con Owasp para comprobar las dependencias o la cobertura de los teses unitarios.
Si me es posible a medida que realice estos cambios actualizaré el artículo.