在使用Terraform来运行AWS环境时,所遇到的问题及其解决方法

2017年3月時点においてのメモです。Terraformのバージョンは0.8.6でしたが、現在のバージョンはTerraform v0.9.0-dev(github/master)です。

想要使用Terraform的最新版本。

为难的事情 (Komat ta koto)

我很想尽快使用0.9版本的功能。

解决办法

好像可以很容易地从GitHub构建。Go语言很厉害。

$ brew uninstall terraform
$ brew install go
$ cat <<'EOS' >> ~/.bashrc
export PATH=$(go env GOPATH)/bin:$PATH
EOS
$ go install github.com/hashicorp/terraform
$ terraform -v
Terraform v0.9.0-dev

有没有什么好的参考代码?

困扰之事 zhī shì)

不只是想要参考资料,还想要看一些示例或样本代码。

策略的解决

在GitHub上,有一个公式的最佳实践。

还有,这里可能也会有所参考价值。
https://github.com/terraform-community-modules

处理AWS账户的身份验证信息

困扰的事情

根据网络上的参考信息,有很多示例都直接将AWS的认证信息(credentials)写入代码中。然而,如果采取修改tf文件的运维方式,很可能会在某个时候出错,不小心提交和推送到版本控制系统中,导致事件发生。

我认为用于首次运行Terraform的用户认证信息通常具有很高的权限,因此我们需要谨慎对待。

解决方案

使用AWS-CLI的命名配置文件。

$ aws configure --profile my-profile-name
AWS Access Key ID [None]: xxxxxxxxxx
AWS Secret Access Key [None]: xxxxxxxxxx
Default region name [None]: ap-northeast-1
Default output format [None]: 

如果事先进行这样的设置,认证信息将以命名方式存储在$home/.aws/credentials中。

从脚本中可以这样引用。这样的话即使进行提交也没有问题。

provider "aws" {
  profile = "my-profile-name"
}

只需按照上述的设置步骤将其写入README.md文件中,然后正常管理KEYID和SECRET即可。

.tfstate文件的处理方式

困扰

如果丢失.tfstate文件,我们将无法继续通过Terraform管理所创建的实例,所以它是最重要的文件。
然而,按照将其提交到git的规定,担心会忘记提交或忘记进行pull时会很麻烦。
另一方面,即使手动在各个地方进行处理,也可能会出现其他开发者基于旧状态创建重复的实例的情况。

解决方案

让我们使用后端S3。这是Terraform 0.9版本的新功能。

terraform {
  backend "s3" {
    bucket  = "my-tfstate-store-name-at-s3"
    key     = "hogehoge/terraform.tfstate"
    profile = "my-profile-name"
  }
}

说在前面,只需运行terraform init,就可以同步S3的tfstate。
请务必将指定的存储桶设置为acl=private!
此外,推荐在上述链接中添加S3的版本控制等设置。(我没有做)

除了S3之外,似乎还准备了各种手段。

这样就可以正常使用git进行管理了。
之前保存在.terraform/terraform.tfstate的数据会直接保存在S3上的文件中,不再进行本地缓存,所以要注意。

解决方法(过时)

让我们使用Backend中的远程状态功能。

有正式的官方文档。https://www.terraform.io/docs/state/remote/s3.html

这样子。因为可以指定个人资料,所以可以在那里写上地区以简化。

$ terraform remote config \
    -backend=s3 \
    -backend-config="bucket=my-tfstate-store-name-at-s3" \
    -backend-config="key=hogehoge/terraform.tfstate" \
    -backend-config="profile=my-profile-name"

在执行此命令时,已存在的terraform.tfstate似乎被移动到./.terraform/terraform.tfstate。

然后,它会自动上传和下载tfstate文件。
请确保指定的存储桶为acl=private!
建议在保存的存储桶上添加版本控制等功能。(我没有这样做)
除了S3,似乎还提供了各种保存选项。

对不同环境中的变量进行处理。

困った事情
(Komatagatta jijō)

我想要像分开暗中操作和正式操作那样,为每个环境设置不同的变量数值。但是,用-var选项在参数中全部指定太麻烦了。

解决方案 (jiě jué cè)

如果将共通配置写在名为terraform.tfvars的文件中,即使不指定也会自动读取。

根据不同的环境,可以使用-var-file选项来进行切换。

$ terraform plan -var-file staging.tfvars
$ terraform plan -var-file production.tfvars

很普通。

只要巧妙地使用,据说var后面指定的内容将会被覆盖,所以应该会很强大。

想要使用Packer创建的AMI生成EC2实例。

困惑的事情 de

当考虑到自动伸缩时,我希望能够正确运营起源的AMI。
(注意,我可能对AWS的自动伸缩不太了解。)

所以,如果使用Packer创建一个不可变的镜像,那将会非常棒。
不可变意味着镜像中必须包含DB连接信息。
因此,需要先使用Terraform来创建RDS,但是Terraform又需要使用AMI,所以就出现了这样的问题…
造成了循环引用。

一开始,在替换AutoScaling下的实例时,要确保不关闭所有的实例,似乎相当困难。
看来AutoScaling与之相容性有待改善。

Reference: http://qiita.com/minamijoyo/items/e32eaeebc906b7e77ef8
参考资料:http://qiita.com/minamijoyo/items/e32eaeebc906b7e77ef8

可能的策略1 空资源解决方案

最终虽然没有成功,但我最初尝试的方法。

有一种叫做 null_resource 的东西。

可以定義代表不進行任何操作的觸發(triggers)屬性和能夠配置provisioner的資源。

variable "ami_name_prefix" {}

resource "null_resource" "packer" {
  triggers {
    db_host = "${aws_db_instance.hogehoge.address}"
  }

  provisioner "local-exec" {
    command = <<EOS
packer build \
  -var 'DB_HOST=${aws_db_instance.hogehoge.address}' \
  -var 'AMI_NAME_PREFIX=${ami_name_prefix}' \
  packer.json"
EOS
  }
}

data "aws_ami" "packer_ami" {
  most_recent = true
  name_regex = "^${var.ami_name_prefix}.*"
  owners = ["self"]
  depends_on = ["null_resource.packer"]
}

resource "aws_instance" "hoge" {
  ami_id = "${data.aws_ami.packer_ami.id}"
  ...
}

就是这种感觉。

然而,使用Terraform 0.8.6版本时,将内插表达式与触发器混合使用会被强制认定为”computed”,导致每次都会创建AMI。从Terraform的角度来看,无法仅执行差异操作,因此无法使用该方式。

放棄不變的解決方案

自动缩放本身在这种情况下可能不可行吗?我认为需要重新进行AMI化。

预先组装好几乎完成的AMI并使用aws_instance的provisioner来完成最后的修改。听说Docker之类的东西可以通过环境变量来读取外部信息,所以我决定采用这种方法。

(2017/3 注:如果使用AWS EC2,可以使用user_data机制。使用这个机制可能更简单。感谢您在评论中指出。)

// .envが欠けた状態でAMIをつくる
$ packer packer.json
data "aws_ami" "packer_ami" {
  most_recent = true
  name_regex = "^packer-ami-.*$"
  owners = ["self"]
  depends_on = ["null_resource.packer"]
}

data "template_file" "envfile" {
  # template = "${file(./env.tpl)}" などとした方が望ましいが例示のため。
  template = <<EOS
export DB_HOST="${db_host}"
export DB_PORT="${db_port}"
....
EOS

  vars {
    db_host = "${aws_db_instance.main.address}"
    db_port = "${aws_db_instance.main.port}"
  }
}

resource "aws_instance" "ec2" {
  ami_id = "${data.aws_ami.packer_ami.id}"
  ...

  # envファイルをuploadする。envファイルはdirenvとかdotenvとかで読み込まれるようにしておく。
  provisioner "file" {
    content = "${data.template_file.envfile.rendered}"
    destination = "/home/ec2-user/.env"
  }

  # 上で入れた環境変数を使ってサービスを立ち上げる。
  provisioner "remote-exec" {
    inline = ["sudo service unicorn start"]
  }

  # provisionerは、実行した環境からssh(のgolang実装。sshコマンドではない)で接続することになる。
  # 当然security_groupとvpc_internet_gatewayが適切に設定されている必要がある。
  connection {
    type = "ssh"
    ...
  }
}

我想要对tf文件进行结构化处理。

无法解决的问题

抄襲和重复是不好的,而且降低了再利用性。

计算

你可以使用COUNT = n来创建多个具有相同类型的资源,就像创建数组一样。
如果你想要给每个资源写入稍微不同的值,

COUNT = 2
ATTR = "${element(list("a", "b", "c", "d"), count.index)}"

可以进行诸如此类的操作。
把list变成变量会更好。

模块 (mó​​​ ​)

如果涉及多个资源的重复模式,最好将其模块化。
关于模块的具体编写方式在此不做解释。

./modules/my_module/variables.tf
./modules/my_module/main.tf
./modules/my_module/output.tf

./模块/我的模块/变量.tf
./模块/我的模块/主要.tf
./模块/我的模块/输出.tf

制造

module "use_of_my_module" {
  source = "./my_module"
  var1 = ...
  var2 = ...
}

用来书写。

$ terraform get

然后,让我们准备好将模块加载到terraform中。(好像在./.terraform/modules/中创建了一个符号链接)

由于似乎有各种不同的模块可供公开使用,也许更好的选择是不自己编写资源。

其他

如果有任何问题,请写下来。

广告
将在 10 秒后关闭
bannerAds