Đăng vào

Sử dụng GitHub Actions để xây dựng CI cho một dự án Nextjs

Gần đây mình có làm việc nhiều liên quan đến GitHub Actions trong việc xây dựng CI/CD cho nhiều dự án sử dụng các ngôn ngữ: C/C++, Java, JavaScripts/Typescripts, ... Hôm nay tranh giới thiệu một ví dụ đơn giản trong việc xây dựng một quy trình CI dành cho một dự án frontend dùng Nextjs và có sử dụng Helm Chart để deploy trên Kubernetes.

Giới thiệu

GitHub Actions

GitHub Actions là một công cụ CI/CD mạnh mẽ tích hợp sẵn trên GitHub, cho phép bạn tự động hóa toàn bộ quy trình phát triển phần mềm từ xây dựng, kiểm thử, đến triển khai. Nó giúp bạn tạo ra các "workflow" (quy trình làm việc) để tự động thực hiện các tác vụ khi có các sự kiện cụ thể xảy trên repo chứa mã nguồn của bạn.

Với GitHub Actions, bạn không cần phải đăng ký dịch vụ CI/CD riêng biệt, không cần cài đặt và quản lý dịch vụ của riêng mình, và không cần thiết lập webhook và mã thông báo truy cập. Tất cả đều được tích hợp sẵn trong GitHub, giúp bạn tập trung vào việc phát triển mã nguồn mà không phải lo lắng về việc triển khai và kiểm tra.

Nextjs

Next.js là một framework mã nguồn mở được xây dựng trên nền tảng của React. Điểm đặc biệt của Next.js là khả năng tạo ra các trang web tĩnh (static) với tốc độ siêu nhanh và thân thiện với người dùng. Nó cũng cho phép bạn xây dựng các ứng dụng web React một cách hiệu quả. Next.js ra đời vào năm 2016 và hiện thuộc sở hữu của Vercel (trước đây là Zeit). Từ đó, Next.js đã trở thành một lựa chọn phổ biến trong cộng đồng phát triển web.

Trong bài viết này mình sẽ giới thiệu ví dụ sử dụng CI workflows đơn giản cho một project frontend dùng Nextjs:

  • Chạy CI build project
  • Scan secret dùng Gitleak
  • Scan docker image bằng Trivy
  • Khi PR được merge thì build và đẩy docker image lên ghcr.io

CI Workflow

Khi nhắc đến việc CI thông thường sẽ diễn ra khi Developer mở một Pull Request (PR), nó sẽ thực hiện các công việc như: build, unit test, ...

Một khi những thay đổi source code trong PR vượt qua dược các công việc trên rồi thì chúng ta mới yêu cầu các đồng nghiệp khác kiểm tra, đánh giá PR đó. Khi được họ đồng ý thì code mới của chúng ta sẽ được thêm vào nhánh chính và coi như xong 1 task.

Hiển nhiên, sau khi được PR được merge thì sẽ có những bài kiểm tra khác, tùy theo project cũng như mức độ phức tạp của dự án. Dưới đây mình chỉ đề cập đến một vài bước cơ bản khi xây dựng một CI cho dự dán frontend dùng NextJS. Các bước cở bản được thể hiện trong file YAML dưới đây:

name: CI Workflows

on:
  pull_request:
    types:
      - opened
      - reopened
      - synchronize
    branches:
      - main
    paths-ignore:
      - 'charts/**'
concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
jobs:
  ci-build:
    ...
  
  sonarqube:
    ...
  
  gitleaks:
    ...
  
  ci-docker:
    ...
  
  ci-trivy:
    ...

Trong đó:

  • on: phần này đề cập đến khi nào Workflows này sẽ chạy. Trong ví dụ này, CI Workflows sẽ chạy khi có PR được mở, PR được mở lại hoặc khi có thêm bất kì commit mới nào trong PR.
  • concurrency: phần này nhằm đảm bảo trong PR này mỗi lần chỉ có thể có duy nhât 1 Workflow chạy. Việc này chủ yếu nhằm tránh lãng phí tài nguyên. Chẳng hạn khi 2 commit trong PR cách nhau quá ngắn, CI của commit trước chạy chưa xong thì CI của commit sau đó buộc phải chờ.
  • jobs: đây chính là nội dung chính của một Workflow, các quy trình, công việc cần thực hiện sẽ được khai báo ở đây.

Sử dụng GitHub Actions cho dự án dùng Nextjs

...
jobs:
  ci-build:
    name: Build and test
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Use Node.js 20
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'yarn'
          cache-dependency-path: yarn.lock

      - name: Install dependencies
        run: yarn install --immutable
      
      # - name: Run lint
      #   run: yarn lint

      - name: Run build
        run: yarn build
      
      # - name: Run test
      #   run: npm test
  sonarqube:
    ...
  
  gitleaks:
    ...
  
  ci-docker:
    ...
  
  ci-trivy:
    ...

Một số lưu ý:

  • runs-on: phần này khai báo runner sẽ thực hiện công việc này. Thông thường sẽ có 2 loại runner: một loại runner do GitHub cung cấp, loại còn lại là runner do người dùng tự cài đặt và thiết lập (self-hosted runner). Với mỗi tài khoản GitHub cho bạn sử dụng 2000 phút chạy miễn phí mỗi tháng với điều kiện là repository để ở dạng công khai (public). Chúng ta có thể sử dụng thời lượng đó để phục vụ học tập nghiên cứu hoặc chạy các dự án nhỏ.
  • steps: phần này khai báo chi tiết các công việc sẽ chạy. Thông thường sẽ luôn có bước actions/checkout@v4 để clone code từ GitHub về runner. Ở ví dụ trên thì sau bước checkout sẽ cài đặt Nodejs do project này dùng Nextjs, rồi sẽ cài các thư viện (dependencies). Cuối cùng là các bước chạy cơ bản như lint, build và unit test.

Sử dụng SonarCloud trên GitHub Actions

SonarQube là một nền tảng mã nguồn mở được phát triển bởi SonarSource để kiểm soát chất lượng mã liên tục. Nó hỗ trợ hơn 30 ngôn ngữ lập trình chính với nhiều plugin khác nhau. SonarQube cloud sẽ cho dùng miễn phí khi mà chúng ta để code trên GitHub ở dạng public. Việc đăng ký khá dễ dàng, chỉ cần đăng nhập sonarcloud.io bằng tài khoản GitHub của bạn. Sau khi đăng nhập bạn tạo một Token và lưu nó trên phần quản lý Secrets của GitHub với tên SONAR_CLOUD_TOKEN chẳng hạn.

Dưới đây là ví dự về SonarCloud với GitHub Actions

jobs:
  ci-build:
  ...
  sonarqube:
    name: Run SonarQube scan
    runs-on: ubuntu-22.04
    needs: ci-build
    steps:
    - uses: actions/checkout@v4
      with:
        # Disabling shallow clones is recommended for improving the relevancy of reporting
        fetch-depth: 0
    - name: SonarQube Scan
      uses: sonarsource/sonarcloud-github-action@v2.3.0
      with:
        projectBaseDir: .
        args: >
          -Dsonar.projectKey=tailwind-nextjs-starter-blog-i18n
          -Dsonar.organization=${{ github.repository_owner }}
          -Dsonar.verbose=true
      env:
        SONAR_TOKEN: ${{ secrets.SONAR_CLOUD_TOKEN }}
        SONAR_HOST_URL: https://sonarcloud.io

Quét thông tin nhạy cảm với gitleaks

Gitleaks là một công cụ mã nguồn mở được thiết kế để phát hiện và ngăn chặn các bí mật mã hóa như mật khẩu, khóa API, và token trong các kho lưu trữ Git1. Đây là một công cụ phân tích bảo mật tĩnh (SAST) giúp phát hiện các bí mật được mã hóa cứng trong mã nguồn của bạn.

...
jobs:
  ci-build:
  ...
  sonarqube:
  ...
  gitleaks:
    name: Run secrets scan
    runs-on: ubuntu-22.04
    needs: ci-build
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Run Gitleaks
        id: gitleaks
        uses: DariuszPorowski/github-action-gitleaks@v2
        with:
          fail: false

      - name: Post PR comment
        uses: actions/github-script@v6
        if: ${{ steps.gitleaks.outputs.exitcode == 1 && github.event_name == 'pull_request' }}
        with:
          github-token: ${{ github.token }}
          script: |
            const { GITLEAKS_RESULT, GITLEAKS_OUTPUT } = process.env
            const output = `### ${GITLEAKS_RESULT}

            <details><summary>Log output</summary>

            ${GITLEAKS_OUTPUT}

            </details>
            `
            github.rest.issues.createComment({
              ...context.repo,
              issue_number: context.issue.number,
              body: output
            })
        env:
          GITLEAKS_RESULT: ${{ steps.gitleaks.outputs.result }}
          GITLEAKS_OUTPUT: ${{ steps.gitleaks.outputs.output }}

Build docker trên GitHub Actions

Trong dự án Nextjs này có sử dụng Docker để triển khai ứng dụng lên Kuberentes, nên bước CI nhất định phải có phần build Docker image. Thông thường, bước CI chúng ta chỉ build chứ không đẩy lên Artifactory liền.

...
jobs:
  ci-build:
  ...
  sonarqube:
  ...
  gitleaks:
  ...
  ci-docker:
    name: Build Docker image
    needs: ci-build
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: |
            ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: false
          cache-from: type=gha
          cache-to: type=gha,mode=max
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          outputs: type=docker,dest=frontend-image.tar
          # load: true
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: frontend-image
          path: frontend-image.tar

Ở bước cuối của ci-docker mình có đẩy image lên GitHub Artifact để dùng cho các bước CI khác, chẳng hạn như quét Docker image bằng Trivy dưới đây.

Chạy Trivy trên GitHub Actions

Trivy là một công cụ mã nguồn mở mạnh mẽ được thiết kế để quét các hình ảnh Docker nhằm phát hiện các lỗ hổng bảo mật. Sau khi docker build thành công thì chúng ta sẽ quét image đó xem có gặp lỗ hổng nào nghiêm trọng hay không bằng công cụ TriVy.

...
jobs:
  ci-build:
  ...
  sonarqube:
  ...
  gitleaks:
  ...
  ci-docker:
  ...
  ci-trivy:
    name: Trivy scan docker image
    needs: ci-docker
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Download frontend image to scan
        uses: actions/download-artifact@v4
        with:
          name: frontend-image
      - name: Run Trivy vulnerability scanner in tarball mode
        uses: aquasecurity/trivy-action@0.20.0
        with:
          input: frontend-image.tar
          ignore-unfixed: true
          severity: 'CRITICAL,HIGH'
          format: 'sarif'
          output: 'trivy-results.sarif'
      - name: Upload Trivy scan results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'
          category: Trivy

Như vậy, mình đã tóm lược sở bộ cách sử dụng GitHub Actions để chạy CI của một dự án sử dụng Nextjs, chi tiết có thể tham khảo thêm tại https://github.com/codemauvn/tailwind-nextjs-starter-blog-i18n/blob/main/.github/workflows/ci.yaml

Chúc thành công, Anh Nguyễn