通过使用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代码专用的私有仓库中创建拉取请求。

image

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

image

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

image
image

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

image

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进行管理。

有多少开发者在使用它?

wantedly_wantedly-terraform.png

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

Add_IAM_user.png

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

README_md.png

使用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上做展示

スクリーンショット 2015-12-18 22.35.14.png
广告
将在 10 秒后关闭
bannerAds