NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

Terraformで複雑な条件分岐を実現する方法

こんにちは、後藤です。
今回もTerraformについての内容で、条件分岐についてのお話です。
Terraformは現時点で一般的なプログラミング言語のif 文をサポートしていません。しかし、条件によって異なる値を設定する方法は存在し、代表的な方法として三項演算子が挙げられます。
当記事では前半で三項演算子のおさらいをして、後半に外部スクリプト実行によって条件分岐するという方法を紹介します。

三項演算子を使った条件分岐

三項演算子を使うことでif文のような条件分岐が可能です。文法は<条件> ? <trueの時の値> : <falseの時の値>の書き方になります。

variable "env" {
  default = "dev"
}

resource "aws_instance" "example" {
  ami           = "ami-xxxxx"
  instance_type = var.env == "prod" ? "t3.large" : "t3.micro"
  subnet_id     = "subnet--xxxxx"
}

この例ではvariable "env"の値に応じてインスタンスタイプが変わるようになっており、prodであればt3.large、それ以外であればt2.microになります。

またcount句を組み込んで三項演算子を利用することで、条件に応じたリソースの作成を制御できます。例えば、特定の条件が真であればリソースを作成し、偽であれば作成しないといった動作が可能です。

variable "create_instance" {
  default = false
}

resource "aws_instance" "example" {
  count = var.create_instance ? 1 : 0
  ami           = "ami-xxxxx"
  instance_type = "t2.micro"
  subnet_id     = "subnet--xxxxx"
}

この例ではvar.create_instancetrueのときにEC2インスタンスが作成され、falseのときには作成されません。

上記のように三項演算子とcount句を利用すれば簡単な条件分岐は可能です。しかし、複数の条件を組み合わせた柔軟な条件分岐や外部データやAPIの結果に基づく動的な判定までは不可能です。 また、resourceブロックの中で実装しなければならないので、少し自由度が低いように思います。

外部スクリプトによって条件分岐する方法

そこで、data "external"を使った、外部スクリプトによって条件分岐をする方法を紹介します。
data "external" は外部スクリプトを実行でき、その結果を JSON 形式で受け取って変数として格納しておくことができます。このスクリプト内で条件分岐を実装すれば、この方法でも実現できます。
data "external" の使い方は以下のようにprogram句にスクリプト実行のコマンドを書くだけです。

data "external" "example" {
  program = ["python3", "${path.module}/script.py"]
}

terraform plan実行時にスクリプト実行されるので、スクリプトによってデータを取得して、その値を使ってAWSリソースを設定することができます。

外部スクリプトを使ったいくつかの使用例

1.上記のインスタンスタイプを決める条件分岐を、外部スクリプトで実装した例
variable "env" {
  default = "prod"
}

data "external" "instance_type" {
  program = ["python3", "${path.module}/get_instance_type.py"]
  query = {
    env = var.env
  }
}

resource "aws_instance" "example" {
  ami           = "ami-xxxxx"
  instance_type = data.external.instance_type.result["instance_type"]
  subnet_id     = "subnet-xxxxx"
}

get_instance_type.py

import json
import sys

# Terraformからの入力を取得
input_data = json.load(sys.stdin)

# 環境変数 "env" に応じた instance_type を決定。デフォルト値は "dev"
env = input_data.get("env", "dev") 
instance_type = {"prod": "t3.large"}.get(env, "t3.micro")

# 結果をJSON形式で出力
print(json.dumps({"instance_type": instance_type}))

このケースではvariable "env"の値をpythonスクリプトが受け取り、スクリプト内でインスタンスタイプを判定しています。

2.セキュリティ機能として使う例

Terraform実行するマシンのIPアドレスやhost名をチェックし、想定通りでなければスクリプトを終了させます。スクリプト終了によってTerraform実行はエラーになります。

data "external" "validate_host" {
  program = ["/bin/sh", "${path.module}/validate_host.sh"]
}

output "validate_host" {
  value = data.external.validate_host.result["validate_statas"]
}

validate_host.sh

#!/bin/bash

# 想定するIPアドレスとホスト名
EXPECTED_IP="10.10.10.10"
EXPECTED_HOSTNAME="example-host"

# 現在のマシンのIPアドレスとホスト名を取得
CURRENT_IP=$(hostname -I | awk '{print $1}')
CURRENT_HOSTNAME=$(hostname)

# 実行環境が想定と一致するかチェック
if [[ "$CURRENT_IP" != "$EXPECTED_IP" || "$CURRENT_HOSTNAME" != "$EXPECTED_HOSTNAME" ]]; then
  echo '{"validate_status": "Invalid environment"}'
  exit 1
fi

echo '{"validate_status": "OK"}'

このケースではシェルスクリプトを実行しています。 今回は簡単な例として紹介しましたが、if文をもっと作りこめばセキュリティの向上に役立ちそうです。

3.条件分岐ではなく、ロジックを盛り込んだ外部スクリプト実行例

特定のVPCにあるプライベートサブネットのうち、空きIPアドレスが最も多いサブネットを選択し、そのサブネットにEC2を作成しています。

data "external" "get_subnet" {
  program = ["python3", "${path.module}/get_subnet.py"]

}

resource "aws_instance" "dev-instance" {
  ami           = "ami-xxxxx"
  instance_type = "t2.micro"
  subnet_id     = data.external.get_subnet.result["subnet_id"]
}

get_subnet.py

import boto3

def fetch_data():
    ec2 = boto3.client('ec2', region_name='ap-northeast-1')

    vpc_id = 'vpc-xxxxxxxxxxxxxx'

    # VPC内のプライベートサブネットを取得
    subnets = ec2.describe_subnets(Filters=[
        {'Name': 'vpc-id', 'Values': [vpc_id]},
        {'Name': 'availability-zone', 'Values': ['ap-northeast-1a', 'ap-northeast-1c', 'ap-northeast-1d']},
        {'Name': 'map-public-ip-on-launch', 'Values': ['false']}
    ])['Subnets']

    # 空きIPアドレスが最も多いプライベートサブネットを取得
    max_available_ips = 0
    best_subnet_id = None
    for subnet in subnets:
        available_ips = subnet['AvailableIpAddressCount']
        if available_ips > max_available_ips:
            max_available_ips = available_ips
            best_subnet_id = subnet['SubnetId']
    return {'subnet_id': best_subnet_id}
    
if __name__ == "__main__":
    result = fetch_data()
    print(json.dumps(result))

このケースのようにロジックを盛り込んで設定値を決定することもできます。

注意点

そこまで致命的ではないですが、いくつかの注意点を書いておきます。
data "external"は実行結果をJSON形式で受け取らないといけないので、スクリプトを作る際は覚えておきましょう。
・スクリプトの実行時間が長くなると、もちろんTerraform実行時間にも影響します。

さいごに

HCL(Terraformを扱うための言語)の外で、別の言語を使って動的参照を行う方法を紹介しました。
data "external"を活用することでTerraform単体では難しい複雑な条件分岐を実装できます。特に、外部APIやローカルマシンの情報を利用できるのは非常に便利かと思います。この記事がお役に立てば幸いです。

執筆者: 後藤 涼太
AWSをメインとするクラウドエンジニア
2024 Japan AWS All Certifications Engineers
執筆記事:https://tech.nri-net.com/archive/author/r-goto