likes
comments
collection
share

深入研究GitHub Actions可重用工作流

作者站长头像
站长
· 阅读数 12

概述

GitHub Actions 作为代码仓库的持续集成工具,提供了可重用工作流功能。这允许我们定义工作流后,存储在仓库中,供其他仓库调用与使用。可重用工作流的主要优点是减少重复工作,实现流程标准化。工作流可以集中定义与维护,被组织内的其他仓库轻松调用,简化了仓库设置和管理。但是,可重用工作流也有一定限制,如依赖外部仓库、调试困难、无法完全定制等。要使用可重用工作流,首先需要在公共仓库中创建 YAML 文件来定义工作流。然后,其他仓库可以使用 “uses” 关键字调用此工作流。调用仓库还可以传入输入参数以定制工作流,也可以覆盖部分步骤修改工作流行为。

在CI/CD中实现可重用工作流的详细说明

GitHub动作可重用工作流于2021年11月29日发布,并自那时起迅速流行起来。可重用工作流的好处非常直接:

  • 遵循DRY (Don 't Repeat Yourself)原则
  • 避免工作流程重复
  • 更容易维护工作流
  • 允许我们在其他人的工作基础上更快地创建新的工作流。
  • 通过使用精心设计并已经过测试的工作流来促进最佳实践。

通过:observation from Thoughtworks’ Technology Radar,

“GitHub Actions中的可重用工作流为管道设计带来了模块化,甚至允许跨存储库进行参数化重用(只要工作流存储库是公共的[或私有的])。它们支持显式地将机密值作为秘密传递,并可以将输出传递给调用作业。 通过几行YAML, GitHub Actions现在为你提供了CircleCI orb或Azure Pipeline模板的灵活性,但不必离开GitHub作为一个平台。”

请注意,上述方括号内引用的修改源于GitHub在2022年12月发布的公告,该公告涉及来自私有存储库的共享操作和可重用工作流的GA发布。

在这个故事中,我们将说明如何在CI/CD项目中实现可重用工作流。我们将从概述开始,然后深入研究可重用工作流。在我们的例子中,我们将使用两个GitHub库:

  • Spring Boot微服务,在其CI/CD中实现可重用工作流。这个微服务有两个工作流ci。Yml和cd.yml。这两个工作流的目的是不言自明的。
  • 一个集中的公共存储库,我们在其中存储可重用工作流。

service-reusable-workflow-example

这是一个典型的Spring Boot微服务,为客户提供CRUD端点。我们在它的.github/workflows目录ci下添加了两个工作流。Yml和cd.yml。请参阅下面的图表,了解这两个工作流中涉及的详细作业和步骤。

深入研究GitHub Actions可重用工作流

如果我们有许多具有类似工作流的微服务,那么在所有存储库中复制和粘贴这两个工作流就会变得乏味。如果我们需要对工作流中的一个或多个步骤进行更改,维护将成为一场噩梦。 让我们看看如何使这两个工作流可重用,这样就可以在集中式可重用工作流中进行更改,并反映在调用这些可重用工作流的所有微服务中。

reusable-workflows-modules

为了与上面提到的两个工作流相对应,我们从两个可重用工作流开始。你可以在下面看到它们。注意,我在可重用工作流的文件命名约定中指定了与特定工作流相关的编程语言(Java)、构建工具(Maven)和关键字(构建、测试、部署、ECS)。这允许在集中式存储库中轻松识别可重用工作流,特别是当您的组织可能使用多种编程语言时。每种语言都有自己的一组可重用工作流,由其命名约定标识。

  • 用于持续集成(CI),它负责代码构建、测试、映像构建以及向AWS弹性容器注册中心(ECR)推送映像。这个可重用的工作流将由ci.yml调用。假设您已经为微服务配置了ECR和ECR存储库。并在GitHub中配置了下面引用的秘密。
name: Build and Test workflow for Spring Boot microservices

on:
  workflow_call:
    inputs:
      # pass in environment through manual trigger, if not passed in, default to 'dev'
      env:
        required: true
        type: string
        default: 'dev'
      # working-directory is added to accommodate monorepo.  For multi repo, defaults to '.', current directory
      working-directory:
        required: false
        type: string
        default: '.'
      # pass in java version to allow different app requiring different java versions to reuse the same workflow, default to java 17
      java-version:
        required: false
        type: string
        default: '17'
      # allowing calling workflows to pass in maven parameter(s) such as '-Dmaven.test.skip' for certain apps, default to blank, not to skip test
      maven-params:
        required: false
        type: string
        default: ''

jobs:

  build:
    name: Build and Test
    runs-on: ubuntu-latest

    # accommodating monorepo, this sets the working directory at the job level, for multi repo, defaults to "."
    defaults:
      run:
        working-directory: ${{ inputs.working-directory }}

    environment: ${{ inputs.env || 'dev' }}

    # only run this job for auto trigger by PR merge, if manual trigger for other environments than dev,
    # no need to run this job as the image will be pulled and promoted to envs higher than dev
    # also dependabot PRs do not need to run this flow as GitHub prohibits dependabot PRs to access workflows
    # dealing with secrets due to security reason.
    if: (inputs.env == null || inputs.env == 'dev') && github.actor != 'dependabot[bot]'
    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@248ae51c2e8cc9622ecf50685c8bf7150c6e8813
        with:
          egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs

      - name: Checkout Code
        uses: actions/checkout@d0651293c4a5a52e711f25b41b05b2212f385d28

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@05b148adc31e091bafbaf404f745055d4d3bc9d2
        with:
          role-to-assume: ${{ secrets.ROLE_TO_ASSUME }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Setup jdk
        uses: actions/setup-java@2506d21b7426d2b544f65d027f277ead4c5f6a9f
        with:
          java-version: ${{ inputs.java-version }}
          distribution: 'adopt'
          cache: maven

      - name: Build with Maven
        run: mvn clean install ${{ inputs.maven-params }} --file pom.xml

      - name: Set project version as environment variable
        run: echo "PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV

      - name: Print debug info
        run: |
          echo environment is ${{ inputs.env }}
          echo working_directory is ${{ inputs.working-directory }}
          echo project version is ${{ env.PROJECT_VERSION }}
          echo java-version is ${{ inputs.java-version }}
      - name: Build, tag, and push image to AWS ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }}
          ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY_NAME }}
          IMAGE_TAG: ${{ env.PROJECT_VERSION }}
        run: |
          # Build a docker container and push it to ECR so that it can be deployed to ECS.
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker login -u AWS -p $(aws ecr get-login-password --region eu-west-1) $ECR_REGISTRY
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
      - name: Scan ECR image with Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@c666240787bede835456c7ceb9f75c9225c3c1b4
        with:
          image-ref: ${{ steps.build-image.outputs.image }}
          format: 'table'
          exit-code: '1'
          ignore-unfixed: true
          vuln-type: 'os,library'
          severity: 'CRITICAL,HIGH'
  • java-api-deploy-to-ecs.yml:用于持续部署(CD),从ECR提取映像并将其部署到AWS弹性容器服务(ECS)。这个可重用的工作流将被cd.yml调用。假设你已经为你的微服务准备了ECS Fargate集群。并在GitHub中配置了下面引用的秘密。
name: Deploy Spring Boot microservice to ECS Fargate

on:
  workflow_call:
    inputs:
      # pass in environment through manual trigger, if not passed in, default to 'dev'
      env:
        required: true
        type: string
        default: 'dev'
      # working-directory is added to accommodate monorepo.  For multi repo, defaults to '.', current directory
      working-directory:
        required: false
        type: string
        default: '.'

jobs:

  deploy:
    name: Deploy to AWS ECS Fargate
    runs-on: ubuntu-latest

    # accommodating monorepo, this sets the working directory at the job level, for multi repo, defaults to "."
    defaults:
      run:
        working-directory: ${{ inputs.working-directory }}

    # important to specify the environment here so workflow knows where to deploy your artifact to.
    # default environment to "dev" if it is not passed in through workflow_dispatch manual trigger
    environment: ${{ inputs.env || 'dev' }}

    # only execute if PR is merged or manual trigger
    if: github.event.pull_request.merged || inputs.env != null

    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@248ae51c2e8cc9622ecf50685c8bf7150c6e8813
        with:
          egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs

      - name: Checkout Code
        uses: actions/checkout@d0651293c4a5a52e711f25b41b05b2212f385d28

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@05b148adc31e091bafbaf404f745055d4d3bc9d2
        with:
          role-to-assume: ${{ secrets.ROLE_TO_ASSUME }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Set project version as environment variable
        run: echo "PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV

      - name: Print debug info
        run: |
          echo environment is ${{ inputs.env }}
          echo working_directory is ${{ inputs.working-directory }}
          echo project version is ${{ env.PROJECT_VERSION }}
          echo github.event.pull_request.merged is ${{ github.event.pull_request.merged }}
      - name: Download task definition
        run: |
          aws ecs describe-task-definition --task-definition ${{ secrets.ECS_TASK_DEFINITION }} --query taskDefinition | jq -r 'del(
                  .taskDefinitionArn,
                  .requiresAttributes,
                  .compatibilities,
                  .revision,
                  .status,
                  .registeredAt,
                  .registeredBy
                )' > task-definition.json
      - name: Fill in the new image ID and pass in the environment variable in the ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@374ee96751fffe528c09b5f427848da60469bb55
        with:
          # important to specify working directory here to accommodate monorepo
          task-definition: ${{ inputs.working-directory }}/task-definition.json
          container-name: ${{ secrets.CONTAINER_NAME }}
          image: ${{ secrets.ECR_REGISTRY }}/${{ secrets.ECR_REPOSITORY_NAME }}:${{ env.PROJECT_VERSION }}
          # this ENVIRONMENT is passed into the active spring profile in start-service.sh in the startup command,
          # so it knows which application yml to retrieve based on the active spring profile
          environment-variables: |
            ENVIRONMENT=${{ inputs.env || 'dev' }}
      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@9c18d81893224634ac107b91720119c91c1d600e
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ secrets.ECS_SERVICE }}
          cluster: ${{ secrets.ECS_CLUSTER }}
          wait-for-service-stability: true

现在让我们深入研究这两个可重用工作流:

可重用工作流的属性

可重用工作流的属性

与其他GitHub操作工作流文件一样,可重用工作流位于公共或私有存储库的. GitHub /workflows目录中。公共存储库。

注意:不支持工作流目录下的子目录。

添加workflow_call触发器

Workflow_call触发器是可重用工作流与普通工作流之间的关键区别。对于可重用的工作流,on的值必须包括workflow_call:

on:
  workflow_call:

添加可选的输入参数

可重用工作流在概念上是模板,这意味着它们很可能需要传递参数,以使它们特定于调用工作流。但是参数并不是使工作流可重用的必要条件。在大多数情况下,可重用工作流利用输入参数使其可用于各种环境、不同版本的编程语言等。 请参阅下面来自java-maven-build-test的代码片段。Yml,请注意每个输入参数上面的注释行,以了解更多关于这些输入参数的用途。


on:
  workflow_call:
    inputs:
      # pass in environment through manual trigger, if not passed in, default to 'dev'
      env:
        required: true
        type: string
        default: 'dev'
      # working-directory is added to accommodate monorepo.  For multi repo, defaults to '.', current directory
      working-directory:
        required: false
        type: string
        default: '.'
      # pass in java version to allow different app requiring different java versions to reuse the same workflow, default to java 17
      java-version:
        required: false
        type: string
        default: '17'
      # allowing calling workflows to pass in maven parameter(s) such as '-Dmaven.test.skip' for certain apps, default to blank, not to skip test
      maven-params:
        required: false
        type: string
        default: ''
  • 在可重用工作流中,我们使用输入关键字来定义将从调用方工作流传递的输入。
  • 在可重用工作流中,我们以 on 方法中定义的输入被引用{{ inputs.###}},例如 \{{ inputs.env}}

如何调用可重用工作流?

由于我们将可重用工作流托管在可重用工作流模块公共存储库中,所以我们可以使用以下语法引用可重用工作流文件:

  • {owner}/{repo}/.github/workflows/{filename}@{ref}

{ref}可以是SHA、发布标记或分支名称。例如,我们的客户-服务-可重用工作流示例的ci。Yml将可重用工作流称为java-maven-build-test。yml来构建和测试应用程序。请参见下面的第21行:


# This CI workflow can be triggered by PR creation or code push in PR, or manually using workflow dispatch.

name: CI workflow for building, testing microservice, and publishing image to ECR

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Environment to run the workflow against'
        type: environment
        required: true
  pull_request:
    branches: [ main ]

jobs:

  build-and-test:
    permissions:
      id-token: write # need this for OIDC
      contents: read
    uses: wenqiglantz/reusable-workflows-modules/.github/workflows/java-maven-build-test.yml@main
    with:
      env: ${{ github.event.inputs.environment }}
    secrets: inherit
  • 在可重用工作流场景中,特定作业的权限是在调用工作流中定义的。参见上面的第18-20行。
  • 为了从调用工作流传递输入或秘密,我们在调用工作流的作业中使用with关键字(第22行)。在同一个组织中调用可重用工作流的工作流可以使用inherit关键字(第24行)隐式传递在GitHub中定义的秘密。

通过在调用工作流中配置secrets: inherit,我们可以在可重用工作流中引用秘密,即使它们没有在on键中定义。请参见第4行和第5行。

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@05b148adc31e091bafbaf404f745055d4d3bc9d2
  with:
    role-to-assume: ${{ secrets.ROLE_TO_ASSUME }}
    aws-region: ${{ secrets.AWS_REGION }}

类似地,cd.yml调用可重用工作流java-api-deploy-to-ec .yml。

# This CD workflow pulls the docker image from ECR and deploys it to the environment specified by the manual trigger

name: CD workflow for deploying microservice to ECS Fargate

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Environment to run the workflow against'
        type: environment
        required: true
  pull_request:
    types: [closed]  # when PR is merged, CD will be triggered

jobs:

  deploy-to-ecs:
    permissions:
      id-token: write # need this for OIDC
      contents: read
    uses: wenqiglantz/reusable-workflows-modules/.github/workflows/java-api-deploy-to-ecs.yml@main
    with:
      env: ${{ github.event.inputs.environment }}
    secrets: inherit

总结

我们探索了关于如何使工作流可重用、如何调用可重用工作流、如何向可重用工作流传递输入参数和秘密的详细步骤。一旦你开始在你的项目中使用可重用工作流,你就再也不会回头了,因为可重用工作流在推出应用程序的CI/CD工作流和消除维护头痛方面节省了大量的时间和精力。 可重用工作流是 GitHub Actions 的重要功能,但是也需要注意其实现和使用中的种种限制与挑战。理解其机制与运用场景,有助于团队管理与技术人员设计可重用的工作流。掌握这项技能,可以提高组织的交付效率和工程效能。深入研究与总结GitHub Actions各项功能,有助我们更好地利用其实现持续集成与交付。这是软件工程师必备的技能,值得投入时间进行系统性学习与实践。

参考链接