Obi Madu 的博客
返回所有文章
InfrastructureDevOpsProjects

通过 Github Pull Requests、Actions、Bot、Environments 和远程后端(Remote Backend)协作实现 Terraform 核心工作流。

使用 GitHub Actions、pull requests 和远程后端为生产团队实现协作式 Terraform 工作流。

通过 Github Pull Requests、Actions、Bot、Environments 和远程后端(Remote Backend)协作实现 Terraform 核心工作流。

你好!欢迎!!在这个项目中,我们将使用 Terraform 创建一些简单的基础设施,同时为生产环境协作实现 Terraform 的核心工作流(即编写、计划和应用)。简而言之,我们将以一种允许工程师团队引入系统更改、进行详细审查、批准或拒绝,并(如果获得批准)部署到生产环境的方式,使用 Terraform 部署基础设施。

我们在本项目中构建的系统将适用于一些中小型组织的运营(无论是否可能添加我们将在项目结尾讨论的更多内容)。

出于本项目的目的,你是一名基础设施工程师,负责为一家中型公司创建初始基础设施。我们将从头开始创建基础设施,全程使用版本控制 (Git),通过工作站上的本地后端 (Local Backend) 引导我们的系统,将其迁移到远程后端 (Remote Backend) 以实现协作,并创建脚本来自动化整个持续集成 (CI) 和持续部署 (CD) 流程。

我们将:

  • 在 DigitalOcean 和 Cloudflare 上使用 Terraform 创建极简的基础设施
  • 配置 Google Cloud Storage 远程后端以支持协作
  • 通过 Github Environments 和 Actions 工作流配置对 GCS 后端的访问权限
  • 创建 Actions 工作流脚本以初始化、验证、预览和应用我们的基础设施更改。
  • 利用 Github Pull Requests 和 Github Bot 来审查(并批准或拒绝)提议的基础设施更改。

这是 Github 上的项目仓库:https://github.com/obiMadu/terraform-github-workflow

让我们直接开始吧 🚀

1. 通过 Terraform 创建我们的基础设施

Infrastructure

我们将创建如上图所示的非常简单的基础设施。这是一个在 Digital Ocean 上运行 Nginx 的简单 Web 服务器,拥有一个 IPv4 地址,并且在 Cloudflare 区域中有一个 DNS A record 将一个子域名指向它。我们将仅通过创建两个 Terraform 资源来实现这一点。

你可以轻松地将 Cloudflare 替换为你选择的任何其他域名服务。例如,如果你通过 Amazon Route 53 管理域名,则无需为了本教程切换到 Cloudflare。只需创建必要的适当 Terraform 配置,将我们服务器实例的 IPv4 地址映射到域名(它也不一定必须是子域名,你可以映射一个顶级域名,随你喜欢)。

不出所料(很可能),你也不需要在 Digital Ocean 上创建服务器,你可以将其部署到 AWS 或你选择的任何其他提供商,只要我们获得一个安装了 Nginx 并具有 IPv4 地址的服务器即可。

为了让事情更有趣,你甚至可以配置一个基础服务器,并使用 Ansible 这样的工具来将 Nginx 或任何其他 Web 应用部署到它上面。你也可以使用 Packer 这样的工具构建自己的自定义镜像并使用它。由你决定。本项目旨在快速且易于跟进。

1.1 创建 providers.tf

接下来,我们将创建一个 providers.tf 文件来配置我们不同的提供商 (providers)。提供商的版本限制被设置为撰写本文时的最新版本。

terraform {
  required_providers {
    digitalocean = {
      source = "digitalocean/digitalocean"
      version = "2.34.1"
    }
    cloudflare = {
      source = "cloudflare/cloudflare"
      version = "4.24.0"
    }
  }
}

# Digital Ocean API credentials
provider "digitalocean" {
  token = var.do_token
}

provider "cloudflare" {
  api_token = var.cloudflare_api_token
}

以上是不同提供商正常运行所需的最绝对的最小参数。创建此文件后,我们将在我们的项目目录中初始化 Git,并通过以下命令将此文件作为我们的第一次 commit;

git init .
git add .
git commit -m "Initial commit"

我们将参数值变成了变量,以防止将这些敏感值硬编码到我们的配置文件中。还要记住,你可以将 Git commit 消息自定义为你喜欢的任何内容。

1.2 创建 variables.tf

由于我们已将变量作为值分配给我们提供商配置中的参数,因此是时候声明这些变量了。我们在一个名为 variables.tf 的文件中执行此操作,内容如下。

variable "do_token" {
  type = string
}

variable "do_region" {
  type = string
}

variable "cloudflare_api_token" {
  type = string
}

variable cloudflare_zone_id" {
  type = string
}

现在我们可以继续并将这个新文件 commit 到 Git,并附上适当的 commit 消息。

1.3 初始化 Terraform

现在我们有了基础配置,是时候在我们的项目目录中运行 terraform init 来初始化 Terraform 了。

Terraform Init

你的成功输出应该类似于上面的图片。

现在我们必须再做两件事;

  • 在我们的项目中添加一个适用于 Terraform 的 .gitignore 文件
  • 为我们已配置 Terraform 去使用的不同变量提供值。

我们可以从以下地址获取适用于 Terraform 的 .gitignore 文件:https://github.com/github/gitignore/blob/main/Terraform.gitignoretext

现在是时候继续并从各自的平台获取我们的提供商所需的不同 API 密钥了。我们将需要;

  • 来自 Digital Ocean 具有 write 访问权限的 API Token,以及一个区域短代码 (region shortcode)(用于指定我们希望在哪个区域部署我们的资源)
  • 来自 Cloudflare 的 API Token,以及我们希望在其中创建 DNS 记录的域名的 Zone ID。

这里将不介绍具体如何获取这些凭据(你应该能够获取它们)。

为了简单起见,你可以使用 fra1 DigitalOcean 区域。这对应于法兰克福数据中心。

现在是时候为 Terraform 提供我们获取的值了。简单起见,我们将创建一个 terraform.tfvars 文件并输入我们的变量值。我们的文件应如下所示;

do_token = "value"
do_region = "fra1"

cloudflare_api_token = "value"
cloudflare_zone_id = "value"

记住:你绝不能将 terraform.tfvars 文件 commit 到 Git。此文件包含绝不能与任何人共享的重要应用机密。如果威胁行为者获得了对这些机密的访问权限,我们的整个基础设施就有被攻破的风险。此文件的唯一目的是供本地使用。当我们进入 CI/CD 环境时,我们将采用更安全的方法向 Terraform 提供这些值。

1.4 创建资源

现在是时候实际为我们的基础设施创建资源了。我们将创建两个文件:一个 servers.tf 文件和一个 dns.tf 文件。

  • 第一个文件 servers.tf,我们将填充资源配置以使用官方 DigitalOcean Nginx 镜像创建我们的 DigitalOcean 服务器。如下所示(我们将创建一个 digitalocean_droplet 资源);
#create server with nginx image
resource "digitalocean_droplet" "server" {
  name     = "gate"
  size     = "s-1vcpu-1gb"
  image    = "nginx"
  region   = var.do_region
}
  • 第二个文件 dns.tf,我们将按如下进行配置(我们正在创建一个 cloudflare_record 资源);
# Add a record to the domain
resource "cloudflare_record" "server" {
  zone_id = var.cloudflare_zone_id
  name    = "server"
  value   = digitalocean_droplet.server.ipv4_address
  type    = "A"
  ttl     = 1
  proxied = true
}

现在保存这两个文件并将它们用适当的 commit 消息 commit 到 Git。

此时,你的目录树应完全如下所示;

Directory tree 1

2. 创建并初始化远程后端

是时候为我们的项目添加一个远程后端了。现在,因为我是一名 Google Cloud 工程师,所以我将在这个项目中使用 gcs 远程后端。你可以将其切换为您选择的任何其他远程后端,例如 s3 后端。

所以我继续创建了一个名为 terraform-github-workflow 的 Google Cloud Storage Bucket。我以相同的名称创建了一个服务账号 (service account),并在 GCS bucket 上为该主体分配了 Storage Object Admin 权限。这将允许服务账号在 terraform-github-workflow bucket 中创建和管理对象。接下来,我为服务账号主体创建了一个 JSON service account key,我下载了它并将其存储在本地工作站中一个安全的位置。

最后一步是将 GOOGLE_APPLICATION_CREDENTIALS 环境变量设置为下载到我 PC 上的 JSON 密钥的绝对路径,如下所示;

export GOOGLE_APPLICATION_CREDENTIALS=/path/to/json/key/file

为了安全起见,你总是可以创建一个不同的 service account key,以便在我们进入 CI/CD 环境时使用。

最后,是时候运行 terraform init 来初始化这个新后端了。根据你选择的后端,你应该会看到与下面非常相似的成功消息;

Initialize Remote Backend

3. 创建 Github Actions 工作流

现在我们的 Terraform 远程后端已初始化完毕,是时候为我们的 CI/CD 管道创建 Github Actions 脚本了。我们将创建总共 3 个不同的工作流,如下所示;

  • 一个 validate 工作流,它在我们项目的 plan 分支上的每次 push 时触发。这将使用 terraform validate 命令验证对我们代码库的任何新更改。
  • 一个 plan 工作流,它在每次对项目的 main 分支提出 Pull request 时触发。此工作流将对即将进行的基础设施更改进行空运行 (dry-run),并使用 Github Bot 将这些更改作为该 Pull request 上的评论发布。
  • 一个 deploy 工作流,它在每次向 main 分支 push/merge 时触发。这会部署我们批准的基础设施更改。

3.1 创建 workflows/validate.yml

我们将首先在项目目录的根目录中创建一个 .github/workflows 文件夹。在 workflows 目录中,我们将创建一个 validate.yml 文件。此时我们的目录树应如下所示;

Tree 2

以下是 validate 工作流文件的内容;

name: Validate

on:
    push: 
        branches: [plan]

env:
    GOOGLE_APPLICATION_CREDENTIALS: ${{ vars.GOOGLE_APPLICATION_CREDENTIALS }}      

jobs:
  deploy:
    runs-on: ubuntu-latest
      
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true  # Fetch submodules (true OR recursive)
          fetch-depth: 0    # Fetch all history for .GitInfo and .Lastmod

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_wrapper: true

      - name: Setup Backend Credentials
        id: backend
        run: echo '${{ secrets.GCS_KEY }}' > ${{ vars.GOOGLE_APPLICATION_CREDENTIALS }}

      - name: Terraform Init
        id: init
        run: terraform init

      - name: Terraform Validate
        id: terraform
        run: terraform validate -no-color

这个工作流:

  • 在每次推送到 plan 分支时触发
  • 运行在 latest Ubuntu 镜像上
  • 第一步 checkout 我们的代码
    • 然后设置 Terraform
    • 设置 gcs 后端凭据
    • 运行 terraform init &
    • 在我们的代码库上运行 terraform validate

现在让我们将此文件 commit 到 Git 并继续前进。

3.2 创建 workflows/deploy.yml

接下来,我们将创建包含以下内容的 deploy 工作流;

name: Deploy

on:
    push:
        branches: [main]

env:
    GOOGLE_APPLICATION_CREDENTIALS: ${{ vars.GOOGLE_APPLICATION_CREDENTIALS }}

jobs:
  deploy:
    runs-on: ubuntu-latest
      
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true  # Fetch submodules (true OR recursive)
          fetch-depth: 0    # Fetch all history for .GitInfo and .Lastmod

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3

      - name: Setup Backend Credentials
        id: backend
        run: echo '${{ secrets.GCS_KEY }}' > ${{ vars.GOOGLE_APPLICATION_CREDENTIALS }}

      - name: Terraform Init
        id: init
        run: terraform init
        
      - name: Terraform Apply
        id: apply
        env:
          TF_VAR_do_token: ${{ secrets.do_token }}
          TF_VAR_do_region: ${{ secrets.do_region }}
          TF_VAR_cloudflare_api_token: ${{ secrets.cloudflare_api_token }}
          TF_VAR_cloudflare_zone_id: ${{ secrets.cloudflare_zone_id }}
        run: terraform apply -auto-approve

这个工作流:

  • 在每次推送/合并到 main 分支时触发
  • 运行在 latest Ubuntu 镜像上
  • 第一步 checkout 我们的代码
    • 然后设置 Terraform
    • 设置 gcs 后端凭据
    • 运行 terraform init &
    • 运行带 -auto-approveterraform apply 以部署我们的基础设施

再次将此工作流使用适当的 commit 消息 commit 到 Git,我们将创建最后一个工作流。

3.3 创建 workflows/plan.yml

我们将创建的最后一个(但绝对不是最不重要的)工作流是 plan 工作流。我们最后创建此工作流,因为我们将在此工作流中集成我们的 Github Bot,以帮助提供空运行 (dry-runs) 的输出,从而使我们的 Pull-request 审查变得更容易、更有价值。

因此,我们将首先利用必要的功能构建工作流,然后在下一步中集成 Bot。以下是我们的 plan 工作流的代码;

name: Plan

on:
    pull_request: 
        branches: [main]

env:
  GOOGLE_APPLICATION_CREDENTIALS: ${{ vars.GOOGLE_APPLICATION_CREDENTIALS }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions: 
      pull-requests: write
      
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true  # Fetch submodules (true OR recursive)
          fetch-depth: 0    # Fetch all history for .GitInfo and .Lastmod

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_wrapper: true

      - name: Setup Backend Credentials
        id: backend
        run: echo '${{ secrets.GCS_KEY }}' > ${{ vars.GOOGLE_APPLICATION_CREDENTIALS }}

      - name: Terraform Init
        id: init
        run: terraform init

      - name: Terraform fmt
        id: fmt
        run: terraform fmt -check
        continue-on-error: true

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color
        
      - name: Terraform Plan
        id: plan
        env:
            TF_VAR_do_token: ${{ secrets.do_token }}
            TF_VAR_do_region: ${{ secrets.do_region }}
            TF_VAR_cloudflare_api_token: ${{ secrets.cloudflare_api_token }}
            TF_VAR_cloudflare_zone_id: ${{ secrets.cloudflare_zone_id }}
        run: terraform plan -no-color

这个工作流:

  • 在每次 pull-request 到 main 分支时触发
  • 运行在 latest Ubuntu 镜像上
  • 获取对 Pull requests 的写入权限
  • 第一步 checkout 我们的代码
    • 然后设置 Terraform
    • 设置 gcs 后端凭据
    • 运行 terraform init
    • 运行 terraform fmt
    • 运行 terraform validate(再次)
    • 运行对我们的基础设施进行空运行 (dry-run) 的 terraform plan

完美!现在我们可以继续并在下一节中添加 Bot。记得将你新的 plan 工作流 commit 到 Git。

4. 设置 Github Bot

将 Github Bot 添加到我们的工作流中非常简单。这只是我们的 plan 工作流上的一个额外步骤。

将以下步骤添加到您的 plan.yml 工作流文件中:

      - name: Comment plan on PR
        uses: actions/github-script@v6
        if: github.event_name == 'pull_request'
        env:
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
                // 1. Retrieve existing bot comments for the PR
                const { data: comments } = await github.rest.issues.listComments({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.issue.number,
                })
                const botComment = comments.find(comment => {
                return comment.user.type === 'Bot' && comment.body.includes('Terraform Format and Style')
                })

                // 2. Prepare format of the comment
                const output = ` ## Terraform Results
                #### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
                #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
                #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
                <details><summary>Validation Output</summary>

                \`\`\`\n
                ${{ steps.validate.outputs.stdout }}
                \`\`\`

                </details>

                #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`

                <details><summary>Show Plan</summary>

                \`\`\`\n
                ${process.env.PLAN}
                \`\`\`

                </details>

                *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Working Directory: \`${{ env.tf_actions_working_dir }}\`, Workflow: \`${{ github.workflow }}\`*`;

                // 3. If we have a comment, update it, otherwise create a new one
                if (botComment) {
                github.rest.issues.updateComment({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    comment_id: botComment.id,
                    body: output
                })
                } else {
                github.rest.issues.createComment({
                    issue_number: context.issue.number,
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    body: output
                })
                }

你的整个 plan.yml 文件现在应该完全如下所示:

name: Plan

on:
    pull_request: 
        branches: [main]

env:
  GOOGLE_APPLICATION_CREDENTIALS: ${{ vars.GOOGLE_APPLICATION_CREDENTIALS }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions: 
      pull-requests: write
      
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true  # Fetch submodules (true OR recursive)
          fetch-depth: 0    # Fetch all history for .GitInfo and .Lastmod

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_wrapper: true

      - name: Setup Backend Credentials
        id: backend
        run: echo '${{ secrets.GCS_KEY }}' > ${{ vars.GOOGLE_APPLICATION_CREDENTIALS }}

      - name: Terraform Init
        id: init
        run: terraform init

      - name: Terraform fmt
        id: fmt
        run: terraform fmt -check
        continue-on-error: true

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color
        
      - name: Terraform Plan
        id: plan
        env:
            TF_VAR_do_token: ${{ secrets.do_token }}
            TF_VAR_do_region: ${{ secrets.do_region }}
            TF_VAR_cloudflare_api_token: ${{ secrets.cloudflare_api_token }}
            TF_VAR_cloudflare_zone_id: ${{ secrets.cloudflare_zone_id }}
        run: terraform plan -no-color

      - name: Comment plan on PR
        uses: actions/github-script@v6
        if: github.event_name == 'pull_request'
        env:
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
                // 1. Retrieve existing bot comments for the PR
                const { data: comments } = await github.rest.issues.listComments({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.issue.number,
                })
                const botComment = comments.find(comment => {
                return comment.user.type === 'Bot' && comment.body.includes('Terraform Format and Style')
                })

                // 2. Prepare format of the comment
                const output = ` ## Terraform Results
                #### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
                #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
                #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
                <details><summary>Validation Output</summary>

                \`\`\`\n
                ${{ steps.validate.outputs.stdout }}
                \`\`\`

                </details>

                #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`

                <details><summary>Show Plan</summary>

                \`\`\`\n
                ${process.env.PLAN}
                \`\`\`

                </details>

                *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Working Directory: \`${{ env.tf_actions_working_dir }}\`, Workflow: \`${{ github.workflow }}\`*`;

                // 3. If we have a comment, update it, otherwise create a new one
                if (botComment) {
                github.rest.issues.updateComment({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    comment_id: botComment.id,
                    body: output
                })
                } else {
                github.rest.issues.createComment({
                    issue_number: context.issue.number,
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    body: output
                })
                }

5. 将代码库推送到 Github

在你完成上述新添加内容至 Git 的 commit 后,是时候我们将我们的基础设施代码库发布 (publish) 到 Github 了。

现在只需前往 Github 并创建一个公共(或私有)仓库,将该仓库添加为我们本地机器上项目的 origin 远程,然后进行推送 (push)。

这样做之后,你的 deploy 工作流应该会立即开始执行,但是不用担心,因为它会失败。它会失败是因为 Github Actions 环境尚未配置合适的凭据来初始化我们的 Terraform Backend,也没有为我们基础设施中定义的变量提供适当的值。

The very first Deploy workflow fails
Failed Deploy workflow details

现在是时候为 Github Actions CI/CD 环境正确配置适当的访问凭据以部署我们的工作负载了。

6. 为 Terraform 后端和变量配置 Github Environments

现在,我们需要前往 Github Environments 并配置我们工作流成功所需的机密 (secrets) 和环境变量 (env vars)。

如果你一直关注我们的 Github Actions 工作流,你会发现对于每一个影响 Terraform 状态的操作,我们都包含了 Terraform 执行所需的不同环境变量。在 deploy 工作流中的 terraform apply 命令处就可以找到一个例子。代码片段如下;

      - name: Terraform Apply
        id: apply
        env:
          TF_VAR_do_token: ${{ secrets.do_token }}
          TF_VAR_do_region: ${{ secrets.do_region }}
          TF_VAR_cloudflare_api_token: ${{ secrets.cloudflare_api_token }}
          TF_VAR_cloudflare_zone_id: ${{ secrets.cloudflare_zone_id }}
        run: terraform apply -auto-approve

因此下面是我们所需设置的所有不同 secretsenv vars 的列表(你将看到它们全部都已融入到了不同工作流的合适位置中);

  • Terraform 变量
    • TF_VAR_do_token
    • TF_VAR_do_region
    • TF_VAR_cloudflare_api_token
    • TF_VAR_cloudflare_zone_id
  • 远程后端机密和变量
    • GCS_KEY - Google Cloud Storage 密钥。
    • GOOGLE_APPLICATION_CREDENTIALS - 文件系统中 Google Cloud Storage 密钥的路径

切记为 Github Actions 环境创建一个不同的 GCS Key,并禁用我们用于在本地初始化 Terraform 的那个,从现在起,对基础设施的任何新更改都将通过我们的工作流进行处理。你甚至不应将你的密钥用于初始的资源部署。

好的,现在前往你的 Github Dashboard -> Settings -> Secrets and Variables -> Actions 页面。如下面页面所示。

Create secrets and variables.

接下来我们将继续填充不同机密和环境变量的值。

由于在这个项目中我们只处理一个单一的环境,因此我们将把这些填为仓库 (Repository) 的机密和变量。在一个更高级的设置中,如果我们拥有 dev、stage、qa、prod 环境,我们将利用核心的 Github Environments 解决方案。这个项目没有那么高级,所以我们将坚持使用 Repo 的机密和变量。

  1. 除了 GOOGLE_APPLICATION_CREDENTIALS 变量外,其它所有的都应是机密 (secret)。为了简单起见,此变量请使用值 ./key.json

  2. 要在机密 (secrets) 和变量 (variables) 之间进行切换,请使用页面上相同名称的选项卡。

当一切设置完毕时,我们的页面应如下所示:

All our repo secrets set
All our repo variables set

现在我们所有的机密和变量都配置好了。 🎉 🎉

7. 测试整个工作流

最后,是时候让这个巨大的轮子转动起来了。

7.1 创建 plan 分支

第一步将是创建项目的 plan 分支。我们在前面的步骤中多次提到了它,是时候真正创建它了。

所以我们将用以下命令来实现:

git checkout -b plan

这将创建新的 plan 分支并将其切换 (check-it-out) 到我们当前的工作目录。接下来我们将此分支推送到 Github,并将其设置为跟踪同一个名为 plan 的新 origin 分支。

git push -u origin plan

此推送应该会在 Github 中触发一个 validate 工作流,如果此工作流成功,那么说明我们在正确的道路上。如果不成功,那么你需要四处寻找看漏掉了什么以便修复它。查看 Actions 工作流错误日志以理解任何错误。

但是,如果你严格遵循本演练,你应该会看到与以下类似的成功画面。你最初的 validate 工作流应该会成功。

Successfull Validate workflow run
All steps executed successfully

7.2 准备并创建第一个提交至 main 的 PR

现在为了启动我们的工作流,我们需要向 main 分支创建我们的第一个 Pull request。在这一点上这是不可能的,因为我们的 mainplan 分支是同步的。为了使其起作用,我们需要对 plan 分支进行一次虚假 (dummy) 的 commit。

为了简单起见,我们只需向我们的一个基础设施文件添加注释,commit 更改,然后向 main 提交 PR。

你可以将注释添加到你选择的任何文件中。我将添加到 backend.tf 文件中。我将把以下行添加到文件开头

# Configure the remote gcs backend

现在保存你修改后的文件,commit 它将其推送到 Github。 这应该会在 Github 上启动一个新的 validate 工作流运行;

3rd workflow run

而且它应该会成功。

Success

现在我们可以继续并创建我们向 main 的第一个 Pull request 了。如果一切正常,你应该得到与下面我得到的相似的结果。

Plan workflow starts to run
Plan workflow succeeds

现在在你的 Pull Request 中每个地方都应该变绿了 🎉 🎉

7.3 通过 Bot 评论检查计划输出

既然你的工作流已成功运行,你应该能够看到新的 Bot 评论,其中包含了关于你提议的基础设施更改的非常重要的详细信息。

Github Bot comments Terraform Plan Output

继续展开评论的不同部分以获得更多细节。例如,单击 Show Plan 按钮将向你展示 terraform plan 命令的输出结果。

如你所见,由于我们的配置中仅定义了两个资源,我们看到一个将要创建两个资源的计划。

Plan: 2 to Add

7.4 审查、迭代并批准(或拒绝)该计划

在这一阶段,利用 Github 原生的分支规则,可以要求最少数量的审查者审查新的基础设施更改并签字批准。团队可以更改任何需要的东西,对 plan 分支的新 commit 会堆积在此 pull request 下,每次都会触发 plan workflow 的重新运行,以确保团队在下次应用时看到最新做出的更改。

现在因为我们想看到这个工作流顺利完成,并确保我们的基础设施真正被创建出来,我们将假设团队非常喜欢我们的新基础设施,并且负责人已经签字。因此,我们将继续并合并 (Merge) 这个 Pull Request #1,让我们的基础设施被部署。

7.5 然后... 就到圣诞节啦

Confirm Merge Pull Request

我使用了 rebasing 仅仅是因为我喜欢整洁的 commit 历史记录。

Pull Request #1 Merged

这应该会触发我们在 main 分支中的 deploy 工作流,正如预期的那样,它应该会完美无瑕地工作,如下所示;

Workflow Deployed Successfully
All Deploy Steps Successful

我们做到了!!! 🎉 🎉

为了确认我们的基础设施在现实世界中处于上线状态,我们可以访问其 web 地址。我的地址是 server.obi.ninja,它运行得非常完美。

All Deploy Steps Successful

我使用了 https:// 因为我配置了来自 Cloudflare 的免费 SSL 服务。我是在 Terraform 之外完成的这一配置,当时正在设置 Cloudflare 区域本身,只是为了保持该项目和演练的简单性。

8. 附加说明

8.1 Github 分支保护规则

为了使该项目坚不可摧,从而能够为你的组织可靠地工作,你需要利用 Github 分支保护规则。借助这些规则,你将能够强制执行重要操作,例如;

  • 确保不能删除 mainplan 分支
  • 仅在 validateplan 工作流都成功之后,才允许合并到 main
  • 指定允许对任何新更改进行签字批准的最小人数
  • 等等。

8.2 附加工具

你可能希望用额外的工具来扩展该项目的不同部分以增强安全性。工具例如:

  • 用于机密管理的 Hashicorp Vault
  • 用于附加静态和代码质量分析的 Sonarqube
  • 等等。

8.3 销毁基础设施

这是一个团队工作流,而且它是公司基础设施的骨干,因此,对资源的任何销毁都必须通过删除或注释所述资源的 Terraform 代码来执行,并让负责该项目的团队对这种变更进行审查。只要妥善管理基础设施机密,就没有人能够只靠运行 terraform destroy 就去销毁资源。

8.4 结论

在构思、执行和记录此项目的过程中,我获得了很多乐趣。我希望你能发现它的用处。如果有什么想法、任何错误或改进空间,请随时在下面的评论区告诉我。