tech.guitarrapc.cóm

Technical updates

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