前回リソースの入れ子をする方法を見ました。 この記事は、Pulumi dotnet Advent Calendar 2019の6日目です。
今回は、ComponenseResourceで親子関係の維持、Dataリソースをasync/awaitで使うという二点を担保できるようにResourceクラスの簡単なガワを用意してみます。
- TL;DR
- やりたいこと
- 原則
- Component Resource を使うのか、使わないのか
- CompoentResource でどうPulumiリソースの作成とAWSリソースの解決を行うか
- Stackの提案
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リソース作成ではなく、何かの情報を拾ってくる操作で必要。(そりゃそうだ)
リソースをまとめるには 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