tech.guitarrapc.cóm

Technical updates

静音マウスと M650 SIGNATUREワイヤレスマウス

MxAnywhere3は素敵なマウスですが、最近出たM650 SIGNATUREマウス (以降m650) もいい静音マウスです。

2年前に静音マウスを試したときは「クリック感が鈍く、静音という割に音がする」と思ってやめていましたが、m650はMxAnywhere3に比べて違和感がすくないという風の噂を聞いて試してみました。

購入してから3か月使ってみて、いい感じだと思ったので記事に起こしておきます。

tl;dr;

左右のクリックは音が全くしないので静音最高です。が、ミドルクリックとサイドボタンは静かながら音はします。

いくつか使うにあたって気を付けたいと思ったことがあります。

  • MxAnywhere3を使っていたならm650はサイズMにしましょう。Lは大きすぎてつらい
  • 電池が入っている分、前が軽く中~後に重心があるので、軽く浮かした時のバランスは微妙です。(前だけ上がる感じ)
  • ラバーグリップはMxAnywhereよりも硬質でざらざらします
  • UnifyingではなくLogi Boltなので、接続の仕方が微妙に違います。時にUSB切り替えしている場合、PCごとにペアリングが必要です。(Unifyingのようなレシーバーとのペアリングではない)

この静かさになれたら、MxAnywhere3には戻れなくなりました。 そして、静音Realforce R3は圧倒的にマウスに比べてうるさいので、もっと静音してほしい。

tech.guitarrapc.com

MxAnywhere3 に残された課題

MxAnywhere3を買ってからずっと使っています。

tech.guitarrapc.com

個人的には今までで最高のマウスですが、最後の課題が音です。

MxAnywhere3は静音マウスではないので、静かな室内でクリック音がカチカチ鳴りうるさいと感じることがあります。これが静音化されればいいのに。 では実際Logicoolの静音マウスでどれぐらい期待できるのかを確認するためm650を試しています。

m650 と MxAnywhere3の比較

m650は2022年2月3日発売のマウスです。

www.logicool.co.jp

価格レンジが違うものの、m650は発売されてすぐで4290円、MxAnywhere 3は10000円前後とお手頃になっています。

マウスの比較

引用: https://www.logicool.co.jp/ja-jp/products/mice/m650-signature-wireless-mouse.html

サイズ

サイズが2通り(LとM) があります。MxAnywhere3を使っているならM一択です。

気づかずLを買ったのですが、大きすぎて失敗しました。

グリップ

m650はラバーグリップです。 MxAnywhere 3のようなシリコンじゃないのかと気になりますが、触った感じはべたべたしなさそうなのでスルーしています。

価格がお求めやすいのもあり、ダメになったら買い替えてもいいでしょう。

電池

充電式ではなく、単三電池1つで24か月持ちます。 電池はマウスが重くなる、予備電池が必要なので好みではないのですが長寿命なので気にしないことにしています。

MxAnyehreは数か月に一度気が付いたときに充電していたので、頻度的にはm650のほうが少なくなります。

静音

SilentTouch を採用しているとのことで、M170と比べるとM650のクリック音は90%軽減と聞きますが買うまでは眉唾でした。

実際に購入してみると、クリック音がほぼ聞こえないので確かに静音です。 1m離れると聞こえないので、ほかの人に聞こえることはあまりないでしょう。

クリック感は普通のマウス同様にあるので違和感もありません。

MacBookのトラックパッドかな? とはじめ思ったぐらいには静かですごい。

ホイール

SmartWheelになっているので、MxAnywhere 3の高速スクロール時のフリースピンはありません。 ページを一気に移動するときに若干不便ですが、勢いよくスクロールするとある程度一気に移動してくれるので妥協できる良さはあります。

水平スクロールはMxAnywhere 3と同じサイドボタンを押しながらスクロールなので便利。

接続

MxAnywhere 3はUnifyingでしたが、m650はLogi Boltに変わりました。 Logi Boltは各PCでペアリングが必要なのは面倒ですが、安定して接続できており今のところ一切不便はありません。

Bluetoothも可能ですが、マウスのBT接続はPCが高負荷時に困るのはあるあるなのでLogi Boltで利用しています。

まとめ

静音マウスすごく気に入ったのでm650大好きです。 コスパ考えても、このマウスは他の人がちょっと試すにはいいと思います。

MxAnywhere 3を使っている人はサイズはMにしましょう、忘れずに。 MxMaster 3の人はLでもいいですね。

GitHub Actions で README にリポジトリのコードを埋め込みたい

README に、リポジトリにおいているコードを埋め込みたい時があります。 そんな時に便利なのが、embedme です。

github.com

今回は、GitHub Actions を使って README にリポジトリのコードを埋め込むことをしたので紹介します。

tl;dr;

  • GitHub Actions で embedme を実行して、README.md にリポジトリのコードを埋め込み自動化ができる
  • リポジトリと README.md で同じコードを示す時の二重管理が解消するのでうれしい
  • 軽量シンプルなので、pull_request、push、workflow_dispatch など任意のトリガーで組めて便利

embedme の基本的な利用例

例として、README.md に src/example.ts の中身を埋め込むことを考えてみましょう。

まずはembedme をインストールします。

npm install -g embedme

README.md に次のようにコードブロックと言語を示しておいてリポジトリのファイルパスを示します。

gist.github.com

TIPS: OS問わずファイルパスは / 区切りがいいです。

あとは、インストールした embedme をREADME.md に実行します。

embedme README.md

これでexample.ts のファイルがコードブロックに差し込まれます。便利。

gist.github.com

行指定も直感的にできます。

// src/embedme.lib.ts#L44-L82

embedme のいいところ

個人的に気に入ってるところは3つあります。

埋め込むときに使ったコメントがコードブロックに残る

埋め込み後もコメントが残ることで、埋め込むソースコードを更新してembedmeを実行すると埋め込みが更新されるので、勝手にメンテが維持します。 どことリンクしていたかも一目瞭然で、README.md を見る人が探すこともできるので好きです。

実行が軽い

インストールも実行も早いのはいいこととです。 npm install -g embedme で入るので、GitHub Actions などの CI との相性もいいです。

実行も即座に終了するのでカジュアルに走らせられる安心感があります。

実行履歴

README.md の何行目のコードブロックを実行した、実行できなかったを理由を添えて表示してくれます。 例えばファイルパスが見つからず埋め込みができなかったら次のように教えてくれます。

   README.md#L381-L383 Found filename .github\workflows\concurrency_control_cancel_in_progress.yaml in comment in first line, but file does not exist at /home/runner/work/githubactions-lab/githubactions-lab/.github\workflows\concurrency_control_cancel_in_progress.yaml!

十分です。

embedme で困るであろうこと

コードブロックのコメント

コードブロック内のコメントを検知するので、埋め込む前提になってないのにコメントを適当にいれたコードブロックを作ると警告は出ます。 ファイルパスが示されていないければ問題ないですが注意です。

言語ごとのコメントが違う

言語ごとに対応しているので当然ですが注意しましょう。 yaml や bash なら # でコメント認識ですし、js や ts なら // でコメント認識です。

GitHub Actions で自動化する

embedme は GitHub Actions としては公開されていないので適当に組みます。 今回は、GitHub Actions をいろいろ試しているリポジトリで組んだ例を示します。

github.com

大量に yaml を埋め込んでいるので本当に助かっています。

https://github.com/guitarrapc/githubactions-lab での自動埋め込み例

GitHub Actions workflow を用意する

普段は pull_request で実行して、変更があれば PR にコミットを積むようにします。 必要に応じて workflow_dispatch で手動で更新もできるようにします。

gist.github.com

PRで リポジトリを checkout するときは、Merge コミットではなく PR のコミットを checkout します。 refを指定せず checkout すると、後段で git push したときにマージコミットが PR に入ってしまいます。

      - if: ${{ github.event_name == 'pull_request' }}
        uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.sha }} # checkout PR HEAD commit instead of merge commit

19-22行目で embedded を実行します。

      - name: Embedding Code into README
        run: |
          npm install -g embedme
          embedme README.md

あとは変更があれば git push します。

      - name: git diff
        id: diff
        run: |
          git add -N .
          git diff --name-only --exit-code
        continue-on-error: true
      - if: steps.diff.outcome == 'failure'
        name: git-commit
        run: |
          git config user.name github-actions[bot]
          git config user.email 41898282+github-actions[bot]@users.noreply.github.com
          git add .
          git commit -m "[auto commit] Embed code"
      - if: steps.diff.outcome == 'failure'
        name: Push changes
        uses: ad-m/github-push-action@master
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          branch: ${{ github.head_ref }}

push で実行してもいいですが、私のリポジトリではREADMEの目次作成 (toc) を push でやっているので pull_request にしています。 コード生成は重なると直列実行 と checkout 時の ref 伝搬を気にしないといけないので注意です。

まとめ

embedme 便利です。 あんまり README にリポジトリのコードを埋め込むのを考えてこなかったのですが、いざやってみると便利でいろいろ使えそうです。(実際コピペではることありますし)

補足情報として、類似したものに tokusumi/markdown-embed-codeがありますが、これはそもそも実行できなかったのと docker のpullが走って重すぎるので好みじゃなかったです。

github.com

Pulumi を C# で書くときに気を付けていること

Pulumi は複数の言語で書くことができるのですが、そのうちの1つに .NET (C#) もあります。

Languages より https://www.pulumi.com/docs/intro/languages/

以前 Pulumi を C# で書く時の事始めシリーズを書いていましたが、あれから時間がたって書き方も変わりました。 今回は、実際にどのように書くことで Pulumi で書きやすい、IaC制御しやすいを目指しているのかを言語化します。

tl;dr;

Pulumi + C# の時に、Pulumi の考えているやりやすいにのっかりつつ、C# 的に書きやすく、IaC として管理しやすいかを検討します。 ポイントは4点です。

  • コンストラクタで処理を完結させる
  • Output`T型 で将来の値を表現する
  • リソースインスタンス作成はコンポーネントの責務を徹底する
  • 定数クラスと分岐で環境差分を表現する

DO/DO NOT/CONSIDER で気を付けていることを順にあげていきます。

コンストラクタで処理を完結させる

  • ✔️ DO: コンストラクタで処理を行う
  • ❌ DO NOT: Task メソッドに処理を分けて呼び出すことを避ける

AWS CDK も似た感じですが、リソース/コンポーネントの作成処理は コンストラクタで完結させます。 例えば次のようになります。

// DO
return await Deployment.RunAsync<MyStack>();

class MyStack : Stack
{
    public MyStack()
    {
        var foo = new FooResource("Foo",  new FooArgs
        {
            // 何か処理
        });
    }
}

Task ExecuteAsync() のように非同期メソッドを生やして呼び出す、ということは避けるといいでしょう。 非同期メソッドに処理を分けると、そのメソッド内部では async/await を使えますが、呼び出し元の最上流はコンストラクタです。 コンストラクタ呼び出し時点でリソースが作られないのは良しとしても、ExecuteAsync 呼び出し忘れや、Task<T>.GetAwaiter().GetResult() とか絶対避けたいでしょう。 あと、明らかに冗長です。

// DO NOT
return await Deployment.RunAsync<MyStack>();

class MyStack : Stack
{
    public MyStack()
    {
        var foo = new FooComponent();
        foo.ExecuteAsync().GetAwaiter().GetResult(); // したくない
    }
}

public class FooComponent : ComponentResource
{
    public FooComponent() : base()
    {
        // 初期化
    }

    public async Task ExecuteAsync()
    {
        var foo = new FooResource("Foo",  new FooArgs
        {
            // 何か処理
        });
    }
}

コンストラクタで async/await は使えないので非同期処理をどうするのか、という疑問がわきますが、それは次の Output<T> を使う話になります。

Output`T型 で将来の値を表現する

Pulumi で何かしらのリソースを作成するときに、リソースが作成されないと決定できない「将来の値 = (いわゆるPromise)」は Output<T> 型で表現されます。

www.pulumi.com

  • ✔️ DO: 将来の値は Output<T> を伝搬させる
  • ✔️ DO: Taskを扱う非同期処理は Output<T> でくるむ
  • ❌ DO NOT: await して生の値をとろうとしない
  • ✔️ DO: Output<T> の 生の値 Tが必要な場合は Output<T>.Apply() メソッド内部のラムダで利用する

Pulumi を C# で書くときは、Output<T> で将来の値を伝搬するのを徹底するのがよいでしょう。 例えば Output<T> を活用すると、次のように foo リソースが作成されていなくても bar リソースで fooリソースを使う/依存があることを示すことができます。

// DO
return await Deployment.RunAsync<MyStack>();

class MyStack : Stack
{
    public MyStack()
    {
        var foo = new FooResource("Foo",  new FooArgs
        {
            // 何か処理
        });

        var bar = new BarResource("Bar", new BarArgs
        {
            FooId = foo.Arn, // foo が生成されるまで値が未確定。fooへの依存も自動的に解析される。
        });
    }
}

Output<T> に対応する、リソースが受け入れるときの型が Input<T> ですが、自分で利用する機会はほぼなく Output<T> しか使わないでしょう。

非同期メソッドと Output`T

Pulumi には、現在認証している クラウド環境 やその環境のリソースを取得するメソッドが用意されています。 例えば、AWS 環境で既存の Vpc を取得するメソッドがあります。

// メソッドの返り値は、Task<Pulumi.Aws.Ec2.GetVocResult>
Pulumi.Aws.Ec2.GetVpc.InvokeAsync(new Pulumi.Aws.Ec2.GetVpcArgs { Id = "foo" });

C# 的には Task なんだから await することで待ちたくなりますが、それでは前出のasync/await を書こうとしたときのジレンマに陥ります。 Taskを扱う非同期処理は、await する代わりに Output<T> でくるんであげることで自然と扱えます。

先の例を Output<T> でくるむには、Output.Create<T>() メソッドを利用するといいでしょう。

// DO
var vpcId = Output.Create(Pulumi.Aws.Ec2.GetVpc.InvokeAsync(new Pulumi.Aws.Ec2.GetVpcArgs { Id = "vpc-0123456" }));

// DO NOT
var vpcId = await Pulumi.Aws.Ec2.GetVpc.InvokeAsync(new Pulumi.Aws.Ec2.GetVpcArgs { Id = "vpc-0123456" });

Output`T の T を取り出したい

また、リソースを生成した結果の Output<T> を 先ほどのInvokeAsync 内の処理で使いたいことがあります。

var foo = new FooResource("Foo",  new FooArgs
{
    // 何か処理
});

// プロパティは仮です、実際とは異なります。
Pulumi.Aws.Iam.GetPolicyDocument.InvokeAsync(new GetPolicyDocumentArgs
{
    Resources = new [] { /* ここでfooリソースのArnを使いたい*/ }
});

直接 foo.Arn を指定しようとすると foo.Arn は Output<T> なので文字列には変換されません。(ここが CDK との違いですね)

Output<T>T を直接使用した処理を書きたい場合は、Output<T>.Apply<a>()メソッドのラムダ内部で処理を書くといいでしょう。 Output<T>.Apply<a>() の返り値は Output<a> となるので、結果を他リソースに食わせる時も自然と与えることができます。

// DO
var policy = foo.Arn.Apply(arn => Pulumi.Aws.Iam.GetPolicyDocument.InvokeAsync(new GetPolicyDocumentArgs
{
    Resources = new [] { $"{arn}/*" } // T が入っている
}));

// DO NOT
var policy = Output.Create(Pulumi.Aws.Iam.GetPolicyDocument.InvokeAsync(new GetPolicyDocumentArgs
{
    Resources = new [] { $"{foo.Arn}/*" } // Output<T> のままなのでダメ
}));

Output<T> から別の Output<T> に変換したいときも同じように Output<T>.Apply()Output<T>.Format() が利用できます。

// 出力される Output<string> は arn:aws:s3:::foo に変換される。
Output.Create("foo").Apply(x => $"arn:aws:s3:::{x}");

リソースインスタンス作成はコンポーネントの責務を徹底する

  • ✔️ DO: 実装 (リソース) は コンポーネントに管理させる
  • ❌ DO NOT: Stack に直接実装 (リソース) を記述する
  • ✔️ CONSIDER: リソース処理の前に値の検証をする
  • ✔️ DO: 他のコンポーネントからリソースの結果を使用たいときはプロパティを公開する

Pulumi でリソースを作る = リソースインスタンスを作ることを指します。 また、リソースは1つ一つがユニークな urnで識別され、urn は リソースがコンポーネントに含まれるかどうかでも IDが変わります。 このため、リソースは当初からコンポーネントに分離することを念頭において、 Stack に直接書き出すのは避けるといいでしょう。

// DO
class MyStack : Stack
{
    public MyStack()
    {
        var opts = new ComponentResourceOptions { Parent = this };

        // コンポーネントを呼び出す
        var sg = new SecurityGroupComponent("my-sg", "securitygroup", opts, new SecurityGroupComponentArgs
        {
            VpcId = "vpc-0123456",
        });
    }
}

public class SecurityGroupComponent : ComponentResource
{
    public SecurityGroupComponent(string service, string component, ComponentResourceoptions opts, SecurityGroupComponentArgs args): base($"{service}:components:{component}", $"{service}-{component}", opts)
    {
        var opt = new CustomResourceOptions { Parent = this };

        // リソースインスタンス管理はコンポーネントのお仕事
        var egresss = new SecurityGroupEgressArgs
        {
            FromPort = 0,
            ToPort = 0,
            Protocol = "-1",
            CidrBlocks = new[] { "0.0.0.0/0" },
        };

        var aSg = new SecurityGroup("a-sg", new SecurityGroupArgs
        {
            Description = "a-SG"
            VpcId = args.VpcId
            Ingress = new[] {
                new SerurityGroupIngressArgs
                {
                    FromPort = 80,
                    ToPort = 80,
                    Protocol = "TCP",
                    Descrsiption = "HTTP access from XXXX",
                    CidrBlocks = "0.0.0.0/0",
                }
                new SerurityGroupIngressArgs
                {
                    FromPort = 443,
                    ToPort = 443,
                    Protocol = "TCP",
                    Descrsiption = "HTTPS access from XXXX",
                    CidrBlocks = "0.0.0.0/0",
                }
            },
            Egress = new[] { egress },
        }, opt);

        var bSg = new SecurityGroup("b-sg", new SecurityGroupArgs
        {
            Description = "b-SG"
            VpcId = args.VpcId
            Ingress = new[] {
                new SerurityGroupIngressArgs
                {
                    FromPort = 0,
                    ToPort = 0,
                    Protocol = "-1",
                    Descrsiption = aSg.Name,
                    SecurityGroups = aSg.Id,
                    Self = false,
                }
            },
            Egress = new[] { egress },
        }, opt);
    }
}

次のようにリソースを Stack に直接書き出したが最後、そのリソースの管理は誰がやるのか困り始めることでしょう。

// DO NOT
class MyStack : Stack
{
    public MyStack()
    {
        var opt = new CustomResourceOptions { Parent = this };

        // リソースを直接Stackに書き出す
        var egresss = new SecurityGroupEgressArgs
        {
            FromPort = 0,
            ToPort = 0,
            Protocol = "-1",
            CidrBlocks = new[] { "0.0.0.0/0" },
        };

        var aSg = new SecurityGroup("a-sg", new SecurityGroupArgs
        {
            Description = "a-SG"
            VpcId = args.VpcId
            Ingress = new[] {
                new SerurityGroupIngressArgs
                {
                    FromPort = 80,
                    ToPort = 80,
                    Protocol = "TCP",
                    Descrsiption = "HTTP access from XXXX",
                    CidrBlocks = "0.0.0.0/0",
                }
                new SerurityGroupIngressArgs
                {
                    FromPort = 443,
                    ToPort = 443,
                    Protocol = "TCP",
                    Descrsiption = "HTTPS access from XXXX",
                    CidrBlocks = "0.0.0.0/0",
                }
            },
            Egress = new[] { egress },
        }, opt);
    }
}

コンポーネントでリソースインスタンスを作成するので、リソース作成に必要な値をコンポーネントに渡す必要があります。 適当なrecord クラスで渡してあげると簡単でいいでしょう。

// DO
class MyStack : Stack
{
    public MyStack()
    {
        var opts = new ComponentResourceOptions { Parent = this };

        // コンポーネントを呼び出す
        var sg = new SecurityGroupComponent("my-sg", "securitygroup", opts, new SecurityGroupCompoonentArgs
        {
            VpcId = "vpc-0123456",
        });
    }
}

public class SecurityGroupComponent : ComponentResource
{
    public SecurityGroupComponent(string service, string component, ComponentResourceoptions opts, SecurityGroupComponentArgs args): base($"{service}:components:{component}", $"{service}-{component}", opts)
    {
        // 省略
    }
}

public record SecurityGroupComponentArgs
{
    public Output<string> VpcId { get; init; }
}

リソース処理の前に値の検証をする

各種ComponentArgs には、値の検証を担保させると Component内部での入力値の検査のほとんどを考慮しなくてよくなるのでオススメです。

// CONSIDER
public class SecurityGroupComponent : ComponentResource
{
    public SecurityGroupComponent(string service, string component, ComponentResourceoptions opts, SecurityGroupComponentArgs args): base($"{service}:components:{component}", $"{service}-{component}", opts)
    {
        // 初めに検証する
        args.Validate();

        // 省略
    }
}

public interface IValidate
{
    void Validate();
}

public record SecurityGroupComponentArgs : IValidate
{
    public Output<string>? VpcId { get; init; }

    [MemberNotNull(nameof(VpcId))]
    public void Validate()
    {
        if (VpcId is null) throw new ArgumentOutOfRangeException(nameof(VpcId));
    }
}

もちろんVpcId のように必須なものはコンストラクタでもいいでしょう。

// CONSIDER
public record SecurityGroupComponentArgs(Output<string> VpcId) : IValidate
{
    public IReadOnlyList<string>? Nanika { get; init; }

    [MemberNotNull(nameof(Nanika))]
    public void Validate()
    {
        if (Nanika is null) throw new ArgumentOutOfRangeException(nameof(Nanika));
    }
}

コンポーネント外部へのプロパティの公開

C# ではクラスの内部の情報を公開するときに プロパティを使いますが、Pulumi でもそれは変わりません。 公開したいリソースは get only プロパティを使うといいでしょう。

Pulumi コンソールでStack 出力に表示したい場合は、[Output] 属性を付けたプロパティで公開します。 プリミティブな型でないと表示できないので注意です。

// DO
class MyStack : Stack
{
    public MyStack()
    {
        var opts = new ComponentResourceOptions { Parent = this };

        var sg = new SecurityGroupComponent("my-sg", "securitygroup", opts, new SecurityGroupComponentArgs
        {
            VpcId = "vpc-0123456",
        });

        SecurityGroupAId = sg.A.Id
    }

    // Stack の Output に公開
    [Output]
    public Output<string> SecurityGroupAId { get; set; } // set 必須
}

public class SecurityGroupComponent : ComponentResource
{
    // リソースを公開する
    public SecurityGroup A { get; }

    public SecurityGroupComponent(string service, string component, ComponentResourceoptions opts, SecurityGroupComponentArgs args): base($"{service}:components:{component}", $"{service}-{component}", opts)
    {
        var aSg = new SecurityGroup("a-sg", new SecurityGroupArgs
        {
            // 省略
        }

        A = aSg;
    }
}

定数クラスと分岐で環境差分を表現する

  • ✔️ DO: 定数クラスに環境ごとの値を定義して Stack で参照させる
  • ❌ DO NOT: Stack に環境ごとの値を直接書く
  • ✔️ CONSIDER: 環境ごとのリソースの違いはコンポーネントの分岐などで表現する

環境差分と定数クラス

コンポーネントの処理は環境で同じ、コンポーネントに与える値だけ違うというケースが多いでしょう。 この場合、コンポーネント呼び出し時にStackへ直接値を書くのではなく、定数クラスを用意して参照させると環境ごとの差分が管理しやすいのでオススメです。

// DO
public static class Constants
{
    public const string Project = "MyProject";
    public const string Env = "dev";
    public const string Service = Project + "-" + Env;

    public static class Vpc
    {
        VpcId = "vpc-0123456"; // 環境ごとに違うであろう値
    }
}

class MyStack : Stack
{
    public MyStack()
    {
        var opts = new ComponentResourceOptions { Parent = this };

        var sg = new SecurityGroupComponent("my-sg", "securitygroup", opts, new SecurityGroupComponentArgs
        {
            VpcId = Constants.Vpc.VpcId,
        });
    }
}

// DO NOT
class MyStack : Stack
{
    public MyStack()
    {
        var opts = new ComponentResourceOptions { Parent = this };

        var sg = new SecurityGroupComponent("my-sg", "securitygroup", opts, new SecurityGroupComponentArgs
        {
            VpcId = "vpc-0123456", // 直接値を指定するのは避けたい
        });
    }
}

環境ごとのリソースの違いはコンポーネントで頑張る

コンポーネントの処理は環境で同じ、コンポーネントに与える値だけ違うというケースが多いでしょう。

と書きましたが、そんなの絵空事です。 実際には、開発にはあるけど、本番にはないリソースというのはよくある話でしょう。

こういった環境差分は、コンポーネントの中で分岐などで適当に頑張るといいでしょう。 幸いにして Terraform と違ってこういった処理は圧倒的にやりやすいのでいい感じの方法をとればいいと思います。

// CONSIDER
public class SecurityGroupComponent : ComponentResource
{
    public SecurityGroupComponent(string service, string component, ComponentResourceoptions opts, SecurityGroupComponentArgs args): base($"{service}:components:{component}", $"{service}-{component}", opts)
    {
        args.Validate();

        if (args.EnableA)
        {
            // A の セキュリティグループを作る
        }
    }
}

public record SecurityGroupComponentArgs : IValidate
{
    public Output<string>? VpcId { get; init; }
    public bool EnableA { get; init; }

    [MemberNotNull(nameof(VpcId))]
    public void Validate()
    {
        if (VpcId is null) throw new ArgumentOutOfRangeException(nameof(VpcId));
    }
}

まとめ

つらつらと普段気を付けていることを挙げてみました。 C# に限らず、Pulumiは各種言語のやりやすいように書けばインフラが管理できるのはとても便利です。 Pulumi と Terraform を行き来していると、Terraform の言語機能の貧弱さに驚くとともに、今ある値かを意識することなくかけるのはすごいと感じます。

利用者の多さ、言語機能の小ささから 今後もTerraform は広く使われるでしょうが、アプリケーションエンジニアの立場から見ると Pulumi は非常に扱いやすく設計も応用できるので好ましいと感じます。

Terrafom も Pulumi もよい、チームとしてより書きやすい、手になじむものを採用していけるといいですね。

Pulumi で Stack解析中の例外でリソースが全て消えてしまうのを防ぐ

Pulumi は Stack の解析を行って、現在のステートとの差分でどのような処理をするか preview / up で表示します。 このため、Stack の解析中 (=コードをビルドして実行してStack生成中) に例外が生じたときにどのようにハンドルされるかは重要です。

今回は ユーザー側の誤った記述で、Stack解析に例外が起こっても処理が止まらず実行されたのをメモしておきます。

目次

tl;dr;

  • sdk/dotnet を利用する場合、Program.Main の返り値の型は int または Task<int> にしましょう
  • Top-level statement なら return を忘れずに、return await Deployment.RunAsync<MyStack>(); としましょう

どういう問題なのか

C# でPulumi を記述する場合、多くの場合は Stack を継承した自分の定義を用いるでしょう。 pulumi previewpulumi apply でStack を実行中に例外が発生した場合は、pulumi cli はそれを検知して実行を止めなくてはいけません。 pulumiは 例外を検知すると、「何もStackの内容を実行せず」例外をターミナルに表示して終了します。

しかしProgram.Mainメソッド の返り値の型を int あるいは Task<int> にしていないと、例外が起こっても pulumi cli は実行を継続しようとします。 この状態で例外が起こると、「例外以降のコードで記述されたリソースに削除マーカーを付与」して差分+例外をターミナルに表示して終了します。

// bad
public static async Task Main(string[] args)
{
    await Deployment.RunAsync<MyStack>();
}

// bad (Top-Level statement)
await Deployment.RunAsync<MyStack>();

Issue が作られておりそちらを見ると詳しくわかります。

github.com

対処方法は前述のとおり、Mainメソッドの返り値を intTask<int> にしましょう。

// ok
public static async Task<int> Main(string[] args)
{
    return await Deployment.RunAsync<MyStack>();
}

// ok (Expression-bodied Method)
public static async Task<int> Main(string[] args) => await Deployment.RunAsync<MyStack>();

// ok (Top-Level statement)
await Deployment.RunAsync<MyStack>();

何がおこったのか

Pulumi は言語のビルドは pulumi cli と切り離されているので、何気に .NET 6 でもビルド、実行できたりします。 その際にうかつにも、await Deployment.RunAsync<MyStack>(); と return を忘れて書いてしまったために、作成してあったリソースの多くが消えるという目にあったのでした。

// missing return!!
await Deployment.RunAsync<MyStack>();

Pulumi は GitHub Aapp あるいは GitHub Actions で PR で差分を表示できるのですが、GitHub Actions でコメントを書くのを使っていたために、差分が埋もれてしまい気づけなかったという顛末です。

対処は return をつけるだけです。 誰かの役に立つと幸いです。

Pulumi に期待すること

例外起こったら Environment.ExitCode = -1 など適切な終了コードをセットしてほしいです。 現在はそういったことをしていないのを、終了コードを Main メソッドで示さないと死ぬので基盤で対処してほしい...。

github.com

2021年を振り返って

毎年やっている昨年の振り返りをしてみます。2020年忘れてました。

2019年はこれ。

tech.guitarrapc.com

総合

2021年は、2020年にやったことをさらに深めていく年でした。 2019年からやっていることを深く広くしていくことが求められており、相変わらず毎日新しいことを学んでいます。クライアントにフルコミットが必要で、この自社の時間を設けるのが難しくて頭が痛い問題が解消しませんでした。

サービス基盤を全面的にみているのですが、基盤を安定させつつ、改善したり抜本的な変更をしていくことを継続的に行っています。省力で行うため2019年から IaC で100%管理を続けており、かなり有効なのでおすすめです。Kubernetes + IaC じゃないと手が足りなかったので選択は正解なのですが、相談できる仲間が足りないと感じていて2022年はかなり頭を痛めそうです。

2021年もわからないことへの挑戦の連続でしたが、教わったり助けてもらいつつ分からないことを1つずつ潰せもしました。来年以降の自分に投げつけた課題もありますが、今年一年みても、寝かせると問題が問題でなくなったり、問題への解決方法を見つけたりするので、優先順位をつけて寝かせるのは大事。

経営

2021年は、あとはリリースというところまで進めたプロダクトをリリースしない決定をしており、なかなかリリースできないというのを痛感しています。 自社よりも他から請け負っている仕事に力が使われているのは、2020年より強くなっており反省しかない。

2022年は2021年の反省を生かさないといけない年になります。

プログラミング

C#、Golang、TypeScript がメインです。

CSharp

2021年、C# でインフラからサーバー、クライアントまで完結できるかと思って取り込んでいたのですができるものですね。 インフラは Pulumi や aws-cdk を使うのですが、おおむね terraform でできることは Pulumi でカバーできます。

C# 10 が出て、C# はかなり手触りが良くなりました。 Minimal API と File scope namespace 、Implicit Global Namespace で本当に書くのが楽になりました。

IaC

Terraform と Pulumi をメインに使い、Bicep も使っています。 Azure で IaCをちゃんとやろうと思ったら、残念ながら Bicep は選んではいけないという学びが悲しかったです。

Pulumi は Pulumi Native が出たことで、Upstream である Terraform の対応が入らないと Pulumi の対応が入らないという悪循環が解消されつつあります。とはいえ Pulumi Native は Beta なだけあって Beta な品質で、特にステートとリソースの協調がまだまだ足りない感じです。(Tag が順不同で実行ごとに変更かかる、変更が安定して適用されないなどプロダクション品質ではない)

Terraform は 1.1 がかなりいい感じで、ようやく state mv が改善されました。このままよくなってほしいけど、Pulumi の State move はクソなのであっち方向にはいってほしくないところ。

dev.classmethod.jp

Terraform の CI/CD は、さんざん Terraform Cloud の VCS 連携を使っていましたが、API ベースでの連動に切り替えました。変なことできないという意味ではVCS連携がいいのですが、どうしても GitHub PR との連動が弱いので、バランスを見ると API ベースがいいですね。 とくに tfcmtgithub-comment の組み合わせはいい感じなので、おすすめできます。

Pulumi の CI/CD は GitHub Actions の v3 が出たのですが、安定して微妙な使い勝手です。Dockerベースの頃より格段にいいのですが、tfcmt のような楽な使い勝手じゃないのでなんとも微妙。

Kubernetes

Kubernetes と YAML の地獄、どうしようもないのだろうかと真剣に悩んでいます。 Kustomize や helm という問題ではなく、YAML を書くのがしんどい、とはいえ DSL 的に他から出すのもブラックボックスで嫌で詰んでる。

記事

17本、少な。 あんまり言語的なのを書かなかったのですが、社内やお手伝い先でさんざん書いていてちょっとブログに起こすのがしんどかったりします。

書くことはあるけど、またこっちにも書くのかとなっているのがよくないので反省です。

ライフスタイル

コロナ大変です。 2021年10月から少し収まったので久々に外出しましたが、好きなところに行くのは楽しいものです。

家の中は、吊り収納にはまっています。水回りは徹底的に浮かしているので、掃除がほぼ不要になって最高です。

スタンディングデスクを使ったことでメリハリも効いてきたので、2022年はよりよく頑張っていきたいところ。

2022年は?

クライアントとの協業は年々強くなっています。期待に応えていきたいところ。 そして自社のリリースも。

個人的にはC# + Kubernetes がどうなるか 2022年はカギになりそうです。