☕️

ecspresso advent calendar 2020 day 19 - Terraform tfstate

2020/12/19に公開

Amazon ECS のデプロイツールである ecspresso の利用法をまとめていく ecspresso Advent calendar 19日目です。

定義ファイルに記述される外部リソース

ecspresso が使用するサービス定義とタスク定義ファイルには、関連する外部リソースの ID や ARN (Amazon Resource Names) が必要になることがあります。

次の例では、サービス定義ファイルにLBターゲットグループのARNと、VPCセキュリティーグループとサブネットのIDが記述されています。

{
  "launchType": "FARGATE",
  "loadBalancers": [
    {
      "containerName": "nginx",
      "containerPort": 80,
      "targetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:12345789012:targetgroup/EC2Co-Defau-SA0YF35VBLCJ/b491afabf44765de"
    }
  ],
  "networkConfiguration": {
    "awsvpcConfiguration": {
      "assignPublicIp": "ENABLED",
      "securityGroups": [
        "sg-043eab2d606362f03"
      ],
      "subnets": [
        "subnet-0089ed3e1bdff1fc9",
        "subnet-0d750adbd139f411d"
      ]
    }
  }
}

ecspresso ではこれらの外部リソースは管理していないため、ID を知ることができません。しかし ID や ARN の値を定義ファイルに直接記述してしまうと、メンテナンス性や記述性を落とす原因になります。

Terraform State ファイルを読み込む

TerraformHashiCorp が開発しているクラウドインフラの構成管理をコードで行う OSS です。AWS における IaC (Infrastructure as Code) ツールとしては、公式の CloudFormation と双璧をなす存在でしょう。

ecspresso では、Terraform の State ファイル (tfstate) を読み込み、そこに記述してあるリソースの名前から ID や ARN などの属性を参照できます。

この機能を使用するためには、設定ファイルに terraform.tfstate ファイルへのパスを記述します。パスは設定ファイルの位置からの相対パスです。

plugins:
  - name: tfstate
    config:
      path: terraform.tfstate

ここでは必要なリソースを管理するために、Terraform の tf ファイルを次のように用意したとします。

provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_vpc" "ecspresso-demo" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "az-a" {
  cidr_block = "10.0.0.0/24"
  vpc_id     = aws_vpc.ecspresso-demo.id
}

resource "aws_subnet" "az-c" {
  cidr_block = "10.0.1.0/24"
  vpc_id     = aws_vpc.ecspresso-demo.id
}

data "aws_security_group" "http" {
  id = "sg-043eab2d606362f03"
}

resource "aws_lb_target_group" "ecspresso-demo" {
  name        = "EC2Co-Defau-SA0YF35VBLCJ"
  port        = 80
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = aws_vpc.ecspresso-demo.id
}

ここでは例として、セキュリティグループのみ Data source として定義してあります。

定義ファイルでの tfstate 参照

定義ファイルのテンプレート関数 tfstate を使用すると、tfstate ファイル内のリソースを Terraform のリソース名指定形式で参照できます。最初に示したサービス定義ファイルを、tfstate 参照を使用して書き換えると次のようになります。

{
  "launchType": "FARGATE",
  "loadBalancers": [
    {
      "containerName": "nginx",
      "containerPort": 80,
      "targetGroupArn": "{{ tfstate `aws_lb_target_group.ecspresso-demo.arn` }}"
    }
  ],
  "networkConfiguration": {
    "awsvpcConfiguration": {
      "assignPublicIp": "ENABLED",
      "securityGroups": [
        "{{ tfstate `data.aws_security_group.http.id` }}"
      ],
      "subnets": [
        "{{ tfstate `aws_subnet.az-a.id` }}",
        "{{ tfstate `aws_subnet.az-c.id` }}"
      ]
    }
  }
}

リソースの属性は id や arn 以外にも tfstate 内に存在している値であれば、すべて参照してテンプレートへの展開が可能です。

これで定義ファイルから生の ID や ARN がなくなり、すべて名前で解決ができるようになりました。

実際に ecspresso render --service-definition でレンダリングしたり、ecspresso diff で ECS 上のサービスの状態と差分がないかを確認できます。

tfstate 参照の利点

ID を直接記述するのと比べて、tfstate を参照する利点は次のようなものが上げられます。

見た目が分かりやすい

ランダムな文字列である ID よりも、人間がみた場合に分かりやすい記述になります。

仮に tfstate 内に存在しない名前を記述した場合はテンプレートの展開が失敗するため、typo してもデプロイする前に容易に発見できます。

別の環境でも同じリソース名であれば定義ファイルを変更しなくてよい

例えばステージング環境と本番環境で実際のリソースが別に存在しているような場合に便利です。

環境ごとに異なる tfstate が同じリソース名で管理されていれば、ecspresso から参照する tfstate を変えるだけで実際のリソース ID を環境によって異なる値にできるため、定義ファイルを共用できます。

tfstate 参照 Tips

配列形式リソース名の扱い

Terraform のリソースを for_each などで記述すると、リソースを特定するための名前は aws_subnet.az["a"] のように配列の参照形式になることがあります。これをそのまま tfstate 関数に渡すために次のように記述すると、JSON としての構造が壊れてしまいます。

"{{ tfstate `aws_subnet.az["a"].id` }}"

5日目 テンプレート記法 で説明したとおり、このように構造が壊れても ecspresso での読み込みには問題ないものの、エディタで編集する時に困ることがあります。

そのため、正規の Terraform の記法ではありませんが、利便性のために ecspresso では配列形式の記述でダブルクォートとシングルクォートを同様に扱うようになっています。

"{{ tfstate `aws_subnet.az['a'].id` }}"

tfstate を実際に参照してどんな値が入ってるか確認したい

tfstate は JSON 形式なので、人間が直接読むことはできます。ただし、Terraform でリソースを指定するアドレスを文字列で検索してリソースにたどりつくことはできないため、確認に便利な方法を2点紹介します。

terraform state list / show を使う

terraform state コマンドの list, show を使うと、 tfstate 内の要素を確認できます。

https://www.terraform.io/docs/commands/state/index.html

$ terraform state list -state=terraform.tfstate
data.aws_security_group.http
aws_lb_target_group.ecspresso-demo
aws_subnet.az-a
aws_subnet.az-c
aws_vpc.ecspresso-demo
$ terraform state show -state=terraform.tfstate aws_subnet.az-a
# aws_subnet.az-a:
resource "aws_subnet" "az-a" {
    arn                             = "arn:aws:ec2:ap-northeast-1:123456789012:subnet/subnet-0d750adbd139f411d"
    assign_ipv6_address_on_creation = false
    availability_zone               = "ap-northeast-1a"
    availability_zone_id            = "apne1-az4"
    cidr_block                      = "10.0.0.0/24"
    id                              = "subnet-0d750adbd139f411d"
    map_public_ip_on_launch         = false
    owner_id                        = "314472643515"
    tags                            = {}
    vpc_id                          = "vpc-0f24baede5950eb2d"

    timeouts {}
}

tfstate-lookup を使う

ecspresso で tfstate 参照を実現するためのパッケージが github.com/fujiwara/tfstate-lookup/tfstate です。

https://github.com/fujiwara/tfstate-lookup

tfstate-lookup コマンドを使うことで、tfstate 内のリソースの一覧と参照が可能です。

ecspresso が実際に利用しているのは terraform state ではなく tfstate-lookup/tfstate なので、こちらを使用するほうが実際にテンプレートで展開される要素を確実に確認できます。また、tfstate を読み込むことだけに特化している分、より高速です。

$ tfstate-lookup -s terraform.tfstate
data.aws_security_group.http
aws_lb_target_group.ecspresso-demo
aws_subnet.az-a
aws_subnet.az-c
aws_vpc.ecspresso-demo
$ tfstate-lookup -s terraform.tfstate aws_subnet.az-a
{
  "arn": "arn:aws:ec2:ap-northeast-1:123456789012:subnet/subnet-0d750adbd139f411d",
  "assign_ipv6_address_on_creation": false,
  "availability_zone": "ap-northeast-1a",
  "availability_zone_id": "apne1-az4",
  "cidr_block": "10.0.0.0/24",
  "id": "subnet-0d750adbd139f411d",
  "ipv6_cidr_block": "",
  "ipv6_cidr_block_association_id": "",
  "map_public_ip_on_launch": false,
  "outpost_arn": "",
  "owner_id": "123456789012",
  "tags": {},
  "timeouts": {
    "create": null,
    "delete": null
  },
  "vpc_id": "vpc-0f24baede5950eb2d"
}

速度の比較 (Terraform v0.14.2, tfstate-lookup v0.0.14)

$ time terraform state list -state=terraform.tfstate > /dev/null
real    0m0.811s
user    0m0.742s
sys     0m0.158s

$ time tfstate-lookup -s terraform.tfstate > /dev/null
real    0m0.019s
user    0m0.018s
sys     0m0.010s

20日目は、タスク定義とサービス定義を Jsonnet というテンプレート言語で生成する方法を説明します。

https://zenn.dev/fujiwara/articles/ecspresso-20201220

Discussion