Webux Lab

Par Studio Webux

Github Actions

TG
Tommy Gingras Studio Webux S.E.N.C 2021-07-21

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

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.

  1. À la racine de votre projet, créer un répertoire comme suit: .github/workflows/
  2. Pour garder le tout simple, nous allons seulement utiliser un seul workflow, créez un fichier comme suit : cd.yaml
  3. 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,

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.

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/ou frontend/ 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@v2pour 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 module actions/checkout@v2 pour être en mesure de toujours bien comparer avec la branche develop.
  • 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/ou frontend/, de plus il est être activé seulement par une branche qui commence par feature/*
  • 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

Sources


Recherche