tech.guitarrapc.cóm

Technical updates

Pulumi のComponentResource を書く時の注意

この記事は、Pulumi dotnet Advent Calendar 2019 の7日目です。

qiita.com

前回Componense Resource を薄くラップして使うことを見ました。今回は ドキュメントにないComponentResource の暗黙的なお約束です。 今後拡充するかもです。

目次

TL;DR

  • pulumi logs のコマンドの実装に依存した暗黙のお約束がある

ComponentResource の type は *:*:* フォーマットにする

pulumi logs がこのフォーマットを期待しているため、フォーマットに気を付けましょう。

An assertion has failed: Module member token 'A:B' missing module member delimiter · Issue #1820 · pulumi/pulumi

pulum logs を使うと、リソースのイベントを起点にログ出力を行い、それを pulumi logs でリソースを横断的に見れる。

Unified Logs with pulumi logs

っぽいが、.NET ではまだサポートされていない模様 (イベントの差し込み口がない)

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

Pulumi のリソースを入れ子にする

この記事は、Pulumi dotnet Advent Calendar 2019 の5日目です。

qiita.com

リソースの親子関係を持たせることで、preview表示、グラフ表示したときに入れ子状態が格段と見やすく把握しやすくなります。 ではどうやって親子関係を持たせればいいのでしょうか。

目次

TL;DR

ResourceOptions ではなく CustomResourceOptions を使いましょう。

Summary

入れ子にできるとコンポーネントの中にリソースがまとまるので、Web UI 的にも Preview 的にもわかりやすくなる。 入れ子にしたいがどうやるのか。

入れ子にしたときのPreview表示

Problem

  • 無指定だと Stack が親になって入れ子にならない
  • new ResourceOptions { Parent = this } で Parent = this を指定しても入れ子にはならない

Components の入れ子

new ResourceOptions { Parent = this } で Parent = this を指定しても XxxxxResourceImParentResource の入れ子にはならない。

class ImParentResource : Pulumi.ComponentResource
{
    public ImParentResource(string name, ResourceOptions opts) : base("pkg:ImParentResource", name, opts)
    {
        new XxxxxResource($"{type}:xxxx", $"{name}-Xxxxx", new ResourceOptions { Parent = this })
        {
        }
    }
}

CustomResourceOptions { Parent = this } を使うことで入れ子にできる。

class ImParentResource : Pulumi.ComponentResource
{
    public ImParentResource(string name, ResourceOptions opts) : base("pkg:ImParentResource", name, opts)
    {
        new XxxxxResource($"{type}:xxxx", $"{name}-Xxxxx", new CustomResourceOptions { Parent = this })
        {
        }
    }
}

リソースの入れ子

リソースを作成するときに、CustomResourceOptions で親ComponentResource を指すことで入れ子ができる。 指定しない場合は、Stack = Root が親になってしまうので、コンポーネントの子にしたい場合は、常に new CustomResourceOptions { Parent = this } を指定する必要がありそう。

CustomResourceOptions ない場合、リソースを持っているコンポーネントリソースが親にならない。

var vpc = new Vpc($"{name}-vpc", new VpcArgs
{
    CidrBlock = "10.0.0.0/16",
    EnableDnsHostnames = true,
    EnableDnsSupport = true,
    Tags = new Dictionary<string, object>(parameter.Tags)
    {
        { $"Name", $"MyVpc"}
    },
});

new CustomResourceOptions { Parent = this } を指定することで、親子にできる。

var vpc = new Vpc($"{name}-vpc", new VpcArgs
{
    CidrBlock = "10.0.0.0/16",
    EnableDnsHostnames = true,
    EnableDnsSupport = true,
    Tags = new Dictionary<string, object>(parameter.Tags)
    {
        { $"Name", $"MyVpc"}
    },
}, new CustomResourceOptions { Parent = this });

Pulumi で Aws のリソース情報の取得

この記事は、Pulumi dotnet Advent Calendar 2019 の4日目です。

qiita.com

terraform でいうところの data リソースに相当する処理を見てみましょう。

すでに存在するリソースを参照して、ほかのリソースで利用するための方法です。

目次

TL;DR

既存のAWS リソースを探索する方法を示す。

基本的に、ami 一覧の取得などのような処理は、Aws.Invokes を使う

terraform でいうところの data リソースに相当する。

基本的に、リソースフォルダにないメソッド名.cs がそれに該当する気配。

https://github.com/pulumi/pulumi-aws/tree/58da21a081d1e62497e1c96b017bc78c1e257f8b/sdk/dotnet

現在の AWS アカウント情報を取得する

var callerIdentity = await Pulumi.Aws.Invokes.GetCallerIdentity();

AMIを取得する

EC2 Linux WebServer Instance

var ami = Aws.Invokes.GetAmi(new Aws.GetAmiArgs
{
    Filters =
    {
        new GetAmiFiltersArgs
        {
            Name = "name",
            Values =  { "amzn-ami-hvm-*" },
        },
    },
    Owners = { "137112412989" }, // This owner ID is Amazon
    MostRecent = true,
});

あるいは、EKS Optimized AMIの取得

var ami = await Pulumi.Aws.Invokes.GetAmi(new GetAmiArgs
{
    MostRecent = true,
    Owners = { "602401143452" },
    Filters = {
        new GetAmiFiltersArgs { Name = "name", Values = { $"amazon-eks-node-{version}-v*" } },
        new GetAmiFiltersArgs { Name = "root-device-type", Values = "ebs" },
        new GetAmiFiltersArgs { Name = "virtualization-type", Values = "hvm" },
    },
});

IAM Role を取得する

AWS の IAM Role を取得する。

await Pulumi.Aws.Iam.Invokes.GetPolicy(new GetPolicyArgs { Arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" });

IAM Document Policy を取得する

JSON を書かず、型から JSON っぽいものを生成するのに使うのが、IAM Document Policy。これを使うと、JSON が不適切化どうかもわかるのでお勧め。

await Pulumi.Aws.Iam.Invokes.GetPolicyDocument(new GetPolicyDocumentArgs
{
    Statements = new GetPolicyDocumentStatementsArgs
    {
        Effect = "Allow",
        Actions = new []
        {
          "elasticloadbalancing:*",
          "ec2:CreateSecurityGroup",
          "ec2:Describe*",
        },
        Resources = "*",
    },
});

これは入れ子が多いので、適当に Statement[] だけ渡せるようにヘルパーを作っておくと便利。

private static async ValueTask<GetPolicyDocumentResult> GetPolicyDocument(GetPolicyDocumentStatementsArgs[] statements, string version = null)
{
    return await Pulumi.Aws.Iam.Invokes.GetPolicyDocument(new GetPolicyDocumentArgs
    {
        Version = version,
        Statements = statements
    });
}

これで次のようにstatement だけ渡すように書ける。

await GetPolicyDocument(new [] {
    new GetPolicyDocumentStatementsArgs
    {
        Effect = "Allow",
        Actions = new []
        {
          "elasticloadbalancing:*",
          "ec2:CreateSecurityGroup",
          "ec2:Describe*",
        },
        Resources = "*",
    },
})

Pulumi Web UI でできること

この記事は、Pulumi dotnet Advent Calendar 2019 の3日目です。

qiita.com

リソースを作るとなんか可視化されるらしい、Pulumi の Web UI についてざっくりみてみましょう。

目次

TL;DR

Pulumi で圧倒的に使いやすいのが Web UI。

特に、履歴、リソースの状態や依存関係が見れるのがいい。

state ファイルの確認はできない模様。

STACKS

pulumi の環境を示す Stack がWeb UI 上で確認できる。

いつ更新があったのか、いくつリソースがあるのかがStackごとに出る。

Stack概要

Stack を選ぶと、更新履歴と outputs、configuration がSTACKに表示される。

Detail や Activity を選ぶと、その実行時に何が行われたのかわかる。

実行履歴

Activity > Timeline で時系列の変化もわかる

実行タイムライン

Activity のログにある PREVIOUS から前の履歴に戻ることもできる

Activityのタブを選択した状態では、直近の変更一覧も可視化される。

直近の実行一覧

Resources をみると、Stack のリソースが一覧で確認できる。

Resourcesタブ

Resources > 適当なリソースを選択すると、そのリソースの詳細を確認できる。

Resource詳細

Resources > リソースのわきにある AWS アイコンあたりでクラウドコンソール上の表示もできる。いくつかのリソースはリンクが出ません。

AWSコンソールのリソースリンク

Resources > リソースの下部には Dependencies があり、依存関係も可視化される。

Resourceの依存関係

なお、kms を見ても、「どこでつかっているか」は可視化されない。

使われているは可視化されない

Resources の下にあるトグルでマップ表示ができる。

Resources表示切替

ResourceのMap表示

SETTINGS

ユーザーのプロファイル設定が確認できる。

Profile概要

Access Tokens で今のAccess Token 一覧の確認と発行、削除が可能。

AccessToken管理

Subscription からプランの確認ができる。

Planの確認

Integrations で CD 連携が確認できる。CircleCI での実行もできる。

IntegrationとCD連携