Terraform v0.8.0-rc3から任意の言語でプロバイダ/データソースを記述できる「external」という機能が導入されます

2016.12.09

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは、中山です。

Terraformの時期RCリリースバージョン0.8.0-rc3で面白そうな機能がマージされました。 external という機能です。これはこちらのPRでマージされたコミットになります。早速使ってみたので本エントリにまとめます。

機能概要

現在(2016/12/09)のところドキュメントは以下のようにプロバイダ/データソース毎に用意されています。

この「external」という機能を一言で説明すると「外部プログラムを実行してその実行結果をTerraformのプロバイダ/データソースとして利用できる機能」となります。今までプロバイダ/データソースを作成するにはGo言語でコードを記述する必要がありました。それが、この「external」が導入されたことにより特定の出力フォーマットを満たせば任意の言語で記述できるようになったという点が大きなアップデートだと思います。

「特定の出力フォーマット」とはどういったものになるでしょうか。サンプルシェルスクリプトがドキュメントに記載されていたので、そのスクリプトに基づき解説します。今回はシェルスクリプトのサンプルをご紹介しますが、この形式に基づいていればもちろん別の言語でも実装可能です。

#!/bin/bash

# Exit if any of the intermediate steps fail
set -e

# Extract "foo" and "baz" arguments from the input into
# FOO and BAZ shell variables.
# jq will ensure that the values are properly quoted
# and escaped for consumption by the shell.
eval "$(jq -r '@sh "FOO=\(.foo) BAZ=\(.baz)"')"

# Placeholder for whatever data-fetching logic your script implements
FOOBAZ="$FOO BAZ"

# Safely produce a JSON object containing the result value.
# jq will ensure that the value is properly quoted
# and escaped to produce a valid JSON string.
jq -n --arg foobaz "$FOOBAZ" '{"foobaz":$foobaz}'

重要な箇所をハイライトしておきました。それぞれ以下の内容を実施する処理になります。

  • 4行名: -e オプションを利用することにより終了ステータスコードが0以外の場合、即座にシェルスクリプトを停止する
    • また、エラーは標準エラー出力に表示されるのでTerraformのエラーとして表示される
  • 10行名: 標準入力から渡された foo baz をそれぞれ FOO BAZ 変数に格納する
    • また、 @sh を利用してPOSIX用にエスケープする
  • 18行目: 結果を {"foobaz": "<変数foobazの値>"} という正常なJSON形式で標準出力に表示させる

シェルスクリプトでexternal機能を利用する場合は上記のように jq コマンドの使用が推奨されています。

注意点

先程のドキュメントにも記載されていましたが、一件良いこと尽くめに見えるこの機能、利用する上で幾つか注意点があります。お手軽にプロバイダ/データソースを記述できる機能ですが、やはりGo言語で作成するのが本筋です。そのため、利用される際には以下の点にご注意ください。

1. Terraformのポータビリティが低減する

外部プログラムが実行されるのは当然Terraformのコードを実行するOS上になります。外部プログラムは指定された出力フォーマットを満たしていればどの言語でも作成可能なため、そのプログラムを実行するOSによって実行結果が異なる場合があります。そのため、標準で用意されているデータソースと比較するとポータビリティが低減し、あるOSに依存したTerraformのコードになる可能性があります。

2. Terraform Enterpriseでの注意点

個人的にTerraform Enterpriseは利用した経験がないのですが、やはり有償サービスのためか注意点が書かれています。以下に引用します。

~> Warning Terraform Enterprise does not guarantee availability of any particular language runtimes or external programs beyond standard shell utilities, so it is not recommended to use this data source within configurations that are applied within Terraform Enterprise.

Terraform Enterpriseでこの機能をご利用される際はご注意ください。

インストール方法

0.8.0-rc3は現時点ではまだリリースされていません。そのため利用する場合は以下のコマンドを実行して自分でTerraformをコンパイルしてください。

$ go get github.com/hashicorp/terraform
$ cd $GOPATH/src/github.com/hashicorp/terraform
$ git checkout e772b45970e1fab679ee85d017ae563880e25714
$ git log -1
commit e772b45970e1fab679ee85d017ae563880e25714
Author: Martin Atkins <mart@degeneration.co.uk>
Date:   Mon Dec 5 09:24:57 2016 -0800

    "external" data source, for integrating with external programs (#8768)

    * "external" provider for gluing in external logic

    This provider will become a bit of glue to help people interface external
    programs with Terraform without writing a full Terraform provider.

    It will be nowhere near as capable as a first-class provider, but is
    intended as a light-touch way to integrate some pre-existing or custom
    system into Terraform.

    * Unit test for the "resourceProvider" utility function

    This small function determines the dependable name of a provider for
    a given resource name and optional provider alias. It's simple but it's
    a key part of how resource nodes get connected to provider nodes so
    worth specifying the intended behavior in the form of a test.

    * Allow a provider to export a resource with the provider's name

    If a provider only implements one resource of each type (managed vs. data)
    then it can be reasonable for the resource names to exactly match the
    provider name, if the provider name is descriptive enough for the
    purpose of the each resource to be obvious.

    * provider/external: data source

    A data source that executes a child process, expecting it to support a
    particular gateway protocol, and exports its result. This can be used as
    a straightforward way to retrieve data from sources that Terraform
    doesn't natively support..

    * website: documentation for the "external" provider
$ make dev
$ $GOPATH/bin/terraform version
Terraform v0.8.0-dev (e772b45970e1fab679ee85d017ae563880e25714)

使ってみる

それでは早速使ってみましょう。今回はデータソースを利用したサンプルコードをご紹介します。内容としてはAWS CLIを利用してドメイン名からHosted Zone IDを取得し、そのIDからリソースレコードセットをアウトプットさせるという単純なものです。外部プログラムはbashで作成しています。ちなみに外部プログラムの実行権限の有無は結果に関係ありません。どちらでもOKです。

list-hosted-zone-by-name.sh

#!/usr/bin/env bash

set -e
eval "$(jq -r '@sh "DOMAIN=\(.domain)"')"

hosted_zone_id="$(aws route53 list-hosted-zones-by-name \
  --dns-name "$DOMAIN" \
  --query 'HostedZones[].Id' \
  --output text)"

jq -n --arg "hosted_zone_id" "$hosted_zone_id" '{"HostedZoneId": $hosted_zone_id}'

Hosted Zone IDを取得するためのスクリプトです。内容はほとんどサンプルスクリプトと同じです。スクリプト単体で実行すると以下のような結果になります。

$ echo '{"domain": "***************************"}' | ./list-hosted-zone-by-name.sh
{
  "HostedZoneId": "/hostedzone/*************"
}

list-resource-record-sets.sh

#!/usr/bin/env bash

set -e
eval "$(jq -r '@sh "HOSTED_ZONE_ID=\(.hosted_zone_id)"')"

resource_record_sets="$(aws route53 list-resource-record-sets \
  --hosted-zone-id "$HOSTED_ZONE_ID")"

jq -n --arg "resource_record_sets" "$resource_record_sets" '{"Outputs": $resource_record_sets}'

Hosted Zone IDからリソースレコードセットを表示させるためのスクリプトです。こちらも同様ほぼ同じ処理をしているだけですね。スクリプト単体で実行すると以下のような結果になります。

$ echo '{"hosted_zone_id": "*************"}' | ./list-resource-record-sets.sh
{
  "Outputs": "{\n    \"ResourceRecordSets\": [\n        {\n            \"ResourceRecords\": [\n                {\n                    \"Value\": \"ns-***.awsdns-**.com.\"
<snip>

main.tf

variable "domain" {}

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

data "external" "hosted_zone_id" {
  program = ["bash", "${path.module}/list-hosted-zone-by-name.sh"]

  query = {
    domain = "${var.domain}"
  }
}

data "external" "resource_record_sets" {
  program = ["bash", "${path.module}/list-resource-record-sets.sh"]

  query = {
    hosted_zone_id = "${replace(data.external.hosted_zone_id.result["HostedZoneId"], "//hostedzone//", "")}"
  }
}

output "resource_record_sets" {
  value = "${data.external.resource_record_sets.result}"
}

externalデータソースを2回利用しています。一回目が list-hosted-zone-by-name.sh を呼び出してドメインからHosted Zone IDを取得しています。 query{"domain": "<変数var.domainの値>"} という形式でスクリプトの標準入力に渡されます。二回目の呼び出しで先程の実行結果を引数に渡してリソースレコードセットを取得しています。最後、 output で取得したリソースレコードセットを全て表示させています。実行結果は以下のとおりです。注意点として、 domain はtfvarsで定義しているのでご利用する際は -var-file オプションで指定してください。

$ $GOPATH/bin/terraform apply -var-file=secrets.tfvars
data.external.hosted_zone_id: Refreshing state...
data.external.resource_record_sets: Refreshing state...

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

resource_record_sets = {
  Outputs = {
    "ResourceRecordSets": [
        {
            "ResourceRecords": [
                {
                    "Value": "ns-***.awsdns-**.com."
                },
                {
                    "Value": "ns-***.awsdns-**.net."
                },
                {
                    "Value": "ns-****.awsdns-**.co.uk."
                },
                {
                    "Value": "ns-****.awsdns-**.org."
                }
            ],
            "Type": "NS",
            "Name": "***************************",
            "TTL": 172800
        },
        {
            "ResourceRecords": [
                {
                    "Value": "ns-***.awsdns-**.com. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400"
                }
            ],
            "Type": "SOA",
            "Name": "***************************",
            "TTL": 900
        },
<snip>

まとめ

いかがだったでしょうか。

0.8.0-rc3で導入予定の「external」というちょっと毛色の異なる機能をご紹介しました。今まで以上にプロバイダ/データソースを簡単に作れる反面、注意点もある機能になります。実現したい目標がこの機能で実装すべきかよく見極めてからご利用されることをオススメします。

本エントリがみなさんの参考になったら幸いに思います。