tech.guitarrapc.cóm

Technical updates

Pulumi の Project と Stack の構成

この記事は、Pulumi dotnet Advent Calendar 2019 の12日目です。

qiita.com

Pulumi には、Project と Stack があります。 この構成どうするのがコンセプト的にはいいのか見てみます。

目次

TL;DR

Stack は、あくまでもその環境の開発状態にすぎない。 そのため、development, staging, production や feature-x-dev、jane-feature-x-dev のような切り方になる。

Stack を使って構成要素を分離するような持ち方は想定されていないので気を付ける

継続して更新予定。

project:stack を 1:n にしない

https://www.pulumi.com/docs/intro/concepts/stack/

Project に sandbox、Stackにeks、ecs、network のように要素ごとに分離しているから stack を分けるという使い方は想定されていない。

DO

projectに、aws-sandbox、stack に master / dev / staging のような開発状態を持つ

DO NOT

  • projectをaws-sandbox などクラウド-アカウント にして、stackに eks-cluster などより細かい粒度にすることはしない
  • project は、クラウドと一致 (aws など)、stack はそのクラウド環境で独立した単位を用いることもしない

Pulumi がTerraform と比較したときに困ったこと

この記事は、Pulumi dotnet Advent Calendar 2019 の11日目です。

qiita.com

Terraform に慣れていると Pulumiもイメージしやすいところはあります。 一方で Terraform との違いでどうすればいいのかな? となることもあります。

どんなことがあるのか見ます。

目次

TL;DR

Pulumiは Pulumi のコンセプトを持っているので、Terraform と全く同じ挙動を期待するのはずれている。

Terraform でこうやっていた経験が Pulumi で生きないということに傾向はある、のでそれをまとめる。

継続して更新予定。

Pulumi で困ったこと

TL;DR と Summary 形式で紹介する。

State の操作時に Component を消すにはResource を先に削除をしておく必要がある

TL;DR

Component != Module

Summary

Terraform で、リソースを取りまとめるときには Module を利用する。 Moduleとその中のResource を丸っとState から消すときは、 terraform state rm module.xxxx とModuleを指定できる。 いちいち リソースを消したりしないで済むので、Module に取りまとめておくことでstate 操作時にリソースのグルーピング対応が可能である。

Pulumi で、リソースを取りまとめるときには ComponentResource を利用する。 ComponentResource とその中の Resource を丸っと State から消そうと思っても、pulumi state delete urn:pulumi:STACK::PROJECT::PARENT_COMPONENT$COMPONENT::COMPONENT_NAME と ComponentResourceを指定してもリソースが1つでも含まれていると削除できない。 いちいちリソースを削除しないといけないので、ComponentResource をつかってstate操作時のリソースのグルーピング対応ができない。

Pulumi の Auto-naming に沿うことによる構成の制約

TL;DR

terraform と違って自動でランダム文字が付く。

LB など命名が長くなると詰むリソースでは注意 or 無効にする。 ただし無効にしないほうが圧倒的に扱いやすいので、無効にするときはそのことを忘れないように。忘れないように (二度言うということはそういうことです)

Summary

Pulumi の Auto-naming に沿う場合、リソース作成時までリソースの名前は不明である。 ここで困るのが、IAM Policy でリソースを指定する + IAM を ComponentResource に取りまとめる場合である。

対象のリソースAの作成 -> IAM の作成 の場合、リソースA の作成結果をもってIAM を作れるので何も困らない。 しかし、リソースA を操作する リソースB を作成し、そのリソースB の権限にIAM でリソースA を指定する というケースでは、リソースA を作成するまで IAM でどのような指定をすればいいのかが不明となる。

対策は2つ。

  1. こういうリソースはAuto-Naming をやめて固定の名前で対処する.。(ResourceのArgs にある Name などのプロパティを指定するとAuto-naming 無効)
  2. 相互の情報を必要としない IAM Role まで作成をしておいて、IAM Policy の作成、Attachment は両方の情報がそろってから別途行う

2を選んだ場合、IAM の設定が IAM ComponentResourceだけでないところに露出し、追いきれなくなる、追い切れても ComponentResourceのParent の紐づけが苦しいことになる。 そのため、妥当な選択は 1 となる。

Auto-naming に基本的に沿っておけばいいとは思うが、紐づけができる範囲でのゆるっとした利用前提を置くのがいいだろう。

Pulumi の設定を Config で保持する

この記事は、Pulumi dotnet Advent Calendar 2019 の10日目です。

qiita.com

Pulumi は Web UIがあり、そこにはConfig が見えます。 実際にコードでも Config が参照でき、機密情報はConfigに保持して参照することでgit から分離できてよさそうです。 早速見てみましょう。

目次

TL;DR

  • Pulumi は、Config設定をProjectに紐づけて保持できる
  • Config 値はローカルの pulumi.STACK.yaml にも保持される
  • Secretはログインしているユーザーでないと復号できないようになっている
  • 構造化コンフィグがサポートされたのでJSONではこっちを使うとよい

イメージ的には、terraform.tfvars がStackに紐づけて保持され、WebUI や CLI、プログラムアクセスで担保された感じ。

基本的な想定環境

Pulumi CLI の動作は公式ドキュメントを頼りに見ていくことになる。 この公式ドキュメントの記載は動作環境に Bash のような Unix Shell (原文ママ) を想定している。

Most of our command line snippets in the docs are for a Unix shell like bash https://github.com/pulumi/docs/issues/2062#issuecomment-560501274

そのため、Windows 環境のコマンドプロンプトや PowerShell ではコマンドがそのまま動かないことがあるので注意されたい。

CLIでのConfig操作

Pulumi config は、 CLI でサクッと設定、取得ができる。

  • pulumi config コマンドに、setget サブコマンドを組み合わせて、key-value ペアを管理していく

基本的なコマンドは次の通り

  • pulumi config set <key> [value] : key に value の値を保持する
  • pulumi config get <key>: key の値を取得する
  • pulumi config: コンフィグ全部取得。--json でJSON として取得

Key は、[namespace:]key-name という組み合わせで構成されている。namespace は任意で省略して key-name のみも可能。namespace を省略した場合、Pulumiが自動的に現在のプロジェクト名を pulumi.yaml から取得して差し込んでくる。

簡単なkey-value 操作を見てみる。現在のプロジェクトで使うconfig値を設定するなら、シンプルに namespace なしにキーを設定できる。

# foo というキーに bar という値を設定
$ pulumi config set foo bar
$ pulumi config get foo
bar

あるいは aws パッケージで使うaws 環境のリージョンを仕込むときは、 aws: namespace を付けて次のように定義する。

$ pulumi config set aws:region ap-northeast-1
$ pulumi config get aws:region
ap-northeast-1

標準入力を value として受け取ることもできる。(この例は Bash/PowerShell/CMD すべてで同じだが、コマンドによってはパイプラインの違いで結果が変わってくるので注意)

$ echo fuga | pulumi config set hoge
$ pulumi config get hoge

秘密文字は、--secret フラグを付ける。--secret はただ base64をした値ではなく、Stackごとの暗号鍵+VaultごとのSalt で暗号化する。 中を見ようとしても、config一覧では [secret] とマスクされている。 直接keyにアクセスすると値が取れる。

$ pulumi config set --secret dbPassword S3cr37
$ pulumi config
KEY                     VALUE
dbPassword              [secret]
$ pulumi config get dbPassword
S3cr37

秘密文字は、awskms など任意の鍵を使うこともできるので必要なら使ってもいい。

Initializing a Stack with Alternative Encryption - Configuration and Secrets

プログラムでのConfig操作

任意のプログラミング言語でPulumi config を取得できるようにAPIが用意されている。C#で Config操作するときはAPIがいくつか生えているので使い分ける必要がある。

まずは new Config でインスタンスを取得したら、ここからConfigにアクセスできる。

var config = new Pulumi.Config();

// foo はオプショナルに取得。なくてもエラーは出ない。
var optionalFoo = config.Get("foo");

// foo はあるものとして取得。なかったらthrow。
var config.Require("foo");

// Secretの取得。実行時はプレーンテキストになるので取扱いに注意
var config.RequireSecret("dbPassword");

Getは型ごとにも用意されているので、適当に使うといい。

動きの把握はコード見たほうが早い。

https://github.com/pulumi/pulumi/blob/eec14527b17584e9a09b786514d9ae164bdeae61/sdk/dotnet/Pulumi/Config.cs#L63

Secret は実行時に生の値になっているので注意、かつ型的には Output<T> となる。

Web UI での確認

Stack ごとに Config は紐づけて保持される。

Web UI でもConfigは確認できる。 Stack を開くとすぐに表示されている。

また Activity でも一件ごとにConfigを確認できる。

ローカルでの Config値

値を設定すると、ローカルの Pulumi.Stack名.yaml にも保持される。

Secret の場合、暗号化された状態で保持されている。

config:
  foo: bar
  aws-sandbox:dbPassword:
    secure: AAABADU0Y5WFz8kp4BGIsqACA+NTPvKBOA1VQQzartD+QBVyesk=

暗号化されているので、git にコミット自体は可能

構造化されたConfig

前はJSON で保持みたいな記述でしたが、新しくYAML方式な構造が提供された。

# JSON で構造化データの投入
$ pulumi config set data '{"active": true, "nums": [1,2,3]}'

# 新しい構造化方式でデータを投入
$ pulumi config set --path data.active true
$ pulumi config set --path data.nums[0] 1
$ pulumi config set --path data.nums[1] 2
$ pulumi config set --path data.nums[2] 3

最近のCLI でよくある、terraform でもよく見るやり方なので、標準的に使えるのはうれしいところ。

JSONでは投入時に、CMD/PowerShell でエスケープの追加対応が必要だったが、構造化データではその考慮は不要でどのシェルでもほぼ同じコマンドで動く。

Escape for `pulumi config set` on Windows CMD and PowerShell · Issue #2062 · pulumi/docs

構造化データを投入すると、 Pulumi.Stack名.yaml は次のようなYAML構造で保持されます。

config:
  aws-sandbox:data:
    active: true
    nums:
    - 1
    - 2
    - 3

C#的には、 System.Text.Json を使って構造化データにアクセスします。

GetProperty() 時点ではJsonEelementなので、GetBoolean() などで型を明示しないと使いにくいので注意。

var config = new Pulumi.Config();
var data = config.RequireObject<JsonElement>("data");
var active = data.GetProperty("active").GetBoolean();
Console.WriteLine($"Active: {active}");

直近のデプロイ時のConfig にローカルのConfigを更新する

pulumi config refresh を行うと、現在の Pulumi.Stack名.yaml がバックアップされ、直近のデプロイ (pulumi up` 時のconfig を持ってきます。

あまり使わないけど、知っておくと便利なのでオススメ。

REF

公式ドキュメントに沿ってるのでみてみるのおすすめ。

Configuration and Secrets - Pulumi

Pulumi で既存のリソースを取り込む

この記事は、Pulumi dotnet Advent Calendar 2019 の9日目です。

qiita.com

前回までの記事で概ね Pulumi でかける感触が出てきたと思います。 今回から、一歩先に進めることを見ていきましょう。

まずは、既存のリソースを取り込むことです。terraform import ですね。 既存環境を Pulumi で取り込むには必須なので気になるやつです。

目次

TL;DR

pulumi でリソースを書いたときに、CustomResourceOptionsImportId を使うことで特定のリソースを取り込むことができる。

想定ケース

Pulumi でstateから特定のリソースを消す | kinoco Kibela で、Route53 のリソースをComponentResource に移動することを考えてみる。

このままでは、既存のリソースがあるのに pulumi up すると新規に作成しようとする。 Route53 は同じ名前のZoneが作れてしまうのでこのままでは困る。

$ pulumi up

     Type                       Name                                                    Plan
     pulumi:pulumi:Stack        pulumi-dev
     └─ pkg:EksClusterResource  sandbox
 +      └─ pkg:Route53Resource  sandbox-route53                                         create
 +         └─ aws:route53:Zone  sandbox-route53-zone-eks-sandbox-pulumi.my.example.com  create

Resources:
    + 2 to create

対応

ComponentResource でZoneリソースを書いたときに、CustomResourceOptions で ImportId に取り込むリソースのIDを入れる。

AWS では多くの場合、Arn や Id となり、ごくまれに name が該当する。このルールは、もちろんterraformの import に従えばいい。

例えば、Route53 Zoneなら次のようになる。

new Zone($"{name}-zone-{parameter.ZoneName}", new ZoneArgs
{
    Name = parameter.ZoneName,                
    Tags = parameter.Tags,
}, new CustomResourceOptions { Parent = this, ImportId = "ABCDE123456789" });

この状態で pulumi up で preview を見てみると、Plan が create ではなく import になっていることがわかる。

$ pulumi up

     Type                       Name                                                    Plan
     pulumi:pulumi:Stack        pulumi-dev
     └─ pkg:EksClusterResource  sandbox
 +      └─ pkg:Route53Resource  sandbox-route53                                         create
 =         └─ aws:route53:Zone  sandbox-route53-zone-eks-sandbox-pulumi.my.example.com  import

Resources:
    + 1 to create
    = 1 to import
    2 changes

この状態で一度 pulumi up を行う。

$ pulumi up

     Type                       Name                                                    Status
     pulumi:pulumi:Stack        pulumi-dev
     └─ pkg:EksClusterResource  sandbox
        └─ pkg:Route53Resource  sandbox-route53
 =         └─ aws:route53:Zone  sandbox-route53-zone-eks-sandbox-pulumi.my.example.com  imported

Outputs:
    config-active: true

Resources:
    = 1 imported

これですでに取り込みができたので、 ImportId 部分を削除する。

// before
new CustomResourceOptions { Parent = this, ImportId = "ABCDE123456789" });

// after
new CustomResourceOptions { Parent = this });

あとは普通通りに pulumi up していっても問題ない。

$ pulumi up

Resources:
    1 unchanged

REF

Programming Model - import

Pulumi でリソースの結果を参照させる

この記事は、Pulumi dotnet Advent Calendar 2019 の8日目です。

qiita.com

リソースを作ったら、ほかのリソースを作るときにその結果を参照させたいお気持ちになります。 どうやるのか見てみましょう。

目次

TL;DR

  • なるべく Pulumi Resource のまま引き回すと依存関係が解決を意識せず、Resource.Property で必要な値が入った状態で来るのでお勧め
  • 値の変形(Transform)が必要になるまで、リソースのプロパティの型 Output<T> はあまり露出しないようにしよう
  • Output<T>の変形には.Apply(Func<Output<T>, Output<T>) で型を変換することが求められるので注意

Summary

Output<T>Input<T> が、Pulumi の dotnet 実装におけるリソースの入出力の型、依存解決の表現で根本を担っています。

Aリソースの出力結果を Bリソースで使いたいときを考えてみます。

Pulumi の .NET 実装では、リソースの作成をしないとわからない出力値 (Idなど) を Output<T> で表現しています。 Output<T> の中身は Taskで、型で依存関係まで表現できているのでドキュメントを見なくてもインテリセンスで型をみることで書けるのがとてもいいポイントです。

Aリソースの出力が Output<T> で表現されているとき、Bリソースではそれに対応する型 Input<T> で受けることができます。これによって、BリソースはAリソースの作成を待って、Output<T> の値をあたかもTであるかのように取り扱うことができます。

Detail

Pulumi は、実行計画と実行結果という2ステージを持っています。

  • 実行計画では、コードで表現した状態からある程度の実行結果を予想して出力してくれますが、リソース作成時に発行されるIdなどはこの時点ではわかりません
  • 実行結果は、実際にリソース作成が実行された後なので、作成時に採番されるId なども出力されます

Outputs

リソースの実行結果で取得できる値(最終的な出力、依存関係解決後の値)は、Pulumi の C# 的には、Output<T> で表現されます。

例えば Vpc.Id は、Vpcが作成されたときにわかるIdですが、これは Output<string> の型を持ちます。

REF: Pulumi - Programming Model - Outputs

Output は認識的には、promises/futures でありプログラミングモデル的には慣れ親しんだものです。 また、Output 型は依存関係の情報それ自体も含んでいます。(内部的には、Task<T> として扱われている)

https://github.com/pulumi/pulumi/blob/8dbe6650e759cbdfbca6c09725ce0db3fee69f6c/sdk/dotnet/Pulumi/Core/Output.cs#L199

単純にいうと、リソースの出力値それぞれは、Output<T> で表現されている。それだけです。

もしOutput<T> に値が入ったことを待って、Tな値をいじったりする場合はどうすればいいでしょうか? このために、Output<T>.Apply(Func<Output<T>, Output<T>) が用意されています。 .Apply() を使うと、Output<T> に対してFuncの中ではTとして扱い式を実行したうえでOutput<T>として吐き出されるので、リソースの作成時まで値がわからなくても vm.dnsName.apply(dnsName => "https://" + dnsName) のようなT自体を加工しつつ、依存関係を維持したままそれを利用することが可能になります。

Inputs

リソースの入力を見てみましょう。

リソースの入力はInput 型を持っています。これは、生の値、 Promise Output<T> を受けることができます。 つまり、リソースの出力 Output<T> はそのまま別のリソースの入力 Input<T> に利用できることを示します。

Output<T> な値を Input<T> に渡せば、Output<T> を発行したリソースの依存関係も自動的に面倒を見てくれます。

Output の処理の種類

もっともよく使うのは、Output<T>.Apply()Output<T>.Format() です。 特に複数の Output<T1>, Output<T2> を処理する必要があるときは、Output<T>.Format が神がかってるので便利すぎます。

  • Output<T>.Apply() : 1つの Output<T> -> Output<T> への Transform
  • Output.Format(FormattableString) : 複数の Output<string> -> 1つのOutput<string> への Transform
  • Output.Concat(Output<ImmutableArray<T>>, Output<ImmutableArray<T>>): 2つの Output<ImmutableArray<T>> を結合して1つのOutput<ImmutableArray<T>> を生成
  • Output.Tuple<T1, T2 ... T8>(Output<T1>, Output<T2> ... Ouyput<T8>).Apply(t => $"{t.Item1}{t.Item2} ... {t.Item8}"): 複数の Output<T> をまとめて1つの Output<T> を返す

Use Cases

いくつかのパターンを見ていきましょう。

リソース間の値の受け渡しはリソースの型を引き回す

Output<T> ではなく、リソースの型を引き回すことで依存関係の解決が適切に行えます。 この場合、パラメーターはリソースの型を受けることになりますが、手でリソースを作らない限り問題ないでしょう。

例えば、Vpc を作って Subnet を作る場合、Subnet は Vpc のId が必要になります。

DO

この場合、Subnetリソースの Input<string> VpcId プロパティには、Output<string> vpc.Id を渡すのが最も楽で適切といえます。

var vpc = new Vpc("my-vpc", new VpcArgs
{
    CidrBlock = "10.0.0.0/16",
    EnableDnsHostnames = true,
    EnableDnsSupport = true,
});
var subnet = new Subnet($"my-subnet", new SubnetArgs
{
    VpcId = vpc.Id,
    CidrBlock = "10.0.0.0/24",
    AvailabilityZone = "ap-northeast-1a",
});

パラメーターを通して値を渡す場合でも、なるべく Vpc型のまま渡して、必要な個所になるまで Output<string> Vpc.Id を露出しないようにします。 これでリソース作成の依存関係が自動的に解決されつつ、値をいい感じで渡すことができます。

DO NOT

上記の例では Vpc.Id を変形させる必要がないのでOutput<T>.Apply() を使う必要はありません。

vpc.Id.Apply(id =>
{
    var subnet = new Subnet($"my-subnet", new SubnetArgs
    {
        VpcId = id,
        CidrBlock = "10.0.0.0/24",
        AvailabilityZone = "ap-northeast-1a",
    });
    return subnet;
});

出力値の変形が必要な個所でのみ Output<T>.Apply を用いる

もしも、Input<T> ではないところで Output<T> の T を使う必要があったら、Apply の出番です。 例えば次の例では、文字列埋め込みの中で Policy.Arn を使いたいというよくわからない例です。

DO

policy.Arn.Apply(policyArn =>
{
    var attach = new RolePolicyAttachment($"{policyArn}-1", new RolePolicyAttachmentArgs
    {
        Role = role.Name,
        PolicyArn = policyArn,
    });
    return attach;
});

DO NOT

もし上記の例が、ただPolicyをアタッチするだけなら、Applyは不要です。 Applyをやめて次のように、リソースの Output<T> を直接 Input<T> に食わせましょう。

var attach = new RolePolicyAttachment($"policy-attachment-hello", new RolePolicyAttachmentArgs
{
    Role = role.Name,
    PolicyArn = policy.Arn,
});

複数の Output<string> を同時に1つの Output<string> へTransform したいときは Output.Format() を用いる

REF: Pulumi - Programming Mode - Output - Format

例えば EKS の出力があるとします。

var eks = new Pulumi.Aws.Eks.Cluster("my-cluster", new Pulumi.Aws.Eks.ClusterArgs
{
    RoleArn = parameter.Role.Arn,
    Version = parameter.ClusterVersion,
    VpcConfig = new ClusterVpcConfigArgs
    {
        SecurityGroupIds = parameter.SecurityGroups.Select(x => x.Id).ToArray(),
        SubnetIds = parameter.Subnets.Select(x => x.Id).ToArray(),
    },
});

このEKS クラスターに参加させる Node の UserData Input<string> として、 eks の eks.Name, eks.Endpoint, eks.CertificateAuthority.Data を利用したい時を考えましょう。つまり、3つの Output<string> から 1 つの Output<string> の生成です。

DO

NOTE: 他言語のInterpolate が dotnet 版では Format と呼ばれているので表現の違いには注意。

Output.Format を利用することで、複数の Output<T> を全て適切に Unwrap させることができる。

// my-cluster-https://xxxxxx.yyy.ap-northeast-1.eks.amazonaws.com-000aaabbbcccddd000eeefffggg
Output.Format($"{eks.Id}-{eks.Endpoint}-{eks.CertificateAuthority.Apply(x => x.Data)}")

Output<string> Format(FormattableString) は、文字列への変換に便利なショートカットで、実態は、All(inputs).Apply() 担っている。

https://github.com/pulumi/pulumi/blob/8dbe6650e759cbdfbca6c09725ce0db3fee69f6c/sdk/dotnet/Pulumi/Core/Output.cs#L57-L70

DO NOT

Output<T>.Apply() では Unwrap できない。

// "Pulumi.Output`1[System.String]-Pulumi.Output`1[System.String]-Pulumi.Output`1[System.String]"
var data = eks.CertificateAuthority.Apply(auth => auth.Data);
var userdata = Output.Tuple<Output<string>, Output<string>, Output<string>>(eks.Name, eks.Endpoint, data)
    .Apply(item => $"{item.Item1}-{item.Item2}-{item.Item3}");

Output.Tuple().Apply() では Unwrap できない。

// "Pulumi.Output`1[System.String]-Pulumi.Output`1[System.String]-Pulumi.Output`1[System.String]"
Output.Tuple<Output<string>, Output<string>, Output<string>>(eks.Name, eks.Endpoint, eks.CertificateAuthority.Apply(x => x.Data)).Apply(item => $"{item.Item1}-{item.Item2}-{item.Item3}")

Output<T>.Apply() の中で、外から渡したOutput<T> はUnwrap されない。Output<T>.Apply() の中でさらに Output<U>.Apply() した値は Unwrap される。

// "Pulumi.Output`1[System.String]-Pulumi.Output`1[System.String]-000aaabbbcccddd000eeefffggg"
Output.Tuple<Output<string>, Output<string>, Output<ClusterCertificateAuthority>>(eks.Name, eks.Endpoint, eks.CertificateAuthority)
    .Apply(item => item.Item3.Apply(auth => $"{item.Item1}-{item.Item1}-{auth.Data}"));

Resource の結果を Data Source で利用するときはOutput<T>.Apply() が必要

Data Source は基本的に、Input<T> ないし InputList<T> を受け付けません。適切な型は、string だったり ImmutableList<T> となります。

dotnet Data Sources Pulumi.Aws.Iam.Invokes could not resolve Resource Output<T>. · Issue #800 · pulumi/pulumi-aws

そのため、Data Source で Resource の結果 (Output<T>)を受けたい場合は、Output<T>.Apply() を用いる必要があります。

DO

var assumepolicy = role.Arn.Apply(roleArn => Pulumi.Aws.Iam.Invokes.GetPolicyDocument(new GetPolicyDocumentArgs
{
    Statements = new[] {
        new GetPolicyDocumentStatementsArgs
        {
            Actions = "sts:AssumeRole",
            Effect = "Allow",
            Principals = new GetPolicyDocumentStatementsPrincipalsArgs
            {
                Type = "Service",
                Identifiers = "ec2.amazonaws.com",
            }
        },
        new GetPolicyDocumentStatementsArgs
        {
            Actions = "sts:AssumeRole",
            Effect = "Allow",
            Principals = new GetPolicyDocumentStatementsPrincipalsArgs
            {
                Type = "AWS",
                Identifiers = roleArn,
            }
        }
    }
}));

DO NOT

Data Source が Input<T>InputList<T> を型に持っていた場合、それは誤りである可能性が高いです。 このように書いても結果を待って評価しないため、エラーになります。

Pulumi.Deployment+InvokeException: Invoke of 'aws:iam/getPolicyDocument:getPolicyDocument' failed: "statement.1.principals.0.identifiers": required field is not set ()

data "aws_iam_policy_document" "main" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = "ec2.amazonaws.com"
    }
  }
}
resource "aws_iam_role" "main" {
  name               = var.name
  assume_role_policy = data.aws_iam_policy_document.main.json
}
data "aws_iam_policy_document" "eks_kube2iam_role_assumerole_policy" {
  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"
    principals {
      identifiers = ["ec2.amazonaws.com"]
      type        = "Service"
    }
  }
  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"
    principals {
      identifiers = [aws_iam_role.main.arn]
      type        = "AWS"
    }
  }
}

この件はPulumi の AWS 実装で見つけており、Issueで報告したことで型表現の修正が進んでいます。

dotnet Data Sources `Pulumi.Aws.Iam.Invokes` could not resolve Resource `Output`. · Issue #800 · pulumi/pulumi-aws