Appleが開発した言語Pklを使ってTerraformコードを生成しAWS環境を構築してみた

# Pklって何？

恐らくこういう反応の方が大半なのではないでしょうか。

Pkl（読み方はピックルらしい）とは、Appleが開発した**設定ファイル（JSONやYAMLなど）に特化したプログラミング言語**です。
2024年2月に発表されましたが、ぶっちゃけ全然注目されていないです。

いったいどんなプログラミング言語で何を目的としているのかというと、公式では下記のように語られています。

> 構成について考えるとき、JSON、YAML、プロパティ リストなどの静的言語を考えるのが一般的です。これらの言語にはそれぞれ独自のメリットがありますが、構成が複雑になると不十分になる傾向があります。たとえば、表現力が不足しているため、コードが繰り返されることがよくあります。さらに、これらの形式では独自の検証が提供されないため、構成エラーが発生しやすくなります。
> ...（中略）...
>
> Pkl を作成したのは、構成は静的言語と汎用プログラミング言語の融合として表現するのが最適だと考えたからです。私たちは両方の長所を取り入れ、宣言的で読み書きが簡単でありながら、汎用言語から借りた機能で強化された言語を提供したいと考えています。Pkl を作成するときは、クラス、関数、条件文、ループなど、期待される言語機能を使用できます。抽象化レイヤーを構築し、パッケージを作成して公開することでコードを共有できます。最も重要なのは、Pkl を使用してさまざまな種類の構成ニーズを満たすことができることです。

ようは「JSONとかYAMLを書いている時に、何度も同じようなことを繰り返し書いたり、ちょっとした記述ミスでうまく動かなかったりするのってだるくない？　Pkl使えばそういう問題防げるようになるよ」ということですね。

最近GitHubActionsやSAM（AWS Serverless Application Model）の学習で長めのYAMLを書く機会があり、確かに結構だるいなと思う場面が多かったです。

もしPklがそれらの面倒臭さを破壊できるほどの力があるなら、これは結構面白いことになるんじゃないか？　と思ったのがきっかけで、Pklに触ってみることにしました。

# どうせならTerraformコード(HCL)生成してえ

AWSの色々な機能を学習していこうとすると、どうしても様々なリソースを作っては壊してという作業が必要になります。作りっぱなしでもいいっちゃいいですが、余計なお金がかかってしまうので、なるべく最小限に抑えたいですよね。

となると、作っては壊してを毎回手動 or CLIでやるのも面倒というもの。SAMならCloudFormationでリソースを定義・破棄することが出来ますが、Terraformの方がAWSに限定されずあれこれいじれるので、どうせならTerraformで管理したい。

しかしTerraformコードをAWSの各機能毎に書いていくのは大変そう……そうだ、Pklだ！ Pklがあるじゃないか！
PklならTerraformコードも楽々書けるようになるのでは……？（果てしない願望

そんな祈り（？）から、Pklを使ってTerraformコード（HCL）を生成し、AWS環境を構築・破棄できるようにしてみようというチャレンジが始まりました。

# しかしPklはHCL非対応だったのでTerraformコード(JSON)に変更

よくよく確認してみると、Pklの出力可能な設定ファイルにHCL（Terraformの構成言語）はありませんでした。なんてこったい。

いきなり夢破れたかと思ったのですが、TerraformはJSONをサポートしてくれているではないですか。

つまりPklでTerraform（JSON）コードを生成すれば、AWS環境を構築・破棄できるようになるはず……！

というわけで改めて、Pklを使ってTerraformコード（JSON）を生成し、AWS環境を構築・破棄できるようにしてみようというチャレンジの結果がこちら。

# チャレンジ結果

きちんとTerraform（JSON）コードの生成、およびAWS環境を構築・破棄できるようになりました。

[Terraformコードを生成するPklリポジトリ](https://github.com/KouYuume/tapkl)

## 利用方法

1. PklでAWSの環境設定を記述する（後述）

2. Terraformの初期化実行
   ```bash
   docker compose run --rm terraform init
   ```

3. PklによるTerraformコード（JSON）生成と`terraform apply`の実行をまとめたシェルスクリプト実行
   ```bash
   sh apply.sh
   ```

4. 必要に応じて環境破棄
   ```bash
   docker compose run --rm terraform destroy
   ```

## PklによるAWS環境設定（例）

```pkl
import ".../packages/aws/vpc/VPC.pkl"

vpc: VPC = new {
  tfName = "kaza_test_dev_vpc"
  cidr_block = "10.0.0.0/16"
  tags {
    ["Name"] = "kaza_test_dev_vpc"
  }
}
```

```pkl
import "vpc.pkl"
import ".../packages/aws/vpc/ElasticIP.pkl"
import ".../packages/aws/vpc/InternetGateway.pkl"
import ".../packages/aws/vpc/NATGateway.pkl"
import ".../packages/aws/vpc/Subnet.pkl"

subnet: Subnet = new {
  vpc_id = vpc.vpc.id
  tfName = "kaza_test_public_a"
  cidr_block = "10.0.1.0/24"
  availability_zone = "ap-northeast-1a"
  map_public_ip_on_launch = true
  tags {
    ["Name"] = "kaza_test_pub_a"
  }
}

eip: ElasticIP = new {
  tfName = "kaza_test_ngw_pub_a"
  vpc = true
  tags {
    ["Name"] = "kaza_test_ngw_pub_a"
  }
}

igw: InternetGateway = new {
  vpc_id = vpc.vpc.id
  tfName = "kaza_test_igw"
  tags {
    ["Name"] = "kaza_test_igw"
  }
}

natGateway: NATGateway = new {
  tfName = "kaza_test_nat_gw"
  allocation_id = eip.id
  subnet_id = subnet.id
}
```

## 実行結果（生成されるJSONファイル）

```json
{
  "terraform": {
    "required_version": "~> 1.9.0",
    "required_providers": {
      "aws": {
        "source": "hashicorp/aws",
        "version": "~> 5.70.0"
      }
    }
  },
  "provider": {
    "aws": {
      "region": "ap-northeast-1",
    }
  },
  "resource": {
    "aws_subnet": {
      "kaza_test_public_a": {
        "vpc_id": "${aws_vpc.kaza_test_dev_vpc.id}",
        "cidr_block": "10.0.1.0/24",
        "availability_zone": "ap-northeast-1a",
        "map_public_ip_on_launch": true,
        "tags": {
          "Name": "kaza_test_pub_a"
        }
      }
    },
    "aws_eip": {
      "kaza_test_ngw_pub_a": {
        "vpc": true,
        "tags": {
          "Name": "kaza_test_ngw_pub_a"
        }
      }
    },
    "aws_internet_gateway": {
      "kaza_test_igw": {
        "vpc_id": "${aws_vpc.kaza_test_dev_vpc.id}",
        "tags": {
          "Name": "kaza_test_igw"
        }
      }
    },
    "aws_nat_gateway": {
      "kaza_test_nat_gw": {
        "allocation_id": "${aws_eip.kaza_test_ngw_pub_a.id}",
        "subnet_id": "${aws_subnet.kaza_test_public_a.id}",
        "tags": {
          "Name": "kaza_test_nat_gw"
        }
      }
    },
    "aws_vpc": {
      "kaza_test_dev_vpc": {
        "cidr_block": "10.0.0.0/16",
        "tags": {
          "Name": "kaza_test_dev_vpc"
        }
      }
    }
  }
}
```

# 使ってみた感想

ぶっちゃけ**いまいち**ですね……。

確かにPklの場合、値に制約をかけられるので、間違った記述を減らすことは出来ます。

```pkl
// こんな感じで、型宣言の後ろに制約をつけられる
typealias CIDRBlock = String(
    this.matches(
      Regex(#"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}$"#)
    )
  )
```

ただ、AIが広く使われるようになった昨今、AIにチェックさせれば問題箇所はすぐ特定できる可能性があるので、制約機能の価値は昔ほど大きくはないかなと感じています。あればもちろん嬉しいは嬉しいですが。

また、Pklでの記述とTerraform（HCL）の記述を見比べてみるとわかるのですが、Pklの方が若干記述量が多い。

もちろん、Pklの場合はテンプレートとして記述したプロパティファイルの一部を書き換えたり、共通のプロパティを簡略的に記述したり、ループ処理で動的に生成したりできるので、色々工夫すれば最終的な記述量は減らせそうだとは思います。

```pkl
// a.pkl
id: Number = 1
name: String = "hoge"

//b.pkl
amends "a.pkl" //amendsを使うと、別の設定ファイル（今回でいえばa.pkl）のオーバーライドができる

name = "fuga" //指定した設定項目だけ上書き
```

```pkl
values: Listing<ValueProperty> = new {
  default {
    name = "hoge" //リストの各オブジェクトが持つ共通のプロパティ値を定義
  }
  {
    id = 1
  }
  {
    id = 2
  }
}


class ValueProperty {
  id: Number
  name: String
}
```

しかしながらPklの言語仕様が言うほど汎用的ではなく、かなり独特なので、学習コストがわりとかかる。HCLの方が正直わかりやすい。

今回のTerraformコードに限らず、設定ファイル（JSONやYAML）は簡単なルールに則って素早く書けるのがメリットだと思うので、それらの有効な代替になるほどの力はない、というのが現時点の評価かなと感じました。

公式が提供している[KubernetesのSampleリポジトリ](https://github.com/apple/pkl-k8s-examples)などを確認すると、yamlをpklに変換して再評価することで問題を検知する運用が紹介されています。設定ファイルのlinter的な運用であれば活躍する場面もあるのかもしれません。

もうちょっとシンプルに書けたり、簡潔に書けるテンプレートなどが提供されれば、便利そうではあるんですが……。

# まとめ

- Pklを使ってTerraformコード（JSON）を生成し、AWS環境を構築することはできた
- しかしながらPklの言語仕様に対する学習コストが高いこと、チェック機能に対する恩恵が昨今の情勢を鑑みても薄い（代替手段が多い）ことから、期待していたほどの効果は得られなかった
- 設定ファイルのlinter的な運用が簡単にできるようになるのであれば価値は出てくるかもだが、まだ発展途上なのもあり、今後次第（ただ全く流行していないので、このまま廃れる可能性も高い）

Pkl × Terraformも、優秀なテンプレートが構築されれば、「堅牢な制約機能により安全な生成が可能になる」というPklが掲げている理念を確かに実現出来そうな気はします。ただ、やはり学習コストが問題ですね。

ListとListing、MapとMappingという何が違うのかパッとは理解し辛い概念が存在していたり、Moduleの概念・使い方が独特であったり、言語仕様が未成熟（現時点でver0.27）なためprotectedなどの機能はまだ利用できなかったり。

また、[Terraform（JSON）もHCLにはない問題](https://github.com/hashicorp/terraform-provider-aws/issues/8786#issuecomment-496935442)があって生成に一手間必要な場面もあったりして、結構大変でした。とはいえ、[Pkl自体はHCLの出力をサポートする予定はない](https://github.com/apple/pkl/issues/488)ようなので、Pkl × Terraformについてはこの方向性しかない感じではありますが。

Pkl採用に対する懸念は下記の記事が的確だったのでご紹介しておきます。

[Pkl: Apple's New Configuration Language That Could Replace JSON and YAML](https://www.trevorlasn.com/blog/pkl-apple-new-configuration-language)

とまあ、結果としてはあまり芳しくないものとなってしまったんですが、「こんなことできるかも！？」を期待して新しい言語をあれこれいじってみるのは結構楽しかったです。

今後Pkl触ってみようかなと思う人がいれば、メリット・デメリットの参考になれば幸いです。

# 余談

同じようなことを考えた人がいたみたいで、GitHubActionsとCloudFormationについてはリポジトリを公開している人がいました。

[pkl × GitHubActions](https://github.com/StefMa/pkl-gha/)

[pkl × CloudFormation](https://github.com/aws-cloudformation/cloudformation-pkl)