Terraform職人入門:将在日常运营中学到的经验整理得平淡无奇
首先
本文是CrowdWorks Advent Calendar 2017的第8天的文章。
我是Terraform工匠@minamijoyo。你采用了基础设施即代码吗?
我开始使用Terraform进行基础设施代码管理已有2年多时间,每天在生产环境中运营时都有各种各样的经验,所以我想共享一些场景中的运维技巧,这些技巧在Terraform入门文章中很少被提及。
如果有正在开始使用或者已经在使用Terraform的人在遇到类似这样的情况时可以参考,那就太好了。
我一开始写的时候变成了超长篇的文章。以下是概要的内容。
-
- 公式ドキュメントを読もう
-
- tfファイルを書く技術
インデントを揃える
組み込み関数に親しむ
lifecycleブロックを使う
リソースの差分を無視する
リソース再生成のときに新しいのを作ってから古いのを削除する
リソースのうっかり削除の保護
テンプレートを使う
外部ファイルを文字列として読み込む
テンプレートに変数を埋め込む
モジュールでコードを共通化する
モジュールの作り方
production/stagingなどの環境差分を管理する
モジュール間で値を参照する
Stateを跨いで値を参照する
条件分岐したい
条件によって変数の値を変える
条件によってリソースを作成したりしなかったり
Terraformのバージョンを管理する技術
Terraformをバージョンアップする
Terraformのバージョンを切り替える
Terraformのバージョンを固定する
Terraformプロバイダをバージョンアップする
Terraformプロバイダのバージョンを固定する
tfstateファイルを書く技術
ローカルでミニマムのStateを見てみる
リモートのStateを見る
強制的にリソースを再生成する
Terraform管理外の既存のリソースをTerraform管理下に入れる
tfファイルをリファクタリングする
Terraformをデバッグする技術
デバッグログを出力する
Terraformのソースコードをコンパイルする
デバッグログにソースコードのファイル名と行番号を付ける
Terraformをデバッガでステップ実行する
在撰写本文时,Terraform的版本为v0.11.1。由于平时主要使用AWS,所以在解释的例子中会提及AWS,但所讨论的主题并不仅限于AWS,几乎都是通用的话题,因此,请根据您使用的云服务提供商进行适当的替换。
我要阅读公式文件。
首先,虽然听起来可能很平凡,但如果有不清楚的事情,我们应该先阅读官方文件。
Terraform是一个目前仍在积极开发中的产品。由于网络上的信息可能过时(尽管这篇文章也会变得过时),如果感到困惑,查阅官方文档是确保的最佳选择。
https://www.terraform.io/docs/index.html
如果能理解Terraform所使用的术语,阅读官方文档等将变得更加容易。以下是对一些至少理解的术语进行大致解释,大致理解如下即可。
*.tf
ファイルにDSLで書くTerraformのコードのこと。HCLHashiCorp Configuration Languageの略。*.tf
ファイルで使われているDSLのこと。ResourceTerraformで管理する対象の基本単位。Data SourceTerraform管理外だけど、Terraform内で参照したい参照専用の外部データ。ProviderResourceやData Sourceなどを作成/更新/削除するプラグイン。aws/google/azurermなど。Provisionerリソースの作成/削除時に実行するスクリプトなどのプラグイン。local-exec/remote-exec/chefなどStateTerraformが認識しているリソースの状態。 *.tfstate
ファイルのこと。BackendStateの保存先。local/s3/gcsなど。ModuleResourceやData Sourceなどを再利用可能なようにまとめたConfigurationの単位。顺便一提,如果在尝试运行 terraform apply 时出现错误,或者在阅读官方文档中关于目标资源的说明时仍然不太明白参数的含义等情况下,只能去查找相关的问题(Issue),最终可能需要阅读源代码。
从0.10版本开始,Terraform按照提供程序(Provider)进行了仓库拆分,因此在查找Terraform的问题或代码时,需要先了解仓库的结构。
Terraform的主体位于以下存储库内:
https://github.com/hashicorp/terraform
供应商位于名为terraform-providers的组织下,网址为https://github.com/terraform-providers。
比如在AWS中,这个仓库可以在以下链接找到:
https://github.com/terraform-providers/terraform-provider-aws
顺便提一下,由于Provisioner数量较少,所以它们位于Terraform主存储库的builtin目录下方。
写tf文件的技术
将缩进对齐
用代码管理基础设施的优势之一是配置更改可以进行代码审查,但是指出tf文件缩进不一致等问题在进行代码审查时是没有意义的。
由于Terraform提供了一个名为”terraform fmt”的命令来统一缩进等样式,所以我们应该使用它。Terraform本身是用Go语言开发的,而Go语言有一个名为”go fmt”的命令来统一代码风格,所以我认为Terraform受到了它的影响。无论如何,很感激官方提供了明确的风格规范,这样可以减少无谓的争论。
terraform fmt会自动调整当前目录中所有文件的格式。
例如,对于微妙地位置不匹配的文件,可以这样描述:
provider "aws" {
version = "1.4.0"
region = "ap-northeast-1"
}
执行 terraform fmt
$ terraform fmt
以下的等号位置将被修正。
provider "aws" {
version = "1.4.0"
region = "ap-northeast-1"
}
只要每次手动执行容易遗忘,因此建议使用编辑器插件等,在文件保存时自动执行 “terraform fmt” 命令,这样就可以无意识地始终保持格式的一致性。同时,自动格式化的副作用是,如果存在括号等闭合忘记的语法错误,格式化会失败,因此可以很方便地及早发现语法错误等初级错误。
值得去查找一下你最喜欢的编辑器是否有适用于Terraform的插件。
我通常使用NeoVim,所以我会简单介绍一下(Neo)Vim,但是在Emacs/Atom/VSCode/IntelliJ等编辑器中,你也可以通过搜索获得各种信息。请在你喜欢的编辑器中尝试搜索一下是否有适用于Terraform的插件。
如果你使用(Neo)Vim,安装hashivim/vim-terraform插件后,你可以使用语法高亮和自动格式化功能,每当保存文件时。https://github.com/hashivim/vim-terraform
NeoBundle 'hashivim/vim-terraform'
let g:terraform_fmt_on_save = 1
请在README中设置g:terraform_fmt_on_save = 1来启用文件保存时的自动格式化。
再加上 juliosueiras/vim-terraform-completion,您还可以实现资源类型和参数名称等的全能补全功能。
https://github.com/juliosueiras/vim-terraform-completion
NeoBundle 'juliosueiras/vim-terraform-completion'
顺便提一下,vim-terraform-completion 内部依赖于 Ruby,所以请确认在 Vim 中 +ruby 选项是否启用。对于 NeoVim,需要运行 gem install neovim。
熟悉嵌入式函数
Terraform中有一些内置函数,如果了解它们,对于编写tf文件会很方便。因此,当想要操作字符串等进行处理时,可以尝试查找一下是否有适用的便利的内置函数。
https://www.terraform.io/docs/configuration/interpolation.html#built-in-functions
如果对这个函数的行为不太理解的话,可以使用terraform console启动一个交互式的REPL来测试,这样非常方便。
$ terraform console
> coalesce("","hoge")
hoge
> coalesce("fuga","hoge")
fuga
使用生命周期块。
忽略资源的差异
当使用Terraform时,有时想忽略某些资源的配置差异。例如,当尝试使用aws_autoscaling_group进行Blue/Green部署时,可能希望动态地更改所附加的load_balancers。因此,虽然资源本身是由Terraform创建的,但由于运营方面的原因,希望忽略手动进行的配置更改,并可以通过在lifecycle块中指定ignore_changes属性来忽略差异。
resource "aws_autoscaling_group" "app_1" {
name = "app-asg-1"
...
lifecycle {
ignore_changes = ["load_balancers"]
}
}
在Terraform中,`ignore_changes`的常见用途之一是在不希望将密码等明文硬编码到tf文件中时使用。
在使用Terraform创建资源时,可能需要将密码等凭据作为配置项。例如,在创建AWS的RDS实例时,需要将DB的主密码作为aws_db_instance的密码设置。
可以将这类凭据作为变量放在tf文件中,并在terraform apply时传递。但是,这种方法会导致密码以明文形式记录在State中,任何具有访问State权限的人都可以查看,因此我个人不太建议使用这种方法。
从个人角度来说,我建议将这类密码在Terraform的ignore_changes中设为不受管理。也就是说,在第一次terraform apply时,只需写入初始密码,并使用ignore_changes指定,那么以后即使更改密码,也会被忽略作为差异,可以实现密码的更改。
resource "aws_db_instance" "db_instance" {
identifier = "db"
password = "hoge"
...
lifecycle {
ignore_changes = ["password"]
}
}
在资源重新生成时,先创建新的资源,然后再删除旧的。
生命周期的其他用法示例是在重新生成资源时删除旧资源,而不是创建新资源,然后再创建新资源,最后删除旧资源。例如,如果想要更新与 aws_autoscaling_group 相关联的 aws_launch_configuration,则 aws_launch_configuration 是不可更新的资源,因此基本上必须每次都进行旧资源的删除和新资源的创建。但是这样会导致 aws_autoscaling_group 仍然引用旧资源作为其依赖项,因此无法删除旧资源。但是,由于我们只想要无中断地进行更新,因此不想删除 aws_autoscaling_group。在存在这种微妙的资源依赖关系的情况下,可以通过在 lifecycle 块中指定 create_before_destroy = true 来按照创建新资源=>删除旧资源的顺序进行执行。
resource "aws_launch_configuration" "as_conf_1" {
image_id = "ami-xxxx"
name_prefix = "app-"
lifecycle {
create_before_destroy = true
}
}
在这种方法中,需要注意一个问题是,在创建新资源之前删除旧资源,这样会临时产生两个资源。如果资源命名有唯一约束,则资源创建会失败。但如果资源是自动编号的,则没有问题。
资源意外删除的保护
生命周期中有一个名为prevent_destroy的标志,用于防止意外删除资源。然而,在修改生产基础设施时,大部分资源都是不能随意删除的,所以过度依赖该标志并不好。我们应该仔细查看计划。
使用模板。
读取外部文件作为字符串
比如说,如果资源的配置参数接收到的是JSON字符串,可以在现场使用Here文档进行编写,也可以将外部文件作为字符串读取进来。
resource "aws_iam_role" "ec2_role" {
name = "ec2-role"
assume_role_policy = "${file("./ec2_assume_role_policy.json")}"
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
特别是对于JSON这样的文件,将其保存在外部文件中后,在编辑器中可以实现语法高亮等功能,同时也可以避免一些初级错误,比如遗漏了末尾逗号导致的错误。因此,我认为将其切割到外部文件中是一个好的选择。
在模板中插入变量
在设定文件可以被读取的时候,我们会希望进行变量嵌入。
这种情况下,我们可以在想要嵌入变量的位置写上 ${变量名}。
{
"Version": "2012-10-17",
"Id": "key-policy-for-${account_name}-account",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::${account_id}:root"},
"Action": "kms:*",
"Resource": "*"
}
]
}
创建一个名为template_file的数据源,然后将变量从外部传入。
data "template_file" "kms_policy" {
template = "${file("${path.module}/kms_policy.json")}"
vars {
account_name = "dev"
account_id = "123456"
}
}
所以,我们会在实际需要字符串的地方进行渲染。
resource "aws_kms_key" "key" {
policy = "${data.template_file.kms_policy.rendered}"
}
通过模块化使代码更通用
模块的制作方法
在Terraform中,有一个非常有用的功能是在建立相似配置的服务器时,大部分工作都可以通过复制和替换来完成。然而,随着复制的增加,我们可能希望将代码进行模块化,以便重复利用。
在Terraform中,我们可以通过定义模块(module)的方式来实现可重用的代码块。我们可以创建一个适当的hoge目录,在其中定义希望共享的资源等内容。
provider "null" {}
resource "null_resource" "hoge" {}
然后,在想要使用的位置,使用模块(module)并通过source指定相对路径进行加载。
module "bar" {
source = "./hoge"
}
module "baz" {
source = "./hoge"
}
只需按照此方式创建目录并将tf文件进行分组,即可开始使用模块。
除了本地目录,还可以引用外部存储库,最近还推出了Terraform Registry这一公开共享模块的机制。
https://registry.terraform.io/
由于资源的命名规则等问题,可能很少直接使用已公开的模块。但是,看看其他人编写的Terraform代码可能会有所收获,因此建议去看一看。
管理生产/暂存等环境的差异
可以在模块中传递变量,因此在管理生产/演练等环境差异时,将通用代码模块化并通过变量来吸收差异是可行的。
在 modules/hoge/ 目录下放置希望进行模块共享的tf文件,
在 services/myapp/production/main.tf 文件中进行生产环境的设置,
在 services/myapp/staging/main.tf 文件中进行暂存环境的设置,
以此方式分别管理不同目录。
$ tree
.
├── modules
│ └── ec2
│ └── aws_instance.tf
└── services
└── myapp
├── production
│ └── main.tf
└── staging
└── main.tf
provider "aws" {}
resource "aws_instance" "app" {
...
tags {
Name = "app-${var.service}-${var.env}"
service = "${var.service}"
env = "${var.env}"
}
}
module "app" {
source = "../../../modules/ec2"
service = "myapp"
env = "production"
}
module "app" {
source = "../../../modules/ec2"
service = "myapp"
env = "staging"
}
实际上,Terraform还有一个叫做Workspace的功能,可以在一个目录中处理多个State。但是,个人而言,我觉得直接将目录分开管理更方便。
在一个理想的世界中,production/staging拥有完全相同的资源配置,只是参数设置有些微差异,也许可以在Workspace中进行运维。但实际情况是,staging环境可能还会有一些未发布的临时资源存在。由于不是完全相同的配置,在模块加载和仅在某些环境中存在的资源等方面,有一个地方可以吸收差异更加方便。
此外,将目录分开的副作用之一是,如果production/staging等环境使用不同的AWS账户,就需要分别使用AWS访问密钥等环境变量。但是,如果目录分开,就可以使用诸如direnv等工具轻松切换环境变量。
跨越多个环境的帐户在全球范围内共享资源,使用一种在不同目录中分离状态,并跨后续状态引用值的方法。
在模块之间引用值。
当进行模块划分时,会遇到在此模块中创建的值需要在其他模块中进行引用的情况。
模块的输出为output,模块的输入为variable,通过使用它们的组合,可以在模块之间传递值。
provider "aws" {}
variable "service" {}
variable "env" {}
output "hoge_private_ip" {
value = "${aws_instance.hoge.private_ip}"
}
resource "aws_instance" "hoge" {
tags {
Name = "hoge-${var.service}-${var.env}"
service = "${var.service}"
env = "${var.env}"
}
}
module "app" {
source = "../../../modules/ec2"
service = "myapp"
env = "production"
}
module "foo" {
source = "../../../modules/bar"
ip = "${module.app.hoge_private_ip}"
}
variable "ip" {}
跨越不同的State参考值
在前面的例子中,尽管涉及到跨模块变量的引用,但如果状态被分隔开,可以通过使用 terraform_remote_state 的数据源来引用其他状态的值。
如果想要在StateB中引用在StateA创建的资源,首先在StateA一侧通过output将需要引用的值输出。
output "hoge_instance_profile_name" {
value = "${aws_iam_instance_profile.hoge.name}"
}
所以,在想要访问的StateB端,我们使用terraform_remote_state来创建并引用StateA的数据源。
data "terraform_remote_state" "aws_main_iam" {
backend = "s3"
config {
bucket = "my-tfstate-main"
key = "aws-accounts/main/iam/terraform.tfstate"
region = "ap-northeast-1"
}
}
module "hoge" {
source = "../../../modules/hoge"
instance_profile_name = "${data.terraform_remote_state.aws_main_iam.hoge_instance_profile_name}"
}
当资源管理增加并且状态变得庞大时,会导致 Terraform 计划的刷新变慢,而且任何运维错误可能会导致影响范围扩大。因此,最好根据适当的粒度将状态进行分割保存。
我想要进行条件分支。
根据条件改变变量的值
当你在写Terraform时,你可能会想要写if语句,但很遗憾目前还没有if语句,但如果使用三元操作符的话是可以写的。
resource "aws_instance" "web" {
subnet = "${var.env == "production" ? var.prod_subnet : var.dev_subnet}"
}
根据条件创建或不创建资源。
尽管使用模块将代码通用化,仍然无法完全通过变量来吸收差异,有时候还需要根据条件创建或不创建某些资源。嗯,虽然可以把模块分开,但如果它们几乎相同,分开也没有那么必要。
Terraform中有一个名为count的参数,用于创建多个资源。结合之前提到的三元运算符,可以根据条件实现创建或不创建资源的功能。
resource "aws_cloudwatch_log_group" "hoge" {
count = "${var.create_log_group ? 1 : 0}"
name = "hoge"
}
管理Terraform版本的技术
升级Terraform的版本。
Terraform一直在不断进化。想要升级的原因各不相同,可能是想要使用最新的功能,也可能是希望解决已知的问题,总之,在长期操作中也会出现想要升级版本的情况。
以下是我的简略翻译:
目前撰写本文时,Terraform的版本是v0.11.1,但仍未达到v1.0,因此即使是v0.10.x => v0.11.x等次要版本升级也会引入不兼容的更改。这里我们将讨论关于Terraform核心的版本升级。所谓Terraform核心,指的是hashicorp/terraform,而从v0.10开始,Terraform将每个云服务提供商(例如AWS、GCP、Azure等)的插件分离为terraform-providers/terraform-provider-*。关于提供商的版本升级将在后文中进行描述。
在进行Terraform的版本升级时,有几个注意事项需要注意,但首先推荐解决升级前已经出现的警告。提前生成废弃警告日志,并在下一个版本中进行功能删除等操作。
因此,即使是次要版本升级,也建议逐步进行,例如从v0.9.x => v0.10.x => v0.11.x,而不是跳跃版本。目前,次要版本大约每几个月就会发布一次。
如何进行非兼容的更改,升级指南和更改日志中都有详细记录,请务必仔细阅读。 有时候可能需要修改*.tf文件。
公式的升级指南可以根据每个版本在以下链接中查阅:
https://www.terraform.io/upgrade-guides/index.html
CHANGELOG可以在 GitHub 的主体仓库中找到。
https://github.com/hashicorp/terraform/blob/master/CHANGELOG.md
如果在团队中进行多人开发,则必须保持版本一致,否则可能会导致事故发生。如果版本更新会导致不兼容的变化,需要修正*.tf文件,团队内将在合适的时机一起进行切换。
请参考下面提到的“切换Terraform版本”的具体步骤来进行版本升级。
升级版本后,请重新运行terraform plan,检查是否有意外的差异或新警告。若出现奇怪的差异,请查找CHANGELOG以确认相关更改。
切换Terraform的版本
Terraform是一个积极开发且版本更新相对较快的产品。
如果团队共同开发,必须注意确保每个人使用相同的版本,以避免因微小的版本差异导致意外的差异。
由于Terraform以编译后的二进制文件进行分发,只需下载特定版本并将其添加到PATH中,即可使用所需的版本。推荐使用名为tfenv的工具来简化这个过程。
如果你使用的是Mac OSX操作系统,可以通过Homebrew来安装tfenv。如果不是的话,你可以通过git clone来获取tfenv,并适当地设置PATH来使用。请参阅tfenv的README以获得更多详情。
$ brew install tfenv
$ tfenv --help
Usage: tfenv <command> [<options>]
Commands:
install Install a specific version of Terraform
use Switch a version to use
uninstall Uninstall a specific version of Terraform
list List all installed versions
list-remote List all installable versions
查看可用版本。
$ tfenv list-remote | head
0.11.1
0.11.0
0.11.0-rc1
0.11.0-beta1
0.10.8
0.10.7
0.10.6
0.10.5
0.10.4
0.10.3
安装指定的版本。
$ tfenv install 0.11.0
[INFO] Installing Terraform v0.11.0
[INFO] Downloading release tarball from https://releases.hashicorp.com/terraform/0.11.0/terraform_0.11.0_darwin_amd64.zip
######################################################################## 100.0%
[INFO] Downloading SHA hash file from https://releases.hashicorp.com/terraform/0.11.0/terraform_0.11.0_SHA256SUMS
tfenv: tfenv-install: [WARN] No keybase install found, skipping GPG signature verification
Archive: tfenv_download.ef4YCM/terraform_0.11.0_darwin_amd64.zip
inflating: /usr/local/Cellar/tfenv/0.6.0/versions/0.11.0/terraform
[INFO] Installation of terraform v0.11.0 successful
[INFO] Switching to v0.11.0
[INFO] Switching completed
$ terraform --version
Terraform v0.11.0
在管理tf文件的存储库根目录下放置一个名为.terraform-version的文件,并在其中写下版本号,这样就可以省略tfenv install中的版本号。
$ cat .terraform-version
0.10.7
$ tfenv install
[INFO] Installing Terraform v0.10.7
[INFO] Downloading release tarball from https://releases.hashicorp.com/terraform/0.10.7/terraform_0.10.7_darwin_amd64.zip
######################################################################## 100.0%
[INFO] Downloading SHA hash file from https://releases.hashicorp.com/terraform/0.10.7/terraform_0.10.7_SHA256SUMS
tfenv: tfenv-install: [WARN] No keybase install found, skipping GPG signature verification
Archive: tfenv_download.bHmtrT/terraform_0.10.7_darwin_amd64.zip
inflating: /usr/local/Cellar/tfenv/0.6.0/versions/0.10.7/terraform
[INFO] Installation of terraform v0.10.7 successful
[INFO] Switching to v0.10.7
[INFO] Switching completed
这种方法的好处是,无论 tf 文件的存储库的 Terraform 版本如何不同,只要进行 cd 操作,它就会自动根据 .terraform-version 切换版本。
固定 Terraform 版本。
如果开发团队中不是每个人都使用tfenv这个方便的工具来切换版本,那就不能强制版本。要强制固定使用Terraform的版本,需要在tf文件中定义terraform.required_version。
我尝试将示例固定在terraform v0.10.7版本上。
terraform {
required_version = "= 0.10.7"
}
假设在v0.11.0版本中使用此项,则会出现以下错误。
$ terraform --version
Terraform v0.11.0
+ provider.null v1.0.0
$ terraform plan
Error: The currently running version of Terraform doesn't meet the
version requirements explicitly specified by the configuration.
Please use the required version or update the configuration.
Note that version requirements are usually set for a reason, so
we recommend verifying with whoever set the version requirements
prior to making any manual changes.
Module: root
Required version: = 0.10.7
Current version: 0.11.0
如果你想严格强制版本,最好在tf文件的一侧写下来。
升级Terraform提供程序的版本。
升级Terraform提供程序的注意事项包括检查警告日志和变更日志等基本操作,与Terraform主程序基本相同。
例如,对于AWS来说,CHANGELOG可以在以下链接找到:
https://github.com/terraform-providers/terraform-provider-aws/blob/master/CHANGELOG.md
当进行版本升级时,你可以在 `terraform init` 命令中使用 `-upgrade` 选项,这样会将提供者升级到符合版本要求的最新版。
$ terraform init -upgrade
因此,建议在升级到指定版本时,也应明确将提供程序与版本限制一起固定。
固定Terraform提供程序版本。
在 provider 块中可以写入提供程序的版本限制。例如,对于 AWS,可以像这样写:
provider "aws" {
version = "= 1.3.0"
}
如果进行团队开发,最好在版本之间固定微小差异,避免出现困难。
编写tfstate文件的技术
在此之后是相当发展性的话题。
要熟练使用Terraform,不可避免地需要理解记录状态的terraform.tfstate文件。
可以说掌握了tfstate就掌握了Terraform,这并不为过。
tfstate文件是内部实现,因此,如果记录在此处,随着Terraform版本的提升,可能会变得陈旧。然而,我认为在当前情况下分享已有的知识仍然有某种意义,所以我会尽力在我理解的范围内进行写作。
承诺:由于手动修改tfstate文件非常危险,即使基础设施被破坏,也无法承担责任。如果要尝试,请先在一个可破坏的环境中进行尝试,并在了解自己在做什么之后自行承担责任。
在本地查看最小化的状态。
我会写一个适当的tf文件作为样本。
provider "null" {}
resource "null_resource" "hoge" {}
运行terraform apply后,State将会被保存。
如果没有使用远程State,默认情况下,State将以terraform.tfstate的名称保存在本地的当前目录中。
让我们来查看一下其内容。
{
"version": 3,
"terraform_version": "0.11.1",
"serial": 1,
"lineage": "6fab4dbe-eca7-405b-8210-c82c5276ac28",
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {
"null_resource.hoge": {
"type": "null_resource",
"depends_on": [],
"primary": {
"id": "1492336661259070634",
"attributes": {
"id": "1492336661259070634"
},
"meta": {},
"tainted": false
},
"deposed": [],
"provider": "provider.null"
}
},
"depends_on": []
}
]
}
看一眼就能明白,它只是一个普通的JSON。
尽管项目的含义并未被明确记录在实施详细文档中,但根据浏览源代码的感觉,如下所示。
modules中,每个module都记录了一个状态,而module是由以下元素构成的。
[root, hoge]
というように記録されてます。outputモジュールのoutputです。resourcesリソースの状態です。depends_onモジュール間の依存です。资源中,每个资源都记录了其状态,而资源由以下要素构成。顺便提一下,虽然称之为资源,但也包括了以”data.”为前缀的特殊数据源资源的记录。
create_before_destroy
のときに使われます。providerプロバイダの名前です。プロバイダはマルチリージョンしたりするのに、エイリアスを作ったりできるので記録されています。主要的部分代表了实际资源的状态。
null_resouce
の場合はただの乱数ですが、一般的にはインスタンスIDなど、APIなどでそのリソース情報を取得するのにユニークに特定し得る値が使われます。attributesリソース属性です。リソースの設定項目などの具体的な値を保存しているのはここです。metaTerraformコアからは無視されるけど外部ツールが使うことを意図したメタ情報です。tainted汚染フラグです。リソースをtainted状態にすると、次回のplanで削除=>作成で再作成するplanになります。查看遠程的狀態
即使正在远程保存状态,使用terraform state pull命令也可以将状态显示在标准输出上。
当状态出现问题时,可以通过检查状态或将其重定向进行简易备份。
{
"version": 3,
"terraform_version": "0.10.7",
"serial": 554,
"lineage": "dd612537-035f-4cf3-94c8-0618bab715cd",
"backend": {
"type": "s3",
"config": {
"bucket": "my-tfstate-main",
"key": "services/myapp/staging/terraform.tfstate",
"region": "ap-northeast-1"
},
"hash": 4435600807653202963
},
"modules": [
{
"path": [
"root"
],
强制重新生成资源。
有时候,出于各种理由,我们希望强制重新生成资源。
例如,当 terraform apply 失败时,可能会导致某些资源处于中途的微妙状态。
在这种情况下,通过使用 terraform taint 命令来标记资源为 tainted 状态,下一次计划(plan)将创建一个包含强制删除和重新生成的计划。
provider "null" {}
resource "null_resource" "hoge" {}
$ terraform taint null_resource.hoge
The resource null_resource.hoge in the module root has been marked as tainted!
在这里,指定了根模块的资源,但如果要taint根模块之外的模块内的资源,请使用参数-module=来指定模块路径。
通过查看计划,可以了解到以下重新生成的+/- 意图。
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
null_resource.hoge: Refreshing state... (ID: 1492336661259070634)
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement
Terraform will perform the following actions:
-/+ null_resource.hoge (tainted) (new resource required)
id: "1492336661259070634" => <computed> (forces new resource)
Plan: 1 to add, 0 to change, 1 to destroy.
------------------------------------------------------------------------
Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.
将不在Terraform管理之下的现有资源纳入Terraform管理。
如果能够在所有东西都由Terraform管理的理想世界中就好了,但是如果在现有项目中后期引入Terraform,可能会希望将已经存在但不受Terraform管理的资源纳入Terraform的管理范围。如果这些资源可以轻松重新生成,那么重新创建是最快的方法。但是对于已经在生产环境运行的资源来说,实际上重新创建是相当困难的事情。
正如我们之前看到的那样,Terraform将其所管理的资源状态存储为State,因此有多种方式可以创建这个State。
首先,可以尝试使用官方提供的terraform import命令。该命令可用于在State中添加新资源,只需指定要导入资源的State地址和资源ID即可。需要注意的是,它并不会自动生成配置,因此需要自己编写tf文件。
provider "aws" {}
让我们以AWS IAM角色资源类型为例,使用Terraform中的资源名称test_kitchen_ec2_role,将AWS上的IAM角色名称test-kitchen-ec2-role导入。
若试图在tf文件中未定义的情况下进行导入,将会出现错误。
$ terraform import aws_iam_role.test_kitchen_ec2_role test-kitchen-ec2-role
Error: resource address "aws_iam_role.test_kitchen_ec2_role" does not exist in the configuration.
Before importing this resource, please create its configuration in the root module. For example:
resource "aws_iam_role" "test_kitchen_ec2_role" {
# (resource arguments)
}
之前可以做到这一点,但是只添加了State,却没有Configuration,这样会悲剧地可以执行销毁计划,因此为了安全起见,可能会变成错误。我将只在tf文件中创建资源框架。
provider "aws" {}
resource "aws_iam_role" "test_kitchen_ec2_role" {
}
我尝试导入一下。
$ terraform import aws_iam_role.test_kitchen_ec2_role test-kitchen-ec2-role
aws_iam_role.test_kitchen_ec2_role: Importing from ID "test-kitchen-ec2-role"...
aws_iam_role.test_kitchen_ec2_role: Import complete!
Imported aws_iam_role (ID: test-kitchen-ec2-role)
aws_iam_role.test_kitchen_ec2_role: Refreshing state... (ID: test-kitchen-ec2-role)
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
让我们看看terraform.tfstate文件。
{
"version": 3,
"terraform_version": "0.11.1",
"serial": 2,
"lineage": "591a7666-d636-4e4f-aea8-8a9df641e460",
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {
"aws_iam_role.test_kitchen_ec2_role": {
"type": "aws_iam_role",
"depends_on": [],
"primary": {
"id": "test-kitchen-ec2-role",
"attributes": {
"arn": "arn:aws:iam::XXXXXX:role/test-kitchen-ec2-role",
"assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}",
"create_date": "2016-07-12T03:00:40Z",
"force_detach_policies": "false",
"id": "test-kitchen-ec2-role",
"name": "test-kitchen-ec2-role",
"path": "/",
"unique_id": "XXXXXXX"
},
"meta": {},
"tainted": false
},
"deposed": [],
"provider": "provider.aws"
}
},
"depends_on": []
}
]
}
可以看出已经加载了assume_role_policy等内容。
尝试规划一下。
$ terraform plan
Error: aws_iam_role.test_kitchen_ec2_role: "assume_role_policy": required field is not set
由于 assume_role_policy 是必需项,所以导致错误。
我们将在tf文件中添加assume_role_policy。你可以直接将State的值硬编码到tf文件中,但由于没有换行的JSON很难阅读,所以我们可以使用jq进行适当的格式化,并将其读取到外部文件中。
$ echo "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}" \
| jq . > ec2_assume_role_policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
provider "aws" {}
resource "aws_iam_role" "test_kitchen_ec2_role" {
assume_role_policy = "${file("./ec2_assume_role_policy.json")}"
}
我会制定计划。
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
aws_iam_role.test_kitchen_ec2_role: Refreshing state... (ID: test-kitchen-ec2-role)
------------------------------------------------------------------------
No changes. Infrastructure is up-to-date.
This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, no
actions need to be performed.
直到计划的差异消失,我们会对tf文件的设置进行调整。
顺便说一下,很遗憾地通知您,这个 terraform import 命令并不支持所有的资源类型。
如果要导入的资源类型不支持,执行导入操作将会出现错误。
如果公式的import命令不支持某种资源类型,则需要自己编写State。但是从空白处开始编写State很困难,因为不知道有哪些属性。实际上,可以按照以下步骤进行操作。
-
- Terraformのドキュメントを眺めつつダミーのtfファイルを作る
-
- ダミーのリソースを terraform apply してtfstateの雛形作成
-
- AWSコンソールを眺めつつターゲットとなるリソースのtfファイルを作る
-
- tfstateの雛形をコピーしてターゲットとなるリソースのtfstateの枠を作る
terraform refresh して terraformでtfstateを既存リソースの状態に合わせる
ダミーリソースを削除して terraform plan で差分がなくなれば完成
我在Terraform v0.6时代写的资料可能有些过时了,但你可以参考以下文章来了解实际示例。基本流程没有改变。
借助Terraform,我们可以自己编写tfstate来将现有资源转移到Terraforming管理下。
需要注意的是从那时起的差异是,如果将 State 保存到 Remote 中,则默认情况下不再保存 terraform.tfstate 文件到本地。要编辑 State,请将 terraform state pull 重定向到文件以先保存到本地,然后编辑文件,并使用 terraform state push 进行覆盖。
顺便说一下,在上述文章中提到的 terraforming 工具是第三方工具,它存在于 terraform import 命令正式发布之前。
重构tf文件
当Terraform的代码逐渐增长时,我们可能会想要进行重构。
想要进行重构的原因有很多,比如我们可能想要统一资源名称的表达方式,或者将常用的资源集合切分为模块等。这时候会面临一个问题,就是如何处理tfstate。
让我们看一个简单的例子。假设有一个名为 null_resource.hoge 的资源,如下所示,
provider "null" {}
resource "null_resource" "hoge" {}
我想将其重命名为 null_resource.fuga 资源。
provider "null" {}
resource "null_resource" "fuga" {}
如果不加思考地重命名,会生成一个计划,即删除 null_resource.hoge,然后创建 null_resource.fuga,如以下所示。请注意,这是用中文进行的重述。
$ terraform plan
Terraform will perform the following actions:
+ null_resource.fuga
id: <computed>
- null_resource.hoge
Plan: 1 to add, 0 to change, 1 to destroy.
如果有可再生成的资源,那么这样做也可以。但实际情况是,有些资源比如数据库难以重新创建。
在这种情况下,可以使用 terraform state mv 命令。
该命令将移动tfstate内的资源位置,并且同时可以进行重命名。
$ terraform state mv null_resource.hoge null_resource.fuga
Moved null_resource.hoge to null_resource.fuga
重命名后再次计划,发现没有差异。
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
null_resource.fuga: Refreshing state... (ID: 8190077603963424291)
------------------------------------------------------------------------
No changes. Infrastructure is up-to-date.
This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, no
actions need to be performed.
调试Terraform的技术
输出调试日志
如果你碰到了Terraform的错误,你可以尝试输出调试日志。
通过将TF_LOG=DEBUG设置为环境变量,可以输出调试日志。日志级别可以指定为TRACE、DEBUG、INFO、WARN和ERROR。
$ TF_LOG=DEBUG terraform init
2017/12/05 23:27:52 [INFO] Terraform version: 0.11.1 a42fdb08a43c7fabb8898fe8c286b793bbaa4835+CHANGES
2017/12/05 23:27:52 [INFO] Go runtime version: go1.9
2017/12/05 23:27:52 [INFO] CLI args: []string{"/usr/local/Cellar/tfenv/0.6.0/versions/0.11.1/terraform", "init"}
2017/12/05 23:27:52 [DEBUG] Attempting to open CLI config file: /Users/minamijoyo/.terraformrc
2017/12/05 23:27:52 [DEBUG] File doesn't exist, but doesn't need to. Ignoring.
2017/12/05 23:27:52 [INFO] CLI command args: []string{"init"}
2017/12/05 23:27:52 [DEBUG] plugin: waiting for all plugin processes to complete...
Terraform initialized in an empty directory!
The directory has no Terraform configuration files. You may begin working
with Terraform immediately by creating Terraform configuration files.
编译Terraform源代码
虽然使用terraform命令无需考虑太多,但其内部是通过多个进程运行的。它的内部结构相当复杂,即使输出了调试日志,也很难一眼看出与源代码的对应关系。如果能在查看调试日志的同时,还能显示文件名和行号,那就会感到很幸福。
为此,需要稍微修改一下当前的源代码,然后尝试从源代码中编译terraform。
Terraform 是用 Go 语言编写的,您可以通过 go get 将其下载到本地,然后修改代码并重新使用 go install 来运行修复版本。我会跳过设置 Go 语言开发环境的部分,请自行搜索。如果要查找所需的编译版本,请参考 .travis.yml 文件。目前似乎是 Go 1.9.1。
$ go get github.com/hashicorp/terraform
$ cd $GOPAHT/src/github.com/hashicorp/terraform
(コードいじる)
$ go install
這不僅適用於Terraform本體,也適用於提供者。只要 $GOPATH/bin 的路徑正確,提供者也會被包括在搜索範圍內,你可以使用自行編譯的版本。
$ go get github.com/terraform-providers/terraform-provider-null
$ cd $GOPAHT/src/github.com/terraform-providers/terraform-provider-null
(コードいじる)
$ go install
在调试日志中添加源代码文件名和行号
现在我们知道如何编译了,让我们在调试日志中添加源代码文件名和行号。
在hashicorp/terraform的main.go文件的main函数开头处稍作修改,配置日志。
func main() {
log.SetFlags(log.Llongfile) // ★この行を追加
// Override global prefix set by go-dynect during init()
log.SetPrefix("")
os.Exit(realMain())
}
只需设置Go标准日志包的log.Llongfile,然后编译并将二进制文件安装到$GOPATH/bin目录中。
$ go install
让我们试一试吧。
$ TF_LOG=DEBUG $GOPATH/bin/terraform init
/Users/minamijoyo/src/github.com/hashicorp/terraform/main.go:120: [INFO] Terraform version: 0.11.2 dev
/Users/minamijoyo/src/github.com/hashicorp/terraform/main.go:123: [INFO] Go runtime version: go1.9
/Users/minamijoyo/src/github.com/hashicorp/terraform/main.go:124: [INFO] CLI args: []string{"/Users/minamijoyo/bin/terraform", "init"}
/Users/minamijoyo/src/github.com/hashicorp/terraform/main.go:239: [DEBUG] Attempting to open CLI config file: /Users/minamijoyo/.terraformrc
/Users/minamijoyo/src/github.com/hashicorp/terraform/main.go:250: [DEBUG] File doesn't exist, but doesn't need to. Ignoring.
/Users/minamijoyo/src/github.com/hashicorp/terraform/main.go:198: [INFO] CLI command args: []string{"init"}
/Users/minamijoyo/src/github.com/hashicorp/terraform/vendor/github.com/hashicorp/go-plugin/client.go:235: [DEBUG] plugin: waiting for all plugin processes to complete...
Terraform initialized in an empty directory!
The directory has no Terraform configuration files. You may begin working
with Terraform immediately by creating Terraform configuration files.
现在,输出日志的地方将显示文件的完整路径和行号。这样,代码阅读将变得更加顺利。
使用调试器逐步执行Terraform
当我们查看日志时,就会想要在调试器中进行单步执行。由于Go语言中有一个叫做delve的调试器,我们可以使用它。由于之前已经写过关于delve的使用说明,所以可以参考这篇文章。
使用Golang的调试工具delve的方法
如果您在使用Terraform和Delve进行调试时,请注意以下事项。这就是Terraform在内部由多个进程组成的事实。为了在内部封装Panic并生成清晰的日志,Terraform使用了名为mitchellh/panicwrap的库,以重新启动自身作为另一个进程。通过阅读代码,您会明白,如果设置了TF_FORK=0环境变量,则可以避免此行为并直接启动进程。
在使用Delve进行调试时,传递给调试目标命令的参数应在 — 之后进行编写。
所以,如果要运行terraform init,可以按以下方式执行。
$ TF_LOG=TRACE TF_FORK=0 dlv debug -- init
Type 'help' for list of commands.
(dlv) b main.main
Breakpoint 1 set at 0x27b28af for main.main() ./main.go:32
(dlv) c
> main.main() ./main.go:32 (hits goroutine(1):1 total:1) (PC: 0x27b28af)
27: const (
28: // EnvCLI is the environment variable name to set additional CLI args.
29: EnvCLI = "TF_CLI_ARGS"
30: )
31:
=> 32: func main() {
33: log.SetFlags(log.Llongfile) // ★この行を追加
34: // Override global prefix set by go-dynect during init()
35: log.SetPrefix("")
36: os.Exit(realMain())
37: }
顺便说一下,Terraform的主进程和提供者的进程是分开的,它们通过RPC进行进程间通信。目前还没有找到一种方法来连接到提供者进程的delve。Delve有一种功能可以连接到远程进程,但是提供者进程并不像守护进程一样始终运行,它只在Terraform命令执行期间运行,因此很难把握时机来进行连接。
如果有人知道更好的提供者端调试方法,请告诉我,因为我不知道除了插入适当的日志输出之外,还有什么更有效的方法来调试提供者端的代码,即所谓的打印调试。
(2019/06/27追記)
根据我的理解,由于provider的进程是从terraform核心启动的,所以很难将delve启动的二进制文件插入其中。但是,在搜索问题(issue)的过程中,我发现可以使用dlv test命令启动acceptance test来调试provider,我觉得这个方法很可行。于是我尝试了一下,果然成功了。
来源:https://github.com/go-delve/delve/issues/496#issuecomment-413049532
$ TF_ACC=1 dlv test ./aws -- -test.v -test.run=TestAccAWSIAMRole_basic
在结尾处
使用 Terraform 日常学到的经验,我简洁地整理了起来。
如果有 Terraform 的专家们知道一些技巧,欢迎共享经验。
(2020/12/11更新)
由于信息已经过时,我不得不感觉到它的陈旧,所以写了三年后的续集。请看下面的作品《2020年再入门Terraform职人》。