前回リソースの入れ子をする方法を見ました。
この記事は、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