在使用CircleCI进行Terraform时的最佳实践

拿到了,开始吧。

我认为目前大多数情况下,执行Terraform是手动进行的,但是这样会遇到很多问题。

    • コードをマージしたけどTerraformの実行を忘れる

 

    • 毎回手動でやるのがめんどくさい

 

    それぞれの環境にTerraoformの実行環境を作る必要がある

为了解决与这种手动操作相关的问题,我们可以尝试在CircleCI中进行自动化!但是实际尝试时可能会遇到各种问题。本文将解释如何使用CircleCI来自动化Terraform的最佳实践。

对Terraform任务的通用化进行改进

通常情况下,Terraform以目录为单位管理基础设施,因此需要在每个目录中运行Terraform命令。虽然也可以使用for循环逐个进入所有目录并执行操作,但为了实现声明性编写,我们将使用CircleCI的参数化作业进行通用化。

Terraform Job的定义

executors:
  terraform:
    docker:
      - image: my-org/my-terraform
    working_directory: ~/src
...
  terraform:
    resource_class: medium
    parameters:
      tf_path:
        type: string
    executor: terraform
    working_directory: ~/src
    shell: /bin/bash -eo pipefail -o nounset
    steps:
      - checkout
      - run:
          name: Running Terraform
          environment:
            TFNOTIFY_LOG: /tmp/tfnotify.log
            PARTITION: << parameters.tf_path >>
          command: ./scripts/run-terraform.sh

使用CircleCI 2.1的参数化作业来实现共享。仅解释主要部分。

    executor: terraform

我们使用executors在定义包含Terraform二进制文件和其他工具的镜像,并使用它。

    parameters:
      tf_path:
        type: string

在tf_path中指定了执行Terraform的目录路径。在作业中,将会进入该目录并执行命令。

command: ./scripts/run-terraform.sh

在这个脚本中,执行 terraform plan 和 terraform apply。虽然也可以直接写在 .circleci/config.yml 文件中,但出于可移植性和易于测试的考虑,我们将它们写在了一个 shell 脚本中。

使用Terraform工作

      - terraform:
          name: "elasticache"
          tf_path: "terraform/elasticache"
          context: terraform
          requires:
            - test

我们将使用所定义的 Terraform Job。通过指定 tf_path 来执行目录,可以声明以声明方式编写在哪个目录中执行。

重要的是指定了名称。如果没有这个,当我们在CircleCI的工作流程界面上查看时,所有的作业都将显示为terraform,无法区分。

执行Terraform的通知

为了将手动操作自动化,一般情况下会先执行terraform plan命令并确认日志后再执行apply。为了在CI/CD中实现自动化,可以使用tfnotify工具。tfnotify有两种使用方式。

在讨论中的分支时

由于希望在执行于分支主题上时,将执行结果包含在PR审核中,所以我们将使用tfnotify在PR中以评论的形式插入执行结果。以下是tfnotify在通知PR时的设置示例。

---
ci: circleci
notifier:
  github:
    token: $GITHUB_TOKEN
    repository:
      owner: <my-org>
      name: <my-repo>
terraform:
  fmt:
    template: |
      {{ .Title }}

      {{ .Message }}

      {{ .Result }}

      {{ .Body }}
  plan:
    template: |
      {{ .Title }} <sup>[CI link]( {{ .Link }} )</sup>
      {{ .Message }}
      {{if .Result}}
      <pre><code> {{ .Result }}
      </pre></code>
      {{end}}
      <details><summary>Details (Click me)</summary>

      <pre><code> {{ .Body }}
      </pre></code></details>

请根据需要适当更改 $GITHUB_TOKEN、、 。
请根据需要适当更改 $GITHUB_TOKEN、、。

在主分支时

后面会提到,由于在master分支上执行apply操作,所以我希望在执行后能立即通过Slack通知确认是否有问题。

---
ci: circleci
notifier:
  slack:
    token: $SLACK_TOKEN
    channel: <my-channel>
    bot: <bot-name>
terraform:
  apply:
    template: |
      {{ .Message }}
      {{if .Result}}
      ``` {{ .Result }} ```
      {{end}}

请将 $SLACK_TOKEN、、 替换为相应内容。

执行tfnotify

既然已经设置了GitHub和Slack的通知,现在我们来看看如何实际进行通知。

GitHub的中文释义是“基于Git的版本控制平台”。

tfnotify --config ~/src/.tfnotify/github.yml plan --title "$(tfnotify_title)" <"$TFNOTIFY_LOG" >/dev/null || true

Slack 绗?劆

tfnotify --config ~/src/.tfnotify/slack.yml apply --title "Changes were successfully applied to $PARTITION" --message "See details at $CIRCLE_BUILD_URL" <"$TFNOTIFY_LOG" >/dev/null || true

$TFNOTIFY 会将 Terraform 执行时的日志流动。

terraform apply -auto-approve -no-color $PLAN_FILE | tee "$TFNOTIFY_LOG"

现在可以通过这个方法来进行Terraform的执行结果PR审核和应用后在Slack确认。

Screen Shot 2019-12-21 at 9.29.57.png

判断申请的地点。

在Terraform中,通过执行terraform apply命令,将基础设施的状态应用到真实的基础设施中。当手动执行时,这是一个非常简单的操作,但是当在CI/CD中尝试自动化时,您会意识到有很多问题需要考虑。

最大的问题在于何时执行应用程序。Terraform按照每个模块的目录进行管理,当在该目录的直接下方进行应用程序时,更改将会生效。

因为人们知道自己所更改的部分,所以只需要进入相应的目录并执行 terraform apply,非常简单。

但是,当在CircleCI进行自动化时,必须确定在哪个目录下执行terraform apply。这是因为CircleCI是根据提交的推送来触发执行的,所以必须判断哪个目录发生了变化,以确保在正确的位置执行apply。

尽管前言有点冗长,但这是关于应用场所判断的问题。

首先,我们将解释一种直观但错误的方法,然后介绍正确的方法。

直觉的方法,但是是错误的方式。

直觉上,最好的方法是通过特定确定每个提交的变更部分,只对发生更改的部分进行 terraform apply。另外,apply 也是Terraform实际应用于基础设施更改的部分,所以只想在发生更改的部分执行它。因此,只对发生更改的地方进行 apply 的方法似乎是正确的。

为了实现这一点,我们在每个目录中实现了一个功能:如果有变更,就执行 terraform apply,否则跳过。

另外,apply 命令只在主分支上执行一次,因此我只想在主分支上执行。

总之,你想做的就是这样的事情。

    • コミット内容から apply するディレクトリを特定

 

    • (トピックブランチ): terraform plan を実行

 

    (masterブランチ): terraform apply を実行

我使用了dnephin/multirepo,当没有变更时,会中止作业,具体的实现方法我不在这里写。

我用这种方法试运行了一段时间后,遇到了以下问题。

問題1: 在主分支上很難定位變更的位置。

在话题分支中,通过 git diff master my-branch 可以方便地看到更改的部分。但是,在主分支上无法使用这种方法,因为无法与其他分支进行比较(自己就是主分支)。一种方法是从合并的Pull Request的合并提交中推测出更改的部分。但是,如果在GitHub上使用Squash Merge,这种方法将会失效。

作为这个问题的解决方法,我们在CircleCI上的主分支工作流的最后保存了HEAD SHA到s3,并在下一个工作流中下载以便与当前的主分支进行比较。

尝试运用这种方法一段时间后,很快就会意识到问题。虽然我们希望SHA上传在所有任务成功完成后执行,但是如果中间的任务因某种原因失败,上传将不会保存。如果在此期间另一个人推送到主分支并运行另一个任务,将导致不一致情况发生。

最终,我们发现整个工作流都依赖于S3,这种方法是不可取的。

追加注释

这个问题被称为单一代码库问题,如果CircleCI能够努力解决,就可以解决这个问题。但是目前CircleCI对单一代码库的支持还比较薄弱,所以无法很好地处理它。

问题2:检测依赖的变更部分。

在Terraform中,通常使用模块来管理共享代码,并将实际的基础设施实施部分写入实现部分。当修改模块时,必须在实际使用该实现的目录中进行应用才能使其生效。但是,如果只应用已更改的部分,模块发生变化时,实现部分中没有代码更改,因此不会应用。

如果使用Terraform的数据等来引用其他基础设施的状态,也会出现问题。当被引用的基础设施发生变化时,引用它的一方也必须进行apply操作,但由于引用方没有进行代码更改,所以不会被apply。

总结一下,我们发现仅使用apply对于Terraform中的变更部分无法很好地处理依赖关系。

正确的方法 de

通过前面的例子,我们了解到选择特定变更然后应用的方法虽然直观,但实际上会使工作流程变得复杂,并且被认为是不良实践。

最终抵达的方法是始终采用apply的方式。在这种方法中,无论在主分支上Terraform的所有目录中是否有更改,都会执行apply操作。通过实施与操作,我们目前认为这种方法是最佳的,现在我将介绍一些引入这种方法后的好处。

消除外部依赖关系 (s3)

这种方法不需要检测修改部分,所以在主分支上无法进行差异比较的问题也消失了。由此,保存SHA到s3的需要也不再存在。

更新依赖模块

由于不断应用,即使只有模块发生了更改而没有实施变更,也可以确保在实施方面执行应用以进行适应。

消除配置漂移(意外的配置差异)

经常会有人意外地手动更改正在使用Terraform运维的基础架构。由于始终采用应用的方法,当CircleCI的主工作流程运行时,会自动应用Terraform,以将手动更改还原为Terraform应有的状态。这样一来,Terraform和实际的基础架构就可以始终保持同步。

经常申请不是很可怕吗?

在使用之前,我们在团队中讨论过,即使没有进行任何更改,执行apply操作是否会让人感到害怕?但实际尝试后发现完全没问题。这是因为之前提到的tfnotify通过通知将变更部分可视化,这样即使应用了意外的更改,也可以立即察觉到,我认为这一点非常重要。

其他注意事项

以下是在CircleCI中执行Terraform时的重点,还有一些有用的技巧:

在使用CircleCI执行Terraform时,请注意以下几点,并额外介绍一些实用的提示。

防止Workflow的Rerun.

在CircleCI中,您可以重新执行以前的作业。如果执行以前的Terraform作业会发生什么呢?由于作业是在先前的提交中执行的,因此有可能将过时的更改应用于最新的基础架构,这是危险的。为了防止这种情况发生,我们会进行检查。

function check_branch_update_to_date() {
  # check remote branch exist or not
  if git ls-remote --exit-code --heads origin "$CIRCLE_BRANCH"; then
    # remote branch is existed
    LATEST_SHA1_IN_CURRENT_REMOTE_BRANCH=$(git rev-parse "origin/$CIRCLE_BRANCH")
    if [[ "${CIRCLE_SHA1}" == "${LATEST_SHA1_IN_CURRENT_REMOTE_BRANCH}" ]]; then
      echo "Found current branch $CIRCLE_BRANCH is update to date, ready to run terraform."
    else
      echo "Skip to run terraform on old commit ${CIRCLE_SHA1} in current branch $CIRCLE_BRANCH."
      exit 1
    fi
  else
    # remote branch is not existed, like you have deleted the remote branch after you merge to master
    echo "Remote branch $CIRCLE_BRANCH is not existed, skip to run terraform."
    exit 1
  fi
}

这个Shell脚本旨在检查执行Terraform的分支的SHA是否是最新的,如果不是最新的则不执行。虽然这个检查不是完美的,但它可以防止错误地重新执行旧的master工作流并破坏基础架构。

执行Terraform之前,先在master上进行rebase。

当从PR分支执行Terraform时,如果其他人将更改推送到主分支并更新了基础架构,那么在PR分支中执行时,可能会再次显示已经在主分支上应用的更改作为diff。

为了防止这种情况发生,我们首先要将最新的master分支rebase到本地,然后再执行Terraform操作。

function merge_master() {
  cat <<EOF
Merging master to the feature branch so that Terraform doesn't show rollbacks of
changes that have been applied from master since the PR was branched.
EOF
  git config --global user.name "$(git show -s --format='%an' "${CIRCLE_SHA1}")"
  git config --global user.email "$(git show -s --format='%ae' "${CIRCLE_SHA1}")"
  git checkout --detach
  git pull origin master --no-edit
  git --no-pager diff --check
}
function restore_branch() {
  cat <<EOF
Restoring the feature branch, without the merge from master, so that tfnotify
can identify the right commit.
EOF
  git reset --hard
  git checkout "$CIRCLE_BRANCH"
}

在执行Terraform时,将其与merge和restore组合使用,您可以始终仅使用最新的master与Terraform执行差异,从而获得正确的差异。

新增内容

这个功能在Travis CI上是一项标准功能,但在CircleCI上没有,所以我们自己实现了它。

广告
将在 10 秒后关闭
bannerAds