この記事は、Pulumi dotnet Advent Calendar 2019 の2日目です。
Getting Started を一日目で見たので、Pulumi で C# を使ってそもそもどういう風に書けばいいのかコンセプトを把握します。
疑問はそれから順次書きながら解消していきましょう。
目次
- 目次
- TL;DR
- 基本構造
- Outputs と Inputs
- Secrets の読み込み
- Stack の出力
- Config 参照
- Components
- Provider
- Runtime code
- REF
TL;DR
- Resource インスタンスを new するとリソースができる
- Resourceインスタンスの実態は、name によって論理的に一意に区別されている (URNという)
- ResourceArgs でリソース特有の値を差し込める (型によってドキュメントが不要)
- ResourceOptions でリソースの共通オプションを指定できる
- リソースを作成すると実際のリソースには Auto-naming でランダムな suffixが付く
- リソースの生成で得られる予定の値は
Output<T>
で表現されている - リソースは他のリソースの依存性を自動的に解決するため
Output<T>
に対応したInput<T>
で値を受けることができる Output<T>
とInput<T>
はT
を渡すことができる (implicit による暗黙的型変換が用意されている)- リソースをまとめるときは ComponentsResource を使う
- Providerを使うと他のアカウントなどプロバイダーを明示的に参照することもできる
- DynamicProvidersなどの機能は .NET プレビューではサポートされていない
基本構造
Resource インスタンスの初期化時に、name, args, options を指定する。
var res = new Resource(name, args, options);
All resources have a name, which must be unique in the Pulumi program.
The args provided to a resource determine what inputs will be used to initialize the resource. These can be either raw values or outputs from other resources.
リソースは、name で区別されているのが特徴。
Resource options
Options は、pulumi がどのようにリソースを制御するかの指定に使う。
additionalSecretOutputs
出力がsecret扱いになってないけどsecretにしたいときに使う。
// Ensure the password generated for the database is marked as a secret var db = new Database("new-name-for-db", new DatabaseArgs(), new CustomResourceOptions { AdditionalSecretOutputs = { "password" } });
alias
旧リソース名が新しいリソースに変更になる場合、ただリソースを作ると、前のリソースを消して新しく作る。が、Alias を使うことで前のリソースはこのリソースにmigrateしたことを示すことができる。(作り直さない。)
// Provide an alias to ensure migration of the existing resource. var db = new Database("new-name-for-db", new DatabaseArgs(), new ResourceOptions { Aliases = { new Alias { Name = "old-name-for-db"} } });
customTimeouts
めっちゃ時間がかかるリソースの待機をしたいときや、さくっと失敗が時間で判別できるときに使う。
適用できるオペレーションは、create
, update
, delete
文字列で “5m”, “40s”, “1d” という指定 customTimeouts: { create: "1m" }
がほか言語で使われるが、C# なら TimeSpan でok。
// Wait up to 30m for the database to be created var db = new Database("db", new DatabaseArgs(), new ResourceOptions { CustomTimeouts = new CustomTimeouts { Create = TimeSpan.FromMinutes(30) } });
deleteBeforeReplace
リソースの入れ替え時に先に現在のリソースを消してから新しく作る。当然ダウンタイムが強制されるが、一部リソースはこの対応が必要。defaultは、false。
// The resource will be deleted before it's replacement is created var db = new Database("db", new DatabaseArgs(), new CustomResourceOptions { DeleteBeforeReplace = true });
dependsOn
基本的に pulumi では、そのリソースで他リソースを参照している場合依存が暗黙的にに解決される。
この dependsOn
を使うのは、リソースで直接参照されてないけど、実は依存があるときの明示的な指定。
var res1 = new MyResource("res1", new MyResourceArgs()); var res2 = new MyResource("res2", new MyResourceArgs(), new ResourceOptions { DependsOn = { res1 } });
ignoreChanges
このオプションを使うことで、create作成時にはプロパティを使うけど、update更新時には無視したいプロパティを指定できる。 つまり、作成はするけど、k8sなどで自動的に値が変わるから pulumi上では変更したくない、k8sに任せたいときが該当する。
// Changes to the value of `prop` will not lead to updates/replacements var res = new MyResource("res", new MyResourceArgs { prop = "new-value" }, new ResourceOptions { IgnoreChanges = { "prop" } });
import
既存リソースの id を指定することでpulumi で管理ができるようになる。これが設定されるとプロバイダーにある現在のリソースを見に行って食える。取り込むときに、コンストラクタは対象の状態とずれてないようにしないと import が失敗します。 一度取り込まれたらこのプロパティは必ず外します。
// The input properties must match the values for the exsiting resource `my-database-id` var db = new Database("db", new DatabaseArgs { /*...*/ }, new CustomResourceOptions { ImportId = "my-database-id" });
parent
リソースの親を設定できます。親に関しては、詳しくは Component 参照。
var parent = new MyResource("parent", new MyResourceArgs()); var child = new MyResource("child", new MyResourceArgs(), new ResourceOptions { Parent = parent });
protect
リソースを保護します。 保護すると、直接削除ができなくなり pulumi destroy などでも消せなくなります。 親コンポーネントから設定は受け継ぐのと、 parentが設定されてない場合はデフォルトでfalse です。
var db = new Database("db", new DatabaseArgs(), new ResourceOptions { Protect = true });
provider
リソースの providerを指定します。
var provider = new Aws.Provider("provider", new Aws.ProviderArgs { Region = "us-west-2" }); var vpc = new Aws.Ec2.Vpc("vpc", new Aws.Ec2.VpcArgs(), new ResourceOptions { Provider = provider });
transformations
デフォルトで tag 足したりできるっぽいけど、ちょっといまいちわからないので TODO
[TBD]
Auto-naming
pulumi でリソースを作成すると、デフォルトで auto-named 、つまり my-role
と作っても my-role-d7c2fa0
みたいになる。
random suffix を追加する理由は2つ。
- 同じプロジェクト内で2つスタックがあったときに名前の競合でデプロイが失敗することがないように
- プロジェクト内で複数のインスタンスを作るのが簡単になるのでよさみ。スケーリングとか、テストとかでよく作るじゃろ?
- もし auto-naming がなかったら自分で suffixを考えて対応しないといけないじゃろ
- pulumi で 0 downtime アップデートをするため
- リソースを更新するときに、一部の更新は入れ替えを必須とします。pulumi が auto-naming をすることで、リソースを新規に作ってから、古いリソースを消すことが可能になり処理がシンプルになります。auto-naming がないと、今のリソースを消してから作成することになって、ダウンタイム必須になるじゃろ
もし Auto-naming が嫌なら実リソースの手動で名前を指定可能です。リソースの Args にある、Name プロパティを使いましょう。
var role = new Aws.Iam.Role("my-role", new Aws.Iam.RoleArgs { Name = "my-role-001", });
もし Name プロパティがそのリソースのArgsにない場合は、個別にリソースを見てください。例えばS3 Bucket の場合、Nameじゃなくて Bucket 担っています。
実リソースと論理リソース名は一致する必要がないのでこういうのでもok
var role = new Aws.Iam.Role("my-role", new Aws.Iam.RoleArgs { Name = "my-role-" + Deployment.Instance.ProjectName + "-" + Deployment.Instance.StackName, });
URNs
Unique Resource Name (URN) は、リソースの作成時に自動的に生成されます。
urn:pulumi:thumbnailer-twitch::video-thumbnailer::cloud:bucket:Bucket$cloud:function:Function::onNewThumbnail urn:pulumi: <stackname> :: <projectname> :: <parenttype> $ <resourcetype> ::<resourcename>
将来的に、よりシンプルになったり複雑になる可能性があるので注意
URNに変更がある = 新旧リソースに関連がないとみなされて、古いリソースは削除、新規リソースは作成扱いになります。 例えば、リソースのコンストラクタの name を変更するとかがソレです。
Outputs と Inputs
もし出力を変更したい場合は、Apply メソッドを使います。
例えば、VMの DnsName
出力に https: をつけないなら
var url = virtualmachine.DnsName.Apply(dnsName => "https://" + dnsName);
現時点で .NETは、Outputs のプロパティアクセスがサポートされないので注意。
こういうのは C# では書けない。
let certCertificate = new aws.acm.Certificate("cert", { domainName: "example.com", validationMethod: "DNS", }); let certValidation = new aws.route53.Record("cert_validation", { // Need to pass along a deep subproperty of this Output records: [certCertificate.domainValidationOptions.apply(domainValidationOptions => domainValidationOptions[0].resourceRecordValue)],
- All
もし Outputs を1つにまとめたいなら、All を使います。が、C# で Output.All
が使えるのがすべての出力の型が同じ場合のみ(Output<string>
とか) なので、Output.Tuple
で得られる Tuple から Apply で生成するほうがいい説があります。
// In .NET 'Output.Tuple' is used so that each unwrapped value will preserve their distinct type. // 'Output.All' can be used when all input values have the same type (i.e. all are Output<string>) var connectionString = Output.Tuple(sqlServer.name, database.name) .Apply(t => `Server=tcp:${t.Item1}.database.windows.net;initial catalog=${t.Item2}...`);
- Convert Input to Output
Input をそのまま Output で使いたい場合は、.ToOutput()
します。
Output<string[]> Split(Input<string[]> input) { var output = input.ToOutput() return output.Apply(v => v.Split(",")); }
- Outputs の文字列で毎回 Apply 書きたくない
Output<string> hostName = // get some Output Output<int> port = // get some Output // Would like to produce a string equivalent to: http://{hostname}:{port}/ var url = // ?
というケースを考えます。
先ほどの例では、var url = pulumi.Tuple(hostname, port).Apply(t =>
http://{t.Item1}:{t.Item2}/);
感じになるのが予想できますが、めんどくさいので、Output.Format()
が公開されているのでこっちで。
// In .NET 'Interpolate' is called 'Format'. var url2 = Output.Format($"http://{hostname}:{port}/");
Secrets の読み込み
- Config からの読み込みでは、
Config.GetSecret(key)
かConfig.RequireSecret(key)
を使います - 出力から生成するには、
Output.CreateSecret(value)
を使います
InSecrue に Parameter Store に突っ込むときとこうなります。
var cfg = new Pulumi.Config() var param = new Aws.Ssm.Parameter("a-secret-param", new Aws.Ssm.ParameterArgs { type = "SecureString", value = cfg.Require("my-secret-value"), });
Secure にするならこう。
var cfg = new Pulumi.Config() var param = new Aws.Ssm.Parameter("a-secret-param", new Aws.Ssm.ParameterArgs { type = "SecureString", value = cfg.RequireSecret("my-secret-value"), });
もし明示的に出力をSecret にするなら、Argsにあった additionalSecretOutputs
を使います。
Stack の出力
エントリーポイントで次の出力を行えばok
return new Dictionary<string, object> { { "url", resource.Url } };
CLI 上では、pulumi stack output url
でurl を出力できます。
出力は JSON なので、Dictionary で出力すると値によってはクォートなしで出力されます。
// The dictionary returned by the function passed to Deployment.Run will be used to provide all the exported values. static Task Main() => Deployment.Run(async () => { return new Dictionary<string, object> { { "x", "hello" }, { "o", new Dictionary<string, int> { { "num", 42 } } }, }; });
なら
$ pulumi stack output x hello $ pulumi stack output o {"num": 42}
まとめて JSON 出力したいなら
pulumi stack output --json { "x": "hello", "o": { "num": 42 } }
また、Dictionary の value が object となっていますが、JSON に直変換できるもの以外は String として出さないとエラーが出ます。 例えば、char を出力しようとすると
error: Running program 'C:\git\xxxxx\xxxxx\pulumi\bin\Debug\netcoreapp3.0\Infra.dll' failed with an unhandled exception: System.InvalidOperationException: System.Char is not a supported argument type.
Stack 参照
現時点で C# はサポートされてません。 ほかの Stack で定義した内容を別のStack で使うことはできない。
Complete Support for .NET · Issue #3470 · pulumi/pulumi · GitHub
Config 参照
pulumi config set
で設定したコンフィグを参照するには、config.Require(name)
とします。
var config = new Pulumi.Config(); var name = config.Require("name"); Console.WriteLine($"Hello, {name}!"); return new Dictionary<string, object> { { "config-name", name} };
設定されてなければエラーが出ます。
error: Missing Required configuration variable 'pulumi:name' please set a value using the command `pulumi config set pulumi:name <value>`
$ pulumi config set name foo $ pulumi up + config-name : "foo" $ pulumi config rm name
JSON オブジェクトを config に設定して読みだすには、RequireObject<System.Text.Json.JsonElement>()
を使います。
ただし、普通に登録しようとするとJSONの中で使っている""
が外れる模様。
TIPS : CMD を使って'{}' でJSON 登録しようとしてもトラップなので注意
# これがWindows cmd ではダメ (powershell ではok) > pulumi config set data '{"active": true, "nums": [1,2,3]}' # これだと JSON として不正に登録 > pulumi config set data "{"active": true, "nums": [1,2,3]}" > pulumi config data {active: true, nums: [1,2,3]} > pulumi up error: Configuration 'pulumi:data' value '{active: true, nums: [1,2,3]}' is not a valid System.Text.Json.JsonElement
これでok
# 前は"" で2つ、後ろは"""" で4つ > pulumi config set data "{""active"""": true, ""nums"""": [1,2,3]}" > pulumi config get data {"active": true, "nums": [1,2,3]}
TIPS: PowerShell も CMD と同様のエスケープが必要。ただし、
'{}'
でくくること。
PS> pulumi config set data '{"active": true, "nums": [1,2,3]}' PS> pulumi config KEY VALUE data {active: true, nums: [1,2,3]} PS> pulumi config get data {active: true, nums: [1,2,3]} PS> pulumi up error: Configuration 'pulumi:data' value '{active: true, nums: [1,2,3]}' is not a valid System.Text.Json.JsonElement
これでok
# 前は"" で2つ、後ろは"""" で4つ PS> pulumi config set data '{""active"""": true, ""nums"""": [1,2,3]}' PS> pulumi config get data {"active": true, "nums": [1,2,3]}
あるいはダブルクォート使うならこう
PS> pulumi config set data "{`"`"active`"`"`"`": true, `"`"nums`"`"`"`": [1,2,3]}" PS> pulumi config get data {"active": true, "nums": [1,2,3]}
RequireObject<System.Text.Json.JsonElement>()
でデシリアライズかけているので、出力時にJSONに対応する型にすること。
var config = new Config(); var data = config.RequireObject<JsonElement>("data"); var active = data.GetProperty("active").GetBoolean(); var num0 = data.GetProperty("nums")[0].GetInt32(); // Export the name of the bucket return new Dictionary<string, object> { { "bucket_name", bucket.Id }, { "config-active", active }, { "config-num0", num0 }, };
Components
リソースをグルーピングして他から参照して使うのに利用するのが Components。 terraform でいうところの modules に該当する。
C# では Pulumi.ComponentResource
クラスを継承して利用する。
この時、.ctor(string type, string name, ResourceOptions? options = null) : base(type, name, options)
の実装が求められるのでここに処理を書く。
Components からの出力は、this.RegisterOutputs(Dictionary<string, object>)
で返す。
class EksClusterResource : Pulumi.ComponentResource { public EksClusterResource(string type, string name, ResourceOptions? options = null) : base(type, name, options) { // initialization logic var bucket = new Pulumi.Aws.S3.Bucket($"{name}-bucket", new Pulumi.Aws.S3.BucketArgs(), new CustomResourceOptions { Parent = this }); this.RegisterOutputs(new Dictionary<string, object> { { "bucketDnsName", bucket.BucketDomainName } }); } }
利用するときは、type、name、ResourceOptions を指定する。
var component = new EksClusterResource("ekscluster", "sandbox", new ComponentResourceOptions { });
これで、指定した type、name に基づいてコンポーネントが生成されていることがわかる。
> pulumi up Previewing update (dev): Type Name Plan Info pulumi:pulumi:Stack pulumi-dev 'dotnet build .' completed successfully����܂ Type Name Plan pulumi:pulumi:Stack pulumi-dev + ├─ ekscluster sandbox create + │ └─ aws:s3:Bucket sandbox-bucket create + ├─ aws:kms:Key my-key create + └─ aws:s3:Bucket my-bucket create Outputs: + bucket_name : output<string> + config-active: true Resources: + 4 to create 1 unchanged
Provider
別のプロバイダーを指定して参照するのに使います。
using Pulumi; using Pulumi.Aws; class Program { async Task Main() => Deployment.Run(() => { // Create an AWS provider for the us-east-1 region. var useast1 = new Aws.Provider("useast1", new Aws.ProviderArgs { Region = "us-east-1" }); // Create an ACM certificate in us-east-1. var cert = new Aws.Acm.Certificate("cert", new Aws.Acm.CertifiateArgs { DomainName = "foo.com", ValidationMethod = "EMAIL", }, new ResourceArgs { Provider = useast1 }); // Create an ALB listener in the default region that references the ACM certificate created above. var listener = new Aws.Lb.Listener("listener", new Aws.Lb.ListenerArgs { LoadBalancerArn = loadBalancerArn, Port = 443, Protocol = "HTTPS", SslPolicy = "ELBSecurityPolicy-2016-08", CertificateArn = cert.arn, DefaultAction: new Aws.Lb.ListenerDefaultAction { TargetGroupArn = targetGroupArn, Type = "forward", }, }); }); }
DynamicProviders
https://www.pulumi.com/docs/intro/concepts/programming-model/#dynamicproviders
C# では現状サポートなし
Runtime code
https://www.pulumi.com/docs/intro/concepts/programming-model/#runtime
実行時の処理実行はC# ではサポートされていない。
let bucket = new aws.s3.Bucket("mybucket"); bucket.onObjectCreated("onObject", async (ev: aws.s3.BucketEvent) => { // This is the code that will be run when the Lambda is invoked (any time an object is added to the bucket). console.log(JSON.stringify(ev)); });