ecspresso と lambroll で tfstate からの値参照に対応した

Amazon ECS デプロイツール ecspressoAWS Lambad デプロイツール Lambroll で、設定ファイル中に tfstate (Terraform state file)の値を検索して使えるようにしました。

これまで面倒だったところ

ecspresso ではサービスとタスク定義、lambroll では関数定義を JSON で用意します。 その JSON の中では {{ env "FOO" }} のような記法で環境変数を展開してから読み込む機能があります。

デプロイや環境ごとに可変になる部分を、デプロイ時に環境変数によって外から与えることで定義ファイルをいちいち更新しなくていいという作りになっています。

しかし、たとえば ecspresso のサービス定義には以下のように、subnet, security group, LB target group などの ID が必要になります。

{
  "launchType": "FARGATE",
  "networkConfiguration": {
    "awsvpcConfiguration": {
      "subnets": [
        "subnet-03476ecc408d3e5e9",
        "subnet-027dd89f0c35a6323"
      ],
      "securityGroups": [
        "sg-0feded3d26f6360e1"
      ]
    }
  },
  "loadBalancers": [
    {
      "containerName": "app",
      "containerPort": 80,
      "targetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:0123456789012:targetgroup/app/d8b8346d68b86bf5"
    }
  ]
}

これらのリソースの管理は ecspresso / lambroll の管轄外のため、ファイルにべた書きすると謎の IDになってしまいます。

別途なんらかの方法で環境変数に設定してもいいのですが、個人的にはこれらのリソースは Terraform で管理しているので、Terraform のリソース名を参照できると便利なのでは!と思い立ち、実装してみました。

Terraform は、リソース定義に使用する .tf ファイルと実体のリソースを結びつける中間状態を terraform.tfstate という JSON で管理しています。State - Terraform by HashiCorp

このファイルを検索すれば、リソース名から実体のIDを検索できます。

tfstate 参照でどのよう変わるか

ecspresso

ecspresso (v0.14.0〜) で設定ファイルに plugins で tfstate の読み込み先を追加します。

terraform.tfstate はローカルファイルのほか、S3 の remote state にも対応しています (.terraform/terraform.tfstate に保存されているもの)

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

これで、サービス定義を以下のように記述できます。

{
  "launchType": "FARGATE",
  "networkConfiguration": {
    "awsvpcConfiguration": {
      "subnets": [
        "{{ tfstate `aws_subnet.az-a.id` }}",
        "{{ tfstate `aws_subnet.az-b.id` }}"
      ],
      "securityGroups": [
        "{{ tfstate `aws_security_group.default.id` }}"
      ]
    }
  },
  "loadBalancers": [
    {
      "containerName": "app",
      "containerPort": 80,
      "targetGroupArn": "{{ tfstate `aws_lb_target_group.app.arn` }}"
    }
  ]
}

対応する terraform の .tf ファイルはたとえば以下のように記述されていて、これを terraform apply すると実体のリソースが作成/更新され、tfstate には実体のリソースが持っているIDなどの属性が保存されています。

resource "aws_subnet" "az-a" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.0.1/24"
  availability_zone = "ap-northeast-1a"

  tags = {
    Name = "az-a"
  }
}

リポジトリで人間が管理するファイルには謎の ID が出現しなくなりました。

lambroll

lambroll (v0.5.0〜) では、実行時に --tfstate オプションで tfstate のファイルのパスを指定します。

{
  "FunctionName": "hello",
  "Handler": "index.handler",
  "MemorySize": 128,
  "Role": "{{ tfstate `aws_iam_role.lambda.arn` }}",
  "Runtime": "nodejs12.x",
  "Timeout": 3,
  "TracingConfig": {
    "Mode": "PassThrough"
  }
}
$ lambroll --tfstate terraform.tfstate deploy --log-level debug --dry-run
2020/04/02 11:37:41 [info] lambroll v0.5.1
2020/04/02 11:37:41 [info] starting deploy function hello2
2020/04/02 11:37:41 [info] creating zip archive from .
2020/04/02 11:37:41 [debug] -rw-rw-r--        145 2019-10-28T09:43:31Z index.js
2020/04/02 11:37:41 [info] zip archive wrote 278 bytes
2020/04/02 11:37:41 [info] updating function configuration **DRY RUN**
2020/04/02 11:37:41 [debug]
{
  FunctionName: "hello2",
  Handler: "index.handler",
  MemorySize: 128,
  Role: "arn:aws:iam::0123456789012:role/hello_lambda_role",
  Runtime: "nodejs12.x",
  Timeout: 3,
  TracingConfig: {
    Mode: "PassThrough"
  }
}

使っている技術

tfstate を読むために、tfstate-lookup というパッケージ/コマンドを作りました。

github.com

コマンドとして使うと tfstate からリソース名 (+属性名) で値を抽出できますし、Go のパッケージとしても利用できます。

$ tfstate-lookup -s .terraform/terraform.tfstate aws_vpc.main.id
vpc-1a2b3c4d

$ tfstate-lookup aws_vpc.main
{
  "arn": "arn:aws:ec2:ap-northeast-1:123456789012:vpc/vpc-1a2b3c4d",
  "assign_generated_ipv6_cidr_block": false,
  "cidr_block": "10.0.0.0/16",
  "default_network_acl_id": "acl-001234567890abcde",
  "default_route_table_id": "rtb-001234567890abcde",
  "default_security_group_id": "sg-01234567890abcdef",
  "dhcp_options_id": "dopt-64569903",
  "enable_classiclink": false,
  "enable_classiclink_dns_support": false,
  "enable_dns_hostnames": true,
  "enable_dns_support": true,
  "id": "vpc-1a2b3c4d",
  "instance_tenancy": "default",
  "ipv6_association_id": "",
  "ipv6_cidr_block": "",
  "main_route_table_id": "rtb-001234567890abcde",
  "owner_id": "123456789012",
  "tags": {
    "Name": "main"
  }
}

実は terraform 単体でも output を定義すれば terraform output ... で表示はできるのですが、検索したいリソースについていちいち output を定義する必要があってだいぶ面倒だったので、自前で tfstate を parse するコードになっています。

内部では go-jq を使って、クエリを組み立てて json を検索するだけ、というシンプルな作りです。go-jq 便利! github.com

まとめ

ecspresso v0.14、lambroll v0.5 で、設定ファイルから tfstate の値を参照できるようになりました。

Terraform で AWS のリソースを管理しつつ、ECS や Lambda の管理には ecspresso / lambroll を利用している、という環境では便利かと思います。どうぞご利用ください。