Github Actions pour le CI/CD
Introduction
C’est un super service offert par GitHub. Il est simple à configurer et à utiliser.
Les fichiers pour faire les workflows sont faciles à mettre en place et les plug-ins disponibles permettent de faire tout ce dont vous avez besoin.
Le tout est intégré directement avec votre code (Pull Request, Section Actions et autres) et le UI est élégant et complet.
Github Actions UI
Structure
Le fichier workflow est constitué de plusieurs sections.
Documentation officielle: Github Actions
- La première partie est le nom du workflow, c’est celui qui va apparaitre sur le UI de Github dans la section Actions
- La seconde partie est le on, cette section permet de configuré les événements qui vont trigger le workflow en question, je vous invite a visiter la documentation officielle pour connaitre les combinaisons possibles. on
- La troisième partie est pour les jobs, donc les scripts et actions que vous allez faire dans votre workflow
Je recommande d’utiliser des scripts ou commandes Bash lorsque possible. La raison est que si vous voulez changer de service pour votre CI/CD, vous allez pouvoir le faire très facilement et rapidement.
- Une job est constituée de plusieurs sections, tels que
- Son identifiant (jobs.
) - Son nom
- Le paramètre runs-on permet de choisir l’os pour exécuter les commandes (OS supporté)
- Puis la section steps contient toutes les étapes qui seront exécutées
- Une étape peut utiliser des outils disponibles sur le Github Marketplace
- Des scripts bash
- Vous pouvez aussi créer vos propres modules, utiliser Docker et bien plus.
- Son identifiant (jobs.
- Une job est constituée de plusieurs sections, tels que
Cette structure couvre qu’une petite partie de ce que vous pouvez faire avec les github actions, laissez-moi un commentaire avec vos questions ! Je vous invite aussi à consulter la documentation officielle Github Actions Workflow
Exemple simple - déployer un projet VueJS sur AWS
Ce blogue est développé en VueJS et je me sers des Github actions pour déployer le tout sur S3 & CloudFront.
- À la racine de votre projet, créer un répertoire comme suit:
.github/workflows/
- Pour garder le tout simple, nous allons seulement utiliser un seul workflow, créez un fichier comme suit :
cd.yaml
- Ajouter le contenu du workflow:
.github/workflows/cd.yaml
name: Continuous Delivery
on:
push:
branches:
- main
jobs:
frontend:
# Name the Job
name: Frontend Deployment
# Set the type of machine to run on
runs-on: ubuntu-latest
steps:
# Checks out a copy of your repository on the ubuntu-latest machine
- name: Checkout code
uses: actions/checkout@v2
# Setup nodejs 12
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 12.x
# Install VUE Cli
- name: Install Vue CLI
run: npm install -g @vue/cli
# Configure the AWS credentials
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ca-central-1
# Run custom bash script available in the assets folder
- name: Upload Assets
working-directory: ./assets
run: bash publish.sh
# Run custom bash script available in the frontend folder
- name: Deploy frontend
working-directory: ./frontend
run: |
npm install
bash publish.sh
Dans cet exemple,
- Le CD est lancé lors d’un push sur la branche
main
- On utilise 3 modules disponibles sur le Github marketplace.
- Puis 3 commandes bash
Voici une capture d’écran d’une job lancée avec succès:
En bas de la page summary, vous pouvez voir les erreurs/recommandations de ESlint:
Exemple plus complet - CI & CD
Voici les pipelines d’un de mes projets.
Dans cet exemple, nous allons avoir plusieurs workflows.
./.github/workflows
cd-dev.yaml
cd-prod.yaml
ci-feat.yaml
create-release.yaml
layer-dev.yaml
layer-prod.yaml
weekly.yaml
./actions/
lambda-ci/
action.yml
vuejs-build/
action.yml
vuejs-ci/
action.yml
Ces workflows permettent de bien séparer les actions et de configurer les évènements de façon isolés.
Le répertoire actions
contient les actions qui sont partagées à travers tous les workflows. C’est l’équivalent d’utiliser un module sur le marrketplace. Par contre dans ce cas ils sont accessibles seulement localement au projet.
Vous pouvez aussi créer des actions et les publier publiquement ou les garder privés créer des actions
Actions personnalisées utilisant les composites steps
Ces 3 actions utilisent des commandes bash seulement. Donc en résumé ces workflows sont paramétrables et seront utilisés à travers les différentes jobs requises.
- La section inputs permet de configurer des variables qui vont être définies en utilisant l’option with (voir le workflow directement)
- La section outputs existe aussi : Documentation des Outputs
actions/lambda-ci/action.yml
- Cette action utilise
lerna
pour installer les dépendances, lancer ESLint, l’audit et les tests. - Vous pouvez voir que pour les tests la variable d’environnement
CI
est définie.
name: "Continuous Integration - Lambda"
description: "Lambda CI"
runs:
using: "composite"
steps:
- name: Install project root dependencies (Especially lerna)
shell: bash
run: npm ci
- name: Install dependencies
shell: bash
run: npm run ci:install-deps
- name: Run Linter
shell: bash
run: npm run ci:lint
- name: Run Audit
shell: bash
run: npm run ci:audit
- name: Run Tests
shell: bash
run: npm run ci:test
env:
CI: true
actions/vuejs-build/action.yml
- Nous avons plusieurs variables de définies pour permettre de build le frontend et d’exporter les fichiers plus tard.
name: "Continuous Integration - VueJS"
description: "VueJS CI"
inputs:
frontendPath: # id of input
description: "Frontend Directory"
required: true
default: "./frontend"
buildPath: # id of input
description: "Build Destination"
required: true
default: "./frontend/dist/**"
packageVersion: # id of input
description: "Package Version"
required: false
default: ""
runs:
using: "composite"
steps:
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.frontendPath }}
run: npm ci
- name: Update Package version
shell: bash
working-directory: ${{ inputs.frontendPath }}
run: npm version ${{ inputs.packageVersion }}
- name: Build Frontend
shell: bash
working-directory: ${{ inputs.frontendPath }}
run: npm run build -- --mode=production
actions/vuejs-ci/action.yml
- Comme pour le backend, les étapes du CI sont lancés en utilisant bash.
name: "Continuous Integration - VueJS"
description: "VueJS CI"
inputs:
frontendPath: # id of input
description: "Frontend Directory"
required: true
default: "./frontend"
runs:
using: "composite"
steps:
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.frontendPath }}
run: npm ci
- name: Run Linter
shell: bash
working-directory: ${{ inputs.frontendPath }}
run: npm run lint
- name: Run Audit
shell: bash
working-directory: ${{ inputs.frontendPath }}
run: npm audit --production
- name: Run Tests
shell: bash
working-directory: ${{ inputs.frontendPath }}
run: npm run test
Continuous Delivery pour dévelopment et production
cd-dev.yaml || cd-prod.yaml
J’ai enlevé les informations en trop pour simplifier quelques étapes. Le pipeline pour production est très similaire alors pour simplifier le tout je partage seulement une partie du pipeline de développement.
Voici ce à quoi ça ressemble visuellement:
- Ce workflow est plutôt complexe, il s’assure que le CI est OK puis il déploie le tout sur AWS.
- La façon de trigger ce workflow est de push sur la branche develop, puis seulement si des changements sont détectés dans les paths
lambdas/
et/oufrontend/
le pipeline va s’exécuter. - le
workflow_dispatch
permet de lancer le pipeline manuellement directement dans Github. Très pratique lorsque vous voulez déployer une branche spécifique pour faire un test ou autres. - La section
env
contient toutes les variables d’environnement requises pour ce pipeline. Assurez-vous de mettre des informations qui NE SONT PAS sensibles.- Les informations sensibles doivent être mises dans des secrets.
- Ce workflow contient 7 jobs qui ont des dépendances entre elles.
- J’ai aussi un timeout en place, car parfois les jobs bloquent sur github et pour éviter de consommer toutes les minutes disponibles, je mets un timeout à 15 minutes.
- Lorsqu’une job fail, vous recevez un courriel.
- Je dois configurer le NPM registry pour utiliser celui de Github pour les packages qui doivent être installés.
- Un secret est configuré pour récupérer le token de github.
- J’utilise ce module
dorny/paths-filter@v2
pour détecter les changements dans le code, il est super simple a utiliser et permet de faire des conditions pour lancer les commandes seulement au besoin.- j’ai dû configurer le
fetch-depth: 0
du moduleactions/checkout@v2
pour être en mesure de toujours bien comparer avec la branche develop.
- j’ai dû configurer le
- Les actions personnalisées sont appelées en utilisant le
uses: ./actions/nom-de-laction
- Pour le CD, j’utilise les commandes de AWS directement pour lancer les CloudFormation et SAM
- Je vais aussi migrer le tout pour utiliser les modules offerts dans Github. Par contre j’aime bien garder la flexibilité des scripts en bash au cas où je veux retourner sur CodePipeline pour la partie CD.
- Pour avoir des dépendances entre les jobs, vous devez utiliser
needs: ["lambdas-ci", "build-puppeteer-layer"]
en spécifiant le nom des jobs.
name: Continuous Delivery for dev environment
on:
push:
branches:
- develop
paths:
- lambdas/**
- frontend/**
workflow_dispatch:
env:
BUCKET_NAME: "OMITTED-dev"
AWS_REGION: "ca-central-1"
QUEUE_URL: "https://sqs.ca-central-1.amazonaws.com/OMITTED/project-dev"
TABLE_NAME: "OMITTED-dev"
INDEX_NAME: "schedule"
LOGGING_TABLE_NAME: "OMITTED-dev"
BOT_ACCESS_TABLE_NAME: "OMITTED-dev"
STAGE: "dev"
MODE: "development"
DISTRIBUTION: "OMITTED"
INFRA_BUCKET: "OMITTED-infra-ca-central-1"
jobs:
build-puppeteer-layer:
name: Build Puppeteer Layer
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Install dependencies
working-directory: layer/puppeteer/nodejs
run: npm install --production
- name: Archive artifacts
uses: actions/upload-artifact@v2
with:
name: latest-${{ env.MODE }}-puppeteer
path: |
./layer/puppeteer/nodejs
if-no-files-found: error
retention-days: 90
lambdas-ci:
name: CI lambdas
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: dorny/paths-filter@v2
id: lambdas
with:
filters: |
src:
- 'lambdas/**'
- name: Configure AWS credentials
if: steps.lambdas.outputs.src == 'true'
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Create Github Configuration
if: steps.lambdas.outputs.src == 'true'
run: |
echo "@webuxlab:registry=https://npm.pkg.github.com/" > ~/.npmrc
echo "//npm.pkg.github.com/:_authToken=${{ secrets.REGISTRY_TOKEN }}" >> ~/.npmrc
- name: Use Node.js
if: steps.lambdas.outputs.src == 'true'
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Lambda CI
if: steps.lambdas.outputs.src == 'true'
uses: ./actions/lambda-ci
frontend-ci:
name: CI frontend
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: dorny/paths-filter@v2
id: frontend
with:
filters: |
src:
- 'frontend/**'
- name: Use Node.js
if: steps.frontend.outputs.src == 'true'
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Frontend CI
if: steps.frontend.outputs.src == 'true'
uses: ./actions/vuejs-ci
frontend-build:
name: Build frontend
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: dorny/paths-filter@v2
id: frontend
with:
filters: |
src:
- 'frontend/**'
- name: Use Node.js
if: steps.frontend.outputs.src == 'true'
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Frontend Build
if: steps.frontend.outputs.src == 'true'
uses: ./actions/vuejs-build
- name: Archive development artifacts
uses: actions/upload-artifact@v2
with:
name: latest-${{ env.MODE }}-frontend
path: "./frontend/dist/**"
if-no-files-found: error
retention-days: 2
private-application-cd:
needs: ["lambdas-ci", "build-puppeteer-layer"]
name: CD Private Application
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Create Github Configuration
run: |
echo "@webuxlab:registry=https://npm.pkg.github.com/" > ~/.npmrc
echo "//npm.pkg.github.com/:_authToken=${{ secrets.REGISTRY_TOKEN }}" >> ~/.npmrc
- name: Package lambdas with SAM CLI
working-directory: ./infrastructure
run: sam build -t application.yaml --parallel
# 2021-01-24 Temporary solution
- name: Download Puppeteer artifact
uses: actions/download-artifact@master
with:
name: latest-${{ env.MODE }}-puppeteer
path: ./layer/puppeteer/nodejs
# 2021-01-24 Temporary solution
- name: Package Puppeteer Layer
working-directory: ./layer/puppeteer
run: zip -q -r nodejs.zip nodejs/
- name: Deploy lambdas (private-${{ env.STAGE }}) with SAM CLI
working-directory: ./infrastructure
run: |
sam deploy \
OMITTED
frontend-application-cd:
needs: ["frontend-ci", "frontend-build"]
name: CD Frontend Application
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: dorny/paths-filter@v2
id: frontend
with:
filters: |
src:
- 'frontend/**'
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Configure AWS credentials
if: steps.frontend.outputs.src == 'true'
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Download local frontend build artifact
if: steps.frontend.outputs.src == 'true'
uses: actions/download-artifact@master
with:
name: latest-${{ env.MODE }}-frontend
path: ./frontend/dist/
- name: Update Frontend
if: steps.frontend.outputs.src == 'true'
working-directory: ./frontend/dist
run: |
aws s3 sync ./ s3://project-name-${{ env.STAGE }}
aws cloudfront create-invalidation --distribution-id ${{ env.DISTRIBUTION }} --paths "/*"
infra-application-cd:
needs: ["lambdas-ci"]
name: CD Infrastructure Application
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy Infrastructure
working-directory: ./infrastructure
run: |
echo "Deploy Storage"
aws cloudformation deploy \
OMITTED
echo "Deploy Databases"
aws cloudformation deploy \
OMITTED
echo "Deploy SQS"
aws cloudformation deploy \
OMITTED
echo "Deploy SNS"
aws cloudformation deploy \
OMITTED
echo "Deploy Certificates"
aws cloudformation deploy \
OMITTED
echo "Create IAM"
aws cloudformation deploy \
OMITTED
echo "Deploy Frontend"
aws cloudformation deploy \
OMITTED
echo "Create API Keys"
aws cloudformation deploy \
OMITTED
Continuous Integration pour les feature branch
ci-feat.yaml
- Ces actions sont triggered seulement si un changement est détecté pour des fichiers présents dans
lambdas/
et/oufrontend/
, de plus il est être activé seulement par une branche qui commence parfeature/*
- Si une action est faite sur une Pull Request, le pipeline va être lancé, la raison est qu’il est nécessaire de le lancer pour assurer que les règles de vérifications soient appliquées.
- Donc ces 3 jobs sont tous lancées et le module
dorny/paths-filter@v2
va lancer les commandes seulement si nécessaire, si ce n’est pas nécessaire les commandes sont seulement skipped. - Le timeout est encore de 15 minutes pour chaque job.
- Puis vous pouvez lancer ce workflow manuellement.
name: Continuous Integration
on:
push:
branches:
- feature/*
paths:
- lambdas/**
- frontend/**
pull_request:
workflow_dispatch:
env:
BUCKET_NAME: "OMITTED-dev"
AWS_REGION: "ca-central-1"
QUEUE_URL: "https://sqs.ca-central-1.amazonaws.com/OMITTED/OMITTED-dev"
TABLE_NAME: "OMITTED-dev"
LOGGING_TABLE_NAME: "OMITTED-dev"
BOT_ACCESS_TABLE_NAME: "OMITTED-dev"
INDEX_NAME: "schedule"
jobs:
lambdas-ci:
name: CI lambdas
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: dorny/paths-filter@v2
id: lambdas
with:
filters: |
src:
- 'lambdas/**'
- name: Configure AWS credentials
if: steps.lambdas.outputs.src == 'true'
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Create Github Configuration
if: steps.lambdas.outputs.src == 'true'
run: |
echo "@webuxlab:registry=https://npm.pkg.github.com/" > ~/.npmrc
echo "//npm.pkg.github.com/:_authToken=${{ secrets.REGISTRY_TOKEN }}" >> ~/.npmrc
- name: Use Node.js
if: steps.lambdas.outputs.src == 'true'
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Lambda CI
if: steps.lambdas.outputs.src == 'true'
uses: ./actions/lambda-ci
frontend-ci:
name: CI frontend
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: dorny/paths-filter@v2
id: frontend
with:
filters: |
src:
- 'frontend/**'
- name: Use Node.js
if: steps.frontend.outputs.src == 'true'
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Frontend CI
if: steps.frontend.outputs.src == 'true'
uses: ./actions/vuejs-ci
frontend-build:
name: Build frontend
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: dorny/paths-filter@v2
id: frontend
with:
filters: |
src:
- 'frontend/**'
- name: Use Node.js
if: steps.frontend.outputs.src == 'true'
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Frontend Build
if: steps.frontend.outputs.src == 'true'
uses: ./actions/vuejs-build
La layer dans AWS
Voir la partie du Continuous Delivery plus haut.
La seule différence est l’usage d’une Cron job:
yaml
on:
push:
branches:
- develop
- feature/*
paths:
- layer/puppeteer/**
workflow_dispatch:
schedule:
- cron: "0 6 1 */3 *"
Le weekly run
weekly.yaml
À chaque semaine le CI et un audit sont lancés pour valider que la layer est à jour.
name: Weekly Check
on:
schedule:
- cron: "0 6 1 * *"
workflow_dispatch:
jobs:
check-puppeteer-layer:
name: Puppeteer Layer Dependencies Check
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Install dependencies
working-directory: layer/puppeteer/nodejs
run: npm ci
- name: Run audit
working-directory: layer/puppeteer/nodejs
run: npm audit --production
La création de release
create-release.yaml
Lorsqu’une branche commençant avec release/*
est créé, cette action va automatiquement créer une version dans Github.
- La condition vérifie que le nom de la branche correspond
- Puis crée une version draft de la version.
on:
create:
# Sequence of patterns matched against refs/tags
name: Create Release
jobs:
build:
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Create Release
if: contains(github.ref, "release")
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: |
Changes in this Release
- First Change
- Second Change
draft: true
prerelease: false
Conclusion
J’ai essayé de tout mettre la base et les trucs que j’utilise dans mon day-to-day et il reste encore plusieurs trucs à explorer.
Basé sur mon expérience avec CodePipeline, CodeBuild, Jenkins, Gitlab et Bitbucket Pipeline. Github actions est le plus simple et il est super bien intégré avec le code source.
Si vous avez des questions, commentaires et autres, n’hésitez pas à laisser un commentaire.
Pas couvert dans cet article
- Matrice pour tester différentes versions (https://docs.github.com/en/actions/learn-github-actions/managing-complex-workflows)
- Activer le mode de débogage (https://docs.github.com/en/actions/managing-workflow-runs/enabling-debug-logging)
- Utilisation de JavaScript pour créer une action (https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action)
- Utilisation de Docker pour créer une action (https://docs.github.com/en/actions/creating-actions/creating-a-docker-container-action)
Sources
- https://docs.github.com/en/actions
- Utilisation des secrets (https://docs.github.com/en/actions/learn-github-actions/managing-complex-workflows)
- Créer des dépendances entre les jobs (https://docs.github.com/en/actions/learn-github-actions/managing-complex-workflows)