在使用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确认。
判断申请的地点。
在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上没有,所以我们自己实现了它。