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