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