通过使用Terraform和CI来实现基础设施的代码化和自动化构建
在2015年的Wantedly Advent Calendar中,今天是第18天。
我是基础设施团队的实习生@dtan4。
在 Wantedly,我们全面采用 Terraform 来进行基础设施的代码化(作为代码的基础设施)。添加或修改基础设施资源是通过编写代码和在 CI 上自动应用来完成的。
在本文中,谈到了在今年5月至少半年的时间里运行 Terraform 的经验。
-
- なぜ Terraform でインフラをコード化しようとしたのか
-
- どのように Terraform を運用しているのか
-
- Terraform 運用にあたって注意すべき点
- 既存リソースから Terraform コードを生成する Terraforming について
我想介绍一下这个事情。
Terraform 是什么意思?
Terraform是HashiCorp开发的一种工具,用于根据代码创建基础设施资源,以及管理基础设施。它广泛兼容多个云提供商,如AWS、GCP、Azure和DigitalOcean,还支持包括DNSimple、Mailgun和Rundeck在内的许多SaaS。
您可以使用符合 JSON 格式的 HashiCorp Configuration Language (HCL) 来编写代码。举个例子,AWS 的 ELB 和 EC2 实例可以这样描述。
# from https://www.terraform.io/
resource "aws_elb" "frontend" {
name = "frontend-load-balancer"
listener {
instance_port = 8000
instance_protocol = "http"
lb_port = 80
lb_protocol = "http"
}
instances = ["${aws_instance.app.*.id}"]
}
resource "aws_instance" "app" {
count = 5
ami = "ami-043a5034"
instance_type = "m1.small"
}
如下所示,当对此代码执行terraform apply命令时,将在AWS上创建一个ELB和5个EC2实例。如果要更改内容,只需修改代码并再次执行terraform apply,即可将更改应用到实际环境中。如果删除代码并执行terraform apply,资源将被删除。
这段代码的terraform apply命令可以生成5个AWS ELB和EC2实例。如果要修改内容,只需更改代码并再次执行terraform apply命令即可将更改应用到实际的AWS环境中。如果要删除代码,请执行terraform apply命令即可删除资源。
虽然Terraform可以通过代码创建资源,但目前还没有从资源生成代码的功能(虽然功能请求已经存在了一年多)。为了解决这个问题,我自己开发了一个叫Terraforming的工具。接下来会详细讲解Terraforming。
为什么选择对基础设施进行编码?
在使用 Terraform 之前,存在以下问题:
-
- 開発チームからインフラチームへの依頼によるスタイル
-
- 新しいリソースを作るのに Management Console 上でポチポチ操作してくのが面倒であり、工数もかかる
-
- しかもポチポチ作業の履歴を残すのが困難
-
- Management Console に行かないとリソースの一覧が把握できない
- 同じ構成でもリソースの複製が面倒
以下是它们的中文同义词/释义:
这些
这些东西
这些东西们
-
- 開発者が欲しいリソースをコードという形で記述して、インフラチームに Pull Request を送る
-
- インフラチームは送られてきたコードを GitHub 上でレビューして、”Merge Pull Request” ボタンを押すだけでリソースが作られる
-
- リソースの追加や変更履歴は、git の diff という形で残る
- リソース複製もコードのコピペで済む
我考虑以这种方式来解决。
为什么选择使用 Terraform?
Wantedly使用了许多AWS服务,但能够统一管理这些服务是我们的决定因素。关于DNS,我们使用的是DNSimple而不是Route53,但能够包括这一点在内进行管理对我们来说非常重要。
我们也考虑了Cookpad所设计的codenize.tools,但由于工具按服务分开,并且兼容的服务有限,所以我们决定不采用。
使用GitHub、Terraform和CI(wercker)实现的Terraform流程。
开发者(或者说是希望添加或修改资源的人)编写Terraform代码,并在Terraform代码专用的私有仓库中创建拉取请求。

当代码被推送到代码库中,wercker 将自动执行 terraform plan,并检查.tf 文件的内容。

当Pull Request的内容被基础设施团队审查并标记为LGTM后,会进行合并操作。合并到主分支后,wercker会执行terraform apply命令,将更改应用到实际环境中。


由于所有的资源都可以在GitHub上作为一种代码来查看,这使得一切变得更加便利。

wercker.yml 是一个YAML格式的文件。
CI 执行的内容是 wercker.yml ,其内容如下。
box: wercker-labs/docker
no-response-timeout: 30
build:
steps:
- create-file:
name: Create .env
filename: $WERCKER_SOURCE_DIR/.env
hide-from-log: true
content: |
AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY
AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION
DNSIMPLE_EMAIL=$DNSIMPLE_EMAIL
DNSIMPLE_TOKEN=$DNSIMPLE_TOKEN
- create-file:
name: Create terraform.tfvars
filename: $WERCKER_SOURCE_DIR/terraform.tfvars
hide-from-log: true
content: |
"hoge_password"="$HOGE_PASSWORD"
...
- script:
name: Pull Terraform image
code: |
docker pull quay.io/wantedly/terraform:latest
- script:
name: terraform remote config
code: |
script/ci-terraform remote-config
- script:
name: terraform plan
code: |
script/ci-terraform plan
after-steps:
- wantedly/pretty-slack-notify:
webhook_url: $SLACK_WEBHOOK_URL
channel: $SLACK_CHANNEL
deploy:
steps:
- script:
name: Pull Terraform image
code: |
docker pull quay.io/wantedly/terraform:latest
- script:
name: terraform apply
code: |
script/ci-terraform apply
- script:
name: terraform remote push
code: |
script/ci-terraform remote-push
after-steps:
- wantedly/pretty-slack-notify:
webhook_url: $SLACK_WEBHOOK_URL
channel: $SLACK_CHANNEL
在这里,script/ci-terraform 是一个如下所示的shell脚本。它将Terraform作为Docker容器运行。
#!/bin/bash
#
# Usage: ci-terraform apply|plan|remote-config|remote-pull|remote-push
# Description: run Terraform on CI environment
#
if [ $# != 1 ]; then
exit 1
fi
case $1 in
"apply" )
docker run --rm --name terraform --env-file=$WERCKER_SOURCE_DIR/.env -v $PWD:/terraform quay.io/wantedly/terraform:latest terraform apply terraform | script/masking-passwords && exit $PIPESTATUS
;;
"plan" )
docker run --rm --name terraform --env-file=$WERCKER_SOURCE_DIR/.env -v $PWD:/terraform quay.io/wantedly/terraform:latest terraform plan terraform | script/masking-passwords && exit $PIPESTATUS
;;
"remote-config" )
docker run --rm --name terraform --env-file=$WERCKER_SOURCE_DIR/.env -v $PWD:/terraform quay.io/wantedly/terraform:latest terraform remote config -backend=S3 -backend-config="bucket=$TFSTATE_BUCKET" -backend-config="key=$TFSTATE_KEY" -backend-config="encrypt=1"
;;
"remote-pull" )
docker run --rm --name terraform --env-file=$WERCKER_SOURCE_DIR/.env -v $PWD:/terraform quay.io/wantedly/terraform:latest terraform remote pull
;;
"remote-push" )
docker run --rm --name terraform --env-file=$WERCKER_SOURCE_DIR/.env -v $PWD:/terraform quay.io/wantedly/terraform:latest terraform remote push
;;
* )
exit 1
;;
esac
同时,script/masking-passwords 是一种如下所示的shell脚本。
#!/bin/bash
#
# Usage: terraform plan | script/masking-passwords
# Description: masking passwords from `terraform plan|apply` output
# Example:
# password: "before" => "after"
# is replaced to
# password: "********" => "********" [hidden]
#
if [ $(uname) == "Darwin" ]; then
# BSD sed
sed -E 's/(password:\s+|auth:\s+)".*?" => ".*?"/\1"********" => "********" [hidden]/g'
else
# GNU sed
sed -u -r -e 's/(password:\s+|auth:\s+)".*?" => ".*?"/\1"********" => "********" [hidden]/g'
fi
这个Shell脚本包含了计划和执行的输出。
# password: "before" => "after"
在中国,只需要一个选项,你需要重新表达以下句子:
を
# password: "********" => "********" [hidden]
如何对密码进行屏蔽的。在RDS的.tf文件中,需要通过terraform.tfvars文件传递密码,但这样会将密码以明文形式显示在执行时的差异中。为了保险起见,我通过这个脚本对其进行了处理。
terraform.tfstate 的分享
在Terraform中有一个名为terraform.tfstate的文件,它包含Terraform自身管理的基础设施信息。通过将此文件放置在S3上,可以在本地机器和CI之间实现共享。详细信息请参阅下面的文章。
在Amazon S3上管理/共享Terraform状态文件terraform.tfstate – Qiita
Terraform 执行环境
当在本地机器上执行 Terraform 时,我们使用 CoreOS VM 上的 quay.io/wantedly/terraform Docker 镜像来作为 Docker 容器运行。在 CI 中也使用同样的镜像。我们希望统一各开发者之间的本地环境和 CI,所以采取了这种方式。
只要 Terraform 自身是一个独立的二进制文件,所以我想或许不需要把它强行放在Docker容器中,尤其是在CI方面。
Terraform 在 Wantedly 的实际运用
你编写了多少代码?
就数量来说,我使用Terraform 管理了28种资源共390个(AWS: 222个,DNSimple: 168个)。
所管理的资源种类是:
管理的资源种类有:
aws_customer_gateway
aws_db_instance
aws_db_parameter_group
aws_db_security_group
aws_db_subnet_group
aws_elasticache_cluster
aws_elasticache_subnet_group
aws_elb
aws_iam_group
aws_iam_group_membership
aws_iam_group_policy
aws_iam_role
aws_iam_role_policy
aws_iam_user
aws_iam_user_policy
aws_instance
aws_internet_gateway
aws_network_acl
aws_route_table
aws_route_table_association
aws_s3_bucket
aws_security_group
aws_subnet
aws_vpc
aws_vpn_connection
aws_vpn_connection_route
aws_vpn_gateway
dnsimple_record
是的。
我们没有使用Terraform来管理EC2实例。大部分实例的扩缩容都是通过我们自己的工具进行的。如果有一些需要持续运行的实例,建议使用Terraform进行管理。
有多少开发者在使用它?

所以,共有18名开发人员(其中包括3名基础架构团队成员)正在编写Terraform的代码。有时候来自新服务(Sync)团队的Pull Request中会包含添加S3 bucket或DNSimple DNS记录,也有可能来自新入职的工程师的Pull Request,用于添加IAM用户。后者被整合到了开发环境搭建的一部分,所以最近入职的工程师们都在使用Terraform的代码。

顺便一提,对于不熟悉 Terraform 的非基础设施团队成员来说,改动此代码库将变得更加困难。为了尽可能降低这个难度,我们准备了这个代码库的 Markdown 形式文件(在 GitHub 上可以进行预览和整理)。

使用Terraform时需要注意的事项
在执行terraform apply时遇到错误
terraform plan 只是对 .tf 文件进行语法检查而已,所以即使内容在 AWS API 方面是无效的,也会通过。
例如,即使在 aws_elb 的名称中包含奇怪的字符,terraform plan 也会通过。
在这个问题上,需要仔细阅读并理解 AWS 的文档,然后才能进行写作。最好的方法是使用 API 的 dry-run 功能,但从外观上看,似乎只有针对 EC2 系列的 API 提供了 dry-run 选项。
此外,如果您正在建立自動應用於 CI 環境的環境,則應該準備一個本地環境,以便在恢復時也可以執行 terraform apply。
對於我們來說,如果應用失敗,我們會創建一個修正了.tf文件的新拉取請求,然後重新合併到主分支=>進行部署。
在ELB下的实例被意外替换 => 通过生命周期解决?!
在 Wantedly,我们使用自有工具来进行 ELB 下实例的扩展和替换。因此,如果在 .tf 文件中硬编码实例 ID,可能会导致与实际环境产生差异。
在今年十月发布的Terraform v0.6.4中,引入了一个名为ignore_changes的新参数。ignore_changes允许我们在进行计划和应用时忽略指定属性的变化。
如果要忽略 aws_elb 的实例变更,可以选择忽略。
resource "aws_elb" "foo" {
lifecycle {
ignore_changes = ["instances"]
}
}
只需写下这个!正是为了解决这个问题而设计的功能。
我在删除 IAM 用户时失败了
相关用户需要在退职或其他情况下删除 IAM 用户时,必须解除其绑定关系。
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
Management Console パスワード
IAM Policy
IAM Group 所属情報
需要提前删除。如果从管理控制台进行删除,则会同时删除,但如果通过API进行删除,则需要逐个进行删除。详细信息请参考此处链接:
AWS身份和访问管理 – AWS IAM
顺便说一下,如果要使用Terraform删除IAM用户,当然可以通过API进行操作。但是,这种情况下会直接调用IAM用户删除API,而忽略与其相关联的资源,因此很有可能在terraform apply过程中出现以下错误。
Error applying plan:
1 error(s) occurred:
* Error deleting IAM User hoge: DeleteConflict: Cannot delete entity, must delete access keys first.
为了应对这个问题,在管理控制台上删除了IAM用户,然后执行terraform apply。尽管这种方法对于推进自动化来说并不好,但目前删除发生的频率很低,所以就这样处理了。
地球成型:从现有的基础设施资源生成Terraform代码
突如其来,我要介绍一下我的作品。
Terraforming 是一个命令行工具,用于从现有的AWS资源生成Terraform的代码文件.tf和状态管理文件terraform.tfstate。Wantedly的大部分Terraform代码是通过Terraforming生成的。
关于Terraforming 的详细使用方法,请参考我的个人博客和在 Qiita 上的其他文章。
-
- Terraforming で既存のインフラを Terraform 管理下におく – Qiita
- Terraforming: 既存のインフラリソースを Terraform コードに落としこむ – 端子録
我觉得在企业引入基础架构即代码时,很多人希望将现有资源编码化。虽然在启动新业务时也可能会这样……正如 Wantedly 做的那样,刚开始时我以为只需要写一些对应的 .tf 文件,但后来才明白这远远不够,真是让人噢噢噢慌张。
我开始制作Terraforming是因为我觉得这个功能要求很多,但手工操作很困难。
作为一种OSS的基础设施转型
感謝您的使用,不管是國內還是國外的各種使用者和公司都在使用,而且我們至今仍持續收到 Pull Request。
說實話,對於平常不使用的資源,我並沒有太大的動力去添加,但是在其他人使用後,說他們希望這個資源,並提交了 Pull Request 的情況越來越多。真的非常感激,非常謝謝您。
未来我们将持续不断地进行开发。首先要实现AutoScaling功能,还想把被拆分成独立的gem的terraforming-dnsimple作为插件加载进来。古桥先生在RubyKaigi的演讲对我们来说是一个很好的参考。
最后
所以,這就是在 Wantedly 上介紹 Terraform 應用案例的原因。希望這能成為考慮將基礎架構代碼化和導入 Terraform 的人的參考。
這種構建方法只是一個例子,可能還有更好的最佳實踐方式。希望能參考其他案例並進一步改進。
另外,由于在今年8月份举办的 HashiCorp Product(Tools) Meetup 上也介绍了类似的内容。虽然自那时起有些许更新,但请一并查看。
在Wantedly上进行土地开发 // 在Speaker Deck上做展示
