tech.guitarrapc.cóm

Technical updates

Pulumi リソースのグルーピングと非同期処理を行う

前回リソースの入れ子をする方法を見ました。 この記事は、Pulumi dotnet Advent Calendar 2019 の6日目です。

qiita.com

今回は、ComponenseResourceで親子関係の維持、Dataリソースをasync/awaitで使うという二点を担保できるようにResourceクラスの簡単なガワを用意してみます。

目次

TL;DR

  • ComponentResource を使うことで、Pulumiリソースをグルーピングできる。
  • ComponentResource を継承したクラスを使って、コンストラクタで初期化を書く。
  • 継承したクラスでCreateAsync メソッドを用意して、そこにPulumiリソースの作成やAWSリソースの探索など実処理を書く
  • これで、Compontをnew して、 CreateAsyncで作成という一連のフローを組む。

やりたいこと

async/await な処理が必ず入りえるので、これをいい感じで処理をしたい。 また、リソースをグルーピングする用途のPulumi.ComponentResourceを使いたい。

原則

Pulumi のリソース作成

Pulumi はリソース自体は、new XxxxResource をした時点で生成される。(pulumi up > yes を選択すると)

例: new S3Bucket() をすると S3 Bucket が作成される

async/await が必要なPulumi操作

AWSリソース作成ではなく、何かの情報を拾ってくる操作で必要。(そりゃそうだ)

tech.guitarrapc.com

リソースをまとめるには ComponentResource を Parentとする

Pulumi のリソースをただ作ると、リソースがすべて平たんに作られる。

これをリソースごとにグループ化して、グラフ生成上きれいにしたり、まとめあげるのに用意されているのが Component Resource

ComponentResource 内でリソースを書き、リソースのParent にそのComponentResource (this) を指定することで、ComponentResource ごとにリソースがまとまる。

Component Resource を使うのか、使わないのか

Components を使う or 使わない の決定で、どう書くかは結構決まってくる印象がある。

Components を使わない場合、Task Main になっているので適当に書いても問題ない。ただしリソースのグルーピングができないので、リソースが増えれば増えるほどつらい。 この場合は、プログラミング的に使える手法はたいがい使える。

  • 継承しないクラスにして、async/await なメソッドで処理
  • ただのメソッドで呼び出し

Components を使うと、対象のリソースをグルーピングしたりGraph でまとめたりできるので必要な個所できっちり使ったほうがよさそう。

CompoentResource でどうPulumiリソースの作成とAWSリソースの解決を行うか

Pulumi リソース自体はnew リソース で作れるものの、AWSのリソースを参照するのに async/await が必要である。

ということは、Pulumiリソースの作成前に AWSリソースの探索をしないといけない。

素直にasync/await を使いたいので、Component Resource 自体はコンストラクタで初期化して、XxxxAsync なりのメソッドで実行するようなモデルを提供する必要がある。

Task.Main で async/await で受け取っておいてパラメーターに渡していくのも可能だが、使うタイミングまで隠したいので、事前にawaitするモデルは避けたい。

Constructor で全部やる作戦

コンストラクタで全部やるのはきもいし、async/await なメリットもなくこれを検討する理由はないと判断している。

.Result/Wait() は、ConsoleApp なので SynchronizationContext ないし、 .ConfigureAwait(false) して .Result / Wait() で同期待ちはできるが、書くのがめっちゃつらい。混じってくる。ので

Base Class作戦

Pulumi.ComponentResource を継承して、完全コンストラクタにしつつ、CreateAsync で実行をトリガーするようにしてみる。 これを規約で縛るため、abstract class ResourceBase を作る。

internal abstract class ComponentBase : Pulumi.ComponentResource
{
    public ComponentBase(string type, string name, ResourceOptions opts) : base(type, name, opts) { }

    public abstract ValueTask CreateAsync();
}

単純なケースで Route53 リソースを考えてみる。 Route53 は、Zone と Record で構成してみる。

class Route53Component : ComponentBase
{
    readonly string name;
    readonly Route53ResourceParameter parameter;
    public Route53Component(string name, ResourceOptions opts, Route53ResourceParameter parameter) : base("pkg:component:route53", name, opts)
    {
        this.name = name;
        this.parameter = parameter;
    }

    public override async ValueTask CreateAsync()
    {
        var zone = new Zone($"{name}-zone-{parameter.ZoneName}", new ZoneArgs
        {
            Name = parameter.ZoneName,                
            Tags = parameter.Tags,
        }, new CustomResourceOptions { Parent = this, ImportId = "XXXXXXXXXXXXXX" });

        zone.Id.Apply(id =>
        {
            var record = new Record($"{name}-record-aaaa", new RecordArgs
            {
                ZoneId = id,
                Type = "A",
                Name = $"aaa.{parameter.ZoneName}",
                Records = "1.1.1.1",
            });
            return record;
        });

        // output
        this.RegisterOutputs(new Dictionary<string, object>
        {
            { "zoneId", zone.Id },
            { "zoneName", zone.Name },
            { "zoneNameServers", zone.NameServers },
        });
    }
}

class Route53ResourceParameter
{
    public string? ZoneName { get; set; }
    public Dictionary<string, object>? Tags { get; set; }
}

async/await を使うIamResource を考えてみる。 IAM には、IamRole / IamUser がいるが、これも個別のリソースとみなせる。 また、既存の AWS Policy は await Pulumi.Aws.Iam.Invokes.GetPolicy(new GetPolicyArgs { Arn = "arn:aws:iam::aws:policy/PolicyName" }); で取得できるが await が必要なことがわかる。

こういった await を含めて、実行処理を async ValueTask CreateAsync で書けるので Task は問題なく処理できる。

参考に、Ec2Component をみてみよう。 EC2の作成には 元となるイメージの指定 = AmiId が必要だ。しかし、なんのAmiIdが今利用可能なのかわからないので、AWS リソースに探索して(await が必要)、ComponentBaseを継承したコンポーネントで使ってみる。

class Ec2Component : ComponentBase
{
    readonly string name;
    public Ec2Component(string name, ResourceOptions opts) : base("pkg:IamResource", name, opts)
    {
        this.name = name;
    }

    public override async ValueTask CreateAsync()
    {
        // AWSリソースの呼び出し。AMI を探索、取得
        var ami = await Pulumi.Aws.Invokes.GetAmi(new GetAmiArgs
        {
            MostRecent = true,
            Owners = { "137112412989" }, // Amazon provided Id
            Filters = { new GetAmiFiltersArgs { Name = "name", Values = { "amzn-ami-hvm-*" } } },
        });

        // Pulumi リソースの作成
        var group = new SecurityGroup("web-secgrp", new SecurityGroupArgs
        {
            Description = "Enable HTTP access",
            Ingress =
            {
                new SecurityGroupIngressArgs
                {
                    Protocol = "tcp",
                    FromPort = 80,
                    ToPort = 80,
                    CidrBlocks = { "0.0.0.0/0" }
                }
            }
        });
        var server = new Instance("web-server-www", new InstanceArgs
        {
            Ami = ami.Id,
            InstanceType = size,
            SecurityGroups = { group.Name },
            UserData = @"
#!/bin/bash
echo ""Hello, World!"" > index.html
nohup python -m SimpleHTTPServer 80 &
",
        });

        // output
        this.RegisterOutputs();
    }
}

Stackの提案

現在 Pulumi では ComponentResource ではなく Stack クラスを使うことが提案されています。

Stackの処理はコンストラクタで、出力は public な Output<T> でといった内容で ComponentResource よりは少し.NET的にはよさそうなのではないかという提案です。

が、ここでも Downsides としている通り、async 処理ができないことは挙げられおり問題の認識はされています。

First-class Stack component for .NET · Issue #3619 · pulumi/pulumi