tech.guitarrapc.cóm

Technical updates

Pulumi のコンセプト - プログラミングモデル

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

qiita.com

Getting Started を一日目で見たので、Pulumi で C# を使ってそもそもどういう風に書けばいいのかコンセプトを把握します。

疑問はそれから順次書きながら解消していきましょう。

目次

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 プレビューではサポートされていない

基本構造

Programming Model

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

Auto-naming: Programming Model

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

URNs - Programming Model

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));
});

REF

Programming Model

Windows開発環境の構築をChocolateyからscoopに切り替える

この記事は、PowerShell Advent Calendar 2019の一日目です。

qiita.com

Windows のパッケージマネージャーの裏はPowerShellが多く使われています。

そんなWindows におけるパッケージマネージャーと言えば、Package Management Chocolatey が有名なのではないでしょうか?

私もChocolatey をパッケージマネージャーに用いて開発環境の構築をしてきましたが、課題が多かったため Scoopに切り替えました。

Chocolatey で何が問題だったのか、なぜscoop を選んだのか、この一年 scoop をどのように環境構築に利用しているのかを紹介したいと思います。

目次

TL;DR

Chocolatey から scoop に切り替えることで開発環境の自動化、ほとんどのアプリケーションをユーザー権限でのインストールに抑え込むことが可能になりました。

scoop に切り替えて得られたものをざくっと紹介します。

  • UACの不要なインストール
  • 簡単なアップグレード
  • 常に最新環境への追随
  • 確実なアンインストール
  • アプリケーションのパス均一化
  • ローカル開発環境構築の自動化

将来的にmsix が広く使われるようになったら、Scoopより利用しやい仕組みになるとうれしいです。

Chocolatey の利用

Scoop に切り替えるまでChocolatey を使っていました。長年利用して感じていた課題を見てみます。

どのように Chocolatey を利用していたのか

2014年にDSCを自動化しようとしていることから、Chocolateyとは5年の付き合いです。2018年までは個人、会社の開発環境構築にChocolatey を使っていました。

Chocolatey は、Windows環境でのパッケージリポジトリの提供とCLI インターフェース choco を通して、Windows においてコマンドラインからアプリケーションをインストールする体験を提供します。

chocolatey.org

最近聞かなくなりましたが、Package Management のバックエンドとすることで「リポジトリの1つとみなして利用」したり、「独自リポジトリにChocolateyフォーマットのnugetパッケージを置いてチームのプライベートリポジトリとする」こともできます。

前職においては、Unityのリリースを検知、自動的にNuGetパッケージを生成、社内NuGetサーバーに配置、Package Managementを使ってChocolateyパッケージをインストール可能な状態にしていました。こうすることで、Unityのインストールパスをチームで共通化し、Unityインストールの最大の敵であるダウンロードの遅さ、パッケージの分かりにくさに対応していました。

今なら Unity Hub があるので不要です。(当時は Unity Hubがなかった)

Chocolatey で困ること

Chocolatey で開発環境を構築したものの困ることがあります。アンインストールとUACです。

アンインストール

Chocolatey は、MSI や exe 形式のインストールをchocolateyinstall.ps1で扱い、アンインストールをchocolateyuninstall.ps1 で扱います。しかしその実体は地道にインストーラをたたいており、実行の待ち受けやパスで失敗したり、インストールで行っていたWindows Serviceやレジストリは消し忘れていたりします。また、そもそも多くのパッケージでアンインストールスクリプトが提供されていません。

Chocolatey でも最大規模のインストールパッケージである、git.install のアンインストール処理でもこんな感じです。

chocolatey.org

gist.github.com

Chocolateyを構築時だけに使うという割り切りならいいのですが、開発環境はどんどん変わっていきます。当然アンインストールをする機会も少なくないでしょう。アンインストールが不安定だったり、未提供だとインストールとは非対称に以前のWindowsでのアプリケーション管理をすることになります。

使ってきた経験から言えるのは、残念ながらChocolatey でインストールしたものであってもWindowsのApps & Feature でアンインストールが必要なものがほとんどです。

私はChocolateyで今後もやっていくという判断ができませんでした。アンインストールはスパッと終わらせたいです。

UAC

UAC はWindowsでコマンドラインを使っていて感じるだるいと感じる筆頭かもしれません。Chocolatey でも残念ながらUACを求めてくるパッケージは多く存在します。 自動化するためのコマンドラインでchocolateyを使ってアプリケーションインストールの実行、席を立って戻ってみるとUACで止まっていた。という経験をお持ちの方もいるのではないでしょうか?

そもそもchoco installなどをするときに管理者に昇格したcmdやPowerShellを使うように言われます。

UACにはサンドボックスとしての安全さの提供という側面はあるものの、コマンドラインで作業していていきなりUACが表示されマウスクリックが必要だったりするのはストレスです。特にこれはLinux/macOS でコマンドラインによる操作に慣れていると顕著に感じるかもしれません。*1

Chocolatey が抱える問題点

Chocolatey が抱えている体験上の問題は2つに起因しているように思います。

  • アンインストールの不完全な提供
  • UACポップアップ

アンインストールに関しては、Chocolatey自体が MSI や exe といった各型式の実行ファイルを開発者が提供するスクリプトでなんとかする方式である以上避けられないように思います。Chocolatey 自身もValidation Program やChocolatey Install/uninstall/WebFile など各種サポート関数を提供していますが、たとえこれを使っても、Windows において回避するのは難しい側面があるのは否めません。

UAC も、MSIを使う以上シカタナイでしょう。%ProgramFiles% は特権ユーザーでないとアクセスできないので。しかし可能であればアプリケーションインストールごときでUAC昇格はしたくないですし、環境によっては厄介に感じます。

Chocolatey つかれました。

Scoop への切り替え

Scoop は Chocolatey とはコンセプトを別にするコマンドラインインストーラーです。

Looking for familiar Unix tools? Tired of Powershell’s Verb-Noun verbosity? Scoop helps you get the programs you need, with a minimal amount of point-and-clicking.

https://scoop.sh/scoop.sh

Homebrew 的に、常に最新のアプリやUnix Toolを、UACの縛りなく、さくっと利用できるとうれしい。そういう使い方を想定されています、まさに私にはぴったりでした。

Scoop installs programs to your home directory by default. So you don’t need admin permissions to install programs, and you won’t see UAC popups every time you need to add or remove a program.

Scoopのインストール

PowerShell からワンライナーでインストールできます。

Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')

# or shorter
iwr -useb get.scoop.sh | iex

使い方は scoop help でわかります。

$ scoop help

Usage: scoop <command> [<args>]

Some useful commands are:

alias       Manage scoop aliases
bucket      Manage Scoop buckets
cache       Show or clear the download cache
checkup     Check for potential problems
cleanup     Cleanup apps by removing old versions
config      Get or set configuration values
create      Create a custom app manifest
depends     List dependencies for an app
export      Exports (an importable) list of installed apps
help        Show help for a command
hold        Hold an app to disable updates
home        Opens the app homepage
info        Display information about an app
install     Install apps
list        List installed apps
prefix      Returns the path to the specified app
reset       Reset an app to resolve conflicts
search      Search available apps
status      Show status and check for new app versions
unhold      Unhold an app to enable updates
uninstall   Uninstall an app
update      Update apps, or Scoop itself
virustotal  Look for app's hash on virustotal.com
which       Locate a shim/executable (similar to 'which' on Linux)


Type 'scoop help <command>' to get help for a specific command.

Scoop でアプリを利用する

sudo や git など、Windows にデフォルトで入ってほしいけど入っていないCLIツールは、まさに Scoop が向いています。

scoop install git

まとめてインストールすることもできます。

scoop install 7zip sudo git jq time unzip openssl

インストールしたアプリは、jq などアプリ名でそのまま利用できます。

$ jq --version
jq-1.6

インストールされているアプリケーションも一覧表示できます。

scoop list

アンインストールも簡単です。 プロセスがロックされていない限りは確実にアンインストールできるので安心できます。*2

scoop uninstall git

Scoop のアプリを検索する

利用可能なアプリは、scoop search で探すことができます。

GitHub にまとまっているので、そこを見て探してもいいでしょう。

github.com

Scoop のアプリをよりたくさんインストールする

Scoop は、アプリのダウンロード元に GitHub を利用しており これを Bucket と呼んでいます。 インストール直後は Main Bucketのみがインストールされています。

scoop bucket list

Bucket はscoop bucket add で追加できます。 Bucketを追加することで多くのアプリケーションをscoopでインストールできるようになります。 例えば、公式で提供されているExtra Bucket は、 Main Bucket にはないけどよく利用されるアプリケーションが含まれています。

scoop bucket add extras

extras Bucketの追加で gitkraken などもscoopでインストール可能になります。

scoop install gitkraken

インストールするときに、Bucket を気にしなくていいのでかなり楽です。

scoop にしたことで混乱したこと

scoopの特性を把握せずにいたときに混乱したこともあります。

  • 複数のパスに実行ファイルが存在することによる意図しない動作
  • グローバルパスに存在が期待されるソフトウェアの継続的な更新の困難さ

例えば、.NET Core SDK は Visual Studio でも入ったりし、あらゆるツールが利用しているため scoop でインストールすることは向きません。

「何をscoopで入れて、何を入れない。」という選択を自分で行う必要があるので、そういう意味では誰にでもオススメできるわけではありません。 自分で選んだ結果を自分で受け入れ、対応していける人向けだと思います。

Scoop で過去のバージョンのアプリをインストールする

Scoop は .env のような制御はないので素直に使うなら、常に最新を使うのが楽です。(scoop install / scoop update していくだけ)

もし過去バージョンを利用する場合、scoop reset で可能です。

scoop reset <app>@<version>

github.com

もし過去バージョンがたまっているversions Bucket にアプリがあるならこれを使うのも手でしょう。

scoop bucket add versions

github.com

私は過去バージョンを Scoop で利用することはしません。するぐらいならScoopやめる。

Scoop を継続的に使っていくための工夫

scoop でパッケージがインストールができるといっても、アプリをインストールするコマンドをずっと書き連ね、そのコマンドをスクリプトにしたりするのはつらいものがあります。

scoop install xxx
scoop install yyy
scoop install xxx

やりたいのは、アプリのインストールで scoopがいい感じにしてくれればなんでもいいのです。 ということで、こんなYAMLを書くことで Ansibles like に Scoop のアプリをインストール/アンインストールできる仕組みがほしいです。

- name: "Install linux tools"
  scoop_install:
    state: present
    bucket: main
    name:
      - cmake
      - gcc
      - gow
      - jq
      - sudo
      - time
      - unzip
      - openssl
      - apache

- name: "Install windows tools"
  scoop_install:
    state: present
    bucket: main
    name:
      - 7zip
      - aws
      - bat
      - chromedriver
      - git
      - git-lfs
      - ngrok
      - pwsh
      - terraform
      - gibo
      - helm
      - mysql-workbench

YAMLでの定義を実現するためのPowrShell モジュール ScoopPlaybook を作ったので次回紹介しようかと思います。

github.com

TIPS: Scoop のアプリ実体

余り意識する必要がないのですが、興味のある人向けです。

scoop でインストールされたアプリは、基本的に ~\scoop\shims\アプリ名.EXE のパスに存在します。shims でわかる通り、これらは ~/scoop/apps/アプリ名/current を参照しており、アプリケーションのインストールと利用が分離されています。

scoopのshimsフォルダー

apps のアプリディレクトリを見ると、バージョンフォルダと current が存在し、current は常に最新バージョンのシンボリックリンクになっています。

shims が参照しているappsのディレクトリ

まとめ

Chocolatey はインストールの1回に限れば便利です。が、今後もモデル的にアンインストールやUACが改善するには msix が来ない限り難しいでしょう。 Scoop を使うと、Unix Toolやsれ以外のツールの多くもいい感じでインストール、アンインストールできます。

私もScoopの独自Bucketでフォントとかいくつかパッケージを公開しているので、独自のアプリケーションを Scoop で配布することもいずれ記事にします。

*1:私はGUI操作時よりコマンドライン操作時の方がUACをストレスに感じます

*2:Windowsなのでプロセスが実行中はファイルがロックされます

Pulumi の .NET Coreの Preview リリースとGetting Started

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

qiita.com

Pulumi の .NET Core 対応版が Preview リリースされました。

Getting Startedから、実際に環境を構築したり疑問点の解決を回を重ねながらみていきましょう。

目次

TL;DR

実際にPulumi x .NET Core 3.0 (C#) で使ってみていますがそのまとめをざっくり。

  • まだ Preview なので、TypeScript や Python と比べてできない機能も数多い
  • 動作もいくつかバグが残っていたりして、NuGet のPreview パッケージが週一でどんどん改善されている
  • 現状は Visual Studio で実行まで完結するわけではないので注意
  • 型は偉大、terraformのようにドキュメントを見る手間が格段に減った
  • 型でわからないときでも terraform のドキュメントをみればok
  • ドキュメントのメソッドやクラスは結構古かったりする

Getting Started はいい出来にはなっているので見てみるといいでしょう。

Pulumi Getting Started

これに基づいて進めていく。

https://www.pulumi.com/docs/get-started/

Login

まずは何はともあれアカウントを作る。

https://app.pulumi.com/signin

アクセストークンを作ることで、pulumi cli とアカウントが紐づいて操作できるようになる。

https://app.pulumi.com/guitarrapc/settings/tokens

事前準備

install .NET Core 3.0 SDK

Pulumi の実行は、コンパイルしてから実行されます。 このコンパイル、実行のために .NET Core 3.0 SDK が必要です。

https://dotnet.microsoft.com/download

Install Pulumi cli

windows

scoopに対応したので、これでpulumi cliを入れましょう。main bucket で利用できます。(scoop インストールしたらすぐにつかえるということです)

scoop install pulumi

cli でログインします。これでCLIが Webと紐づいて、ログが記録されたりリソースが可視化されます。

pulumi login

macOS

Homebrew でどうぞ。

brew install pulumi

更新は upgrade で。

brew upgrade pulumi

linux

curl で拾ってきて、実行するだけです。

curl -fsSL https://get.pulumi.com | sh

アクセストークン の生成方法

3つあります。アクセストークンを管理したくないので、自動生成がお勧めです。

  • (recommended) pulumi login でログインしたら、何も入力せずに Enterします。ブラウザが立ち上がるので、ログインすると自動的にアクセストークンが生成されます

  • アクセストークンを pulumi loginで入れる
  • アクセストークンを PULUMI_ACCESS_TOKEN 環境変数に入れる

Language Support

  • GA: TYPESCRIPT, JAVASCRIPT, PYTHON
  • PREVIEW: C# (.NET Core 3.0 SDK or later.), Golang

Configure AWS

AWS を操作する前に、認証だけ解決しておくこと。 default プロファイルを使わない場合、権限のある User を以下の環境変数に入れておくのが手っ取り早い。

  • AWS_PROFILE

Visual Studio の Launch Profile で 環境変数に入れてもok です。 pulumi up でdotnet build されるので、それが使われます。

Create a New Project

AWS C# project を作成します。

mkdir pulumi
cd pulumi
pulumi new aws-csharp

デフォルトだと次のようになる。

> pulumi new aws-csharp

This command will walk you through creating a new Pulumi project.

Enter a value or leave blank to accept the (default), and press <ENTER>.
Press ^C at any time to quit.

project name: (pulumi)
project description: (A minimal AWS C# Pulumi program)
Created project 'pulumi'

Please enter your desired stack name.
To create a stack in an organization, use the format <org-name>/<stack-name> (e.g. `acmecorp/dev`).
stack name: (dev)
Created stack 'dev'

aws:region: The AWS region to deploy into: (us-east-1) ap-northeast-1
Saved config

Your new project is ready to go!

To perform an initial deployment, run 'pulumi up'

これで自動的に csproj や Program.cs、 yaml が生成される。

$ ls -la
drw-rw-rw-  6 S07671 0 4096 2019-11-13 10:21 .
drw-rw-rw-  6 S07671 0 4096 2019-11-12 18:33 ..
-rw-rw-rw-  1 S07671 0 6401 2019-11-12 18:41 .gitignore
-rw-rw-rw-  1 S07671 0  318 2019-11-12 18:41 Infra.csproj
-rw-rw-rw-  1 S07671 0 1115 2019-11-12 19:00 Infra.sln
-rw-rw-rw-  1 S07671 0 1357 2019-11-13 10:21 Program.cs
-rw-rw-rw-  1 S07671 0   37 2019-11-12 18:41 Pulumi.dev.yaml
-rw-rw-rw-  1 S07671 0   74 2019-11-12 18:41 Pulumi.yaml

ファイルは次の通り。

  • Pulumi.yaml defines the project
  • Pulumi.dev.yaml contains configuration values for the stack we initialized
  • Program.cs is the Pulumi program that defines our stack resources. Let’s examine it
name: pulumi
runtime: dotnet
description: A minimal AWS C# Pulumi program
config:
  aws:region: ap-northeast-1
using System.Collections.Generic;
using System.Threading.Tasks;

using Pulumi;
using Pulumi.Aws.S3;

class Program
{
    static Task<int> Main()
    {
        return Deployment.RunAsync(() => {

            // Create an AWS resource (S3 Bucket)
            var bucket = new Bucket("my-bucket");

            // Export the name of the bucket
            return new Dictionary<string, object>
            {
                { "bucketName", bucket.Id },
            };
        });
    }
}
1.8.0

仮実行とエラー解消

この状態で pulumi up とするとスタックがどう生成されるか表示される。

Config に aws:region の設定が抜けているとエラーが発生する

と思いきや、エラー。

$ pulumi up
Previewing update (sandbox):
     Type                 Name            Plan     Info
     pulumi:pulumi:Stack  pulumi-sandbox           'dotnet build -nologo .' completed successfully
 ms
 +   pulumi:pulumi:Stack          pulumi-sandbox  create     1 error; 2 messages
����ł����B                Name            Plan       Info
 +   └─ pkg:component:ekscluster  sandbox         create

Diagnostics:
  pulumi:pulumi:Stack (pulumi-sandbox):
    �N���v���t�@�C�� "(����)" ���K�p�ł��܂����ł����B
    �g�p�\�ȋN���v���t�@�C�����������܂����ł����B

    error: Running program 'C:\git\infra\pulumi\sandbox\bin\Debug\netcoreapp3.0\Infra.dll' failed with an unhandled exception:
    System.NullReferenceException: Object reference not set to an instance of an object.
       at Pulumi.Deployment.InvokeAsync[T](String token, ResourceArgs args, InvokeOptions options, Boolean convertResult)
       at Infra.EksClusterComponent.CreateAsync() in C:\git\infra\pulumi\sandbox\EksClusterComponent.cs:line 25
       at Program.<>c.<<Main>b__0_0>d.MoveNext() in C:\git\infra\pulumi\sandbox\Program.cs:line 39
    --- End of stack trace from previous location where exception was thrown ---
       at Pulumi.Stack.RunInitAsync(Func`1 init)
       at Pulumi.Output`1.GetValueAsync()
       at Pulumi.Deployment.RegisterResourceOutputsAsync(Resource resource, Output`1 outputs)
       at Pulumi.Deployment.Runner.WhileRunningAsync()

エラーは、System.NullReferenceException: Object reference not set to an instance of an object.。リソースの読み取りを使用とする時点で怒られる。

原因は、pulumi で AWS の Region情報 aws:region を config に設定していないため。 なので、Config設定する。

$ pulumi config set aws:region ap-northeast-1

AWS Profile の指定が抜けているとエラーが発生する

config にaws:region の設定後、再実行するとまた失敗する。

$ pulumi up
Previewing update (dev):
     Type                 Name        Plan     Info
     pulumi:pulumi:Stack  pulumi-dev           .NET Core ���� Microsoft (R) Build Engine �o�[�W���� 16.3.0+0f     pulumi:pulumi:Stack  pulumi-dev             C:\git\xxx\infra\pulumi\Infra.csproj �̕����� 45.39 ms �Ŋ��     pulumi:pulumi:Stack  pulumi-dev           'dotnet build .' completed successfully
[resource plugin aws-1.8.0] installing
Downloading plugin: 61.24 MiB / 61.24 MiB [=========================] 100.00% 5s
Moving plugin... done.

     Type                 Name        Plan       Info
 +   pulumi:pulumi:Stack  pulumi-dev  create
     └─ aws:s3:Bucket     my-bucket              1 error

Diagnostics:
  aws:s3:Bucket (my-bucket):
    error: unable to discover AWS AccessKeyID and/or SecretAccessKey - see https://pulumi.io/install/aws.html for details on configuration

エラーは error: unable to discover AWS AccessKeyID and/or SecretAccessKey - see https://pulumi.io/install/aws.html for details on configurationなのでAWSプロファイルがないことを示す。

ということで、aws configure でdefault profile の認証を設定する。

https://www.pulumi.com/docs/intro/cloud-providers/aws/setup/

もし複数プロファイルがあり、任意のプロファイルを参照したい場合は、AWS_PROFILE 環境変数にプロファイルを指定する。

リソースを作成してみる

これで pulumi up すると、yes / no が選択できる。 yes でリソースが作成、削除される。

$ pulumi up

Previewing update (dev):
     Type                 Name        Plan     Info
     pulumi:pulumi:Stack  pulumi-dev           .NET Core ���� Microsoft (R) Build Engine �o�[�W���� 16.3.0+0f     pulumi:pulumi:Stack  pulumi-dev             C:\git\infra\pulumi\Infra.csproj �̕����� 34.9 ms �Ŋ��      pulumi:pulumi:Stack  pulumi-dev           'dotnet build .' completed successfully

     Type                 Name        Plan
 +   pulumi:pulumi:Stack  pulumi-dev  create
 +   └─ aws:s3:Bucket     my-bucket   create

Resources:
    + 2 to create

Do you want to perform this update? yes
Updating (dev):
     Type                 Name        Status     Info
     pulumi:pulumi:Stack  pulumi-dev             .NET Core ���� Microsoft (R) Build Engine �o�[�W���� 16.3.0+     pulumi:pulumi:Stack  pulumi-dev               C:\git\infra\pulumi\Infra.csproj �̕����� 32.92 ms �Ŋ�     pulumi:pulumi:Stack  pulumi-dev             'dotnet build .' completed successfully

     Type                 Name        Status
 +   pulumi:pulumi:Stack  pulumi-dev  created
 +   └─ aws:s3:Bucket     my-bucket   created

Outputs:
    bucketName: "my-bucket-626a03e"

Resources:
    + 2 created

Duration: 9s

Permalink: https://app.pulumi.com/guitarrapc/pulumi/dev/updates/1

リソースを見てみると、後ろにハッシュがつくことで名前の重複が防がれていることがわかる。

リソースを削除してみる

消すと、pulumi up で実行したときに消える

            // Create an AWS resource (S3 Bucket)
            //var bucket = new Bucket("my-bucket");
> 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
 -   └─ aws:s3:Bucket     my-bucket   delete

Outputs:
  - bucketName: "my-bucket-626a03e"

Resources:
    - 1 to delete
    1 unchanged

Do you want to perform this update? yes
Updating (dev):
     Type                 Name        Status     Info
     pulumi:pulumi:Stack  pulumi-dev             'dotnet build .' completed successfully
�����
     Type                 Name        Status
     pulumi:pulumi:Stack  pulumi-dev
 -   └─ aws:s3:Bucket     my-bucket   deleted

Outputs:
  - bucketName: "my-bucket-626a03e"
    hogemoge  : "piyopiyo"

Resources:
    - 1 deleted
    1 unchanged

Duration: 5s

Permalink: https://app.pulumi.com/guitarrapc/pulumi/dev/updates/3

余談: VS での F5 実行

F5 で実行はまだ無理げ。

Complete Support for .NET · Issue #3470 · pulumi/pulumi https://github.com/pulumi/pulumi/issues/3470

VS上のPulumi_Monitor とかも環境変数仕込むと gRPC のchannelがフックされるのでいったんなしがよさそう。

new Pulumi.Aws.Provider() で指定できる? 例えば、pulumi-sandbox profile の ap-northeast-1 region ならこう。-> profile 参照されてないときと同じエラーでダメだった。

        new Pulumi.Aws.Provider("sandbox", new Pulumi.Aws.ProviderArgs()
        {
            Profile = "pulumi-sandbox",
            Region = "ap-northeast-1",
        });

さらにリソースを追加

KMS を追加してみる。

    static Task<int> Main()
    {
        return Deployment.RunAsync(() => {

            // Create a KMS Key for S3 server-side encryption
            var key = new Pulumi.Aws.Kms.Key("my-key");

            // Create an AWS resource (S3 Bucket)
            var bucket = new Pulumi.Aws.S3.Bucket("my-bucket", new Pulumi.Aws.S3.BucketArgs
            {
                ServerSideEncryptionConfiguration = new Pulumi.Aws.S3.Inputs.BucketServerSideEncryptionConfigurationArgs
                {
                    Rule = new Pulumi.Aws.S3.Inputs.BucketServerSideEncryptionConfigurationRuleArgs
                    {
                        ApplyServerSideEncryptionByDefault = new Pulumi.Aws.S3.Inputs.BucketServerSideEncryptionConfigurationRuleApplyServerSideEncryptionByDefaultArgs
                        {
                            SseAlgorithm = "aws:kms",
                            KmsMasterKeyId = key.Id,
                        },
                    },
                },
            });

            // Export the name of the bucket
            return new Dictionary<string, object> {
                { "bucket_name", bucket.Id },
            };
        });
    }

追加される。

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
 +   ├─ aws:kms:Key       my-key      create
 +   └─ aws:s3:Bucket     my-bucket   create

Outputs:
  + bucket_name: output<string>
  - hogemoge   : "piyopiyo"

Resources:
    + 2 to create
    1 unchanged

Do you want to perform this update?

kms を見ると、name に指定した文字列がどこにもないのが気になる。

stack destroy

$ pulumi destroy

Previewing destroy (dev):

     Type                 Name        Plan
 -   pulumi:pulumi:Stack  pulumi-dev  delete
 -   ├─ aws:s3:Bucket     my-bucket   delete
 -   └─ aws:kms:Key       my-key      delete

Outputs:
  - bucket_name: "my-bucket-c3f5c37"

Resources:
    - 3 to delete

Do you want to perform this destroy?
  yes
> no
  details
Duration: 24s

Permalink: https://app.pulumi.com/guitarrapc/pulumi/dev/updates/5
The resources in the stack have been deleted, but the history and configuration associated with the stack are still maintained.
If you want to remove the stack completely, run 'pulumi stack rm dev'.

TODO

Getting Started の中で幾つもわからないことが出てきました。 次回から順にみていきましょう。

  • pulumi up 時の文字化け対策
  • Pulumi Web UI でできること
  • 後ろのハッシュはずすのどうするの?
  • IAM Document Policy のような JSON をいい感じで型から生成できるの?
  • リソース作成後の型が Output<string> だけどいい感じに変換できるの?
  • Terraform Module 的な仕組みどうするの?
  • 既存リソースの import どうするの?
  • よく使うコマンドは?
  • state ってどうなってるの?
  • 他のstate 参照どうするの?
  • VS 上 で F5実行のステータスどうなってる?

CircleCI でdotnet test が遅い問題と対処方法

この記事は、C# その2 Advent Calendar 2019 の一日目の記事です。

qiita.com

C# で書いたアプリをLinuxやコンテナ環境で動かすことも当たり前になりました。コンテナイメージを作成するのにCircleCI は非常に便利な機能が多く、私も多くの場面で CircleCI をCI環境として第一に選ぶことが多いです。

さて、CircleCI で dotnet sdk を使ってDocker image でビルドを行っていると、「手元で1秒で終わるテストが30秒余りかかったりタイムアウトする」状況に出会うことがあります。

今回はこの「CircleCI で dotnet test の実行が遅い問題」を適切な時間で終わるようにする方法についてみてみましょう。

目次

TL;DR

dotnet test の実行時に < /dev/null 付けて標準入力に null を与えることで CircleCI でのみテストが遅い問題が解消する。

ローカルでは早いテストがCircleCIでのみ遅い状況になった場合は、dotnet test < /dev/null としてみて下さい。

手元では次のグラフのように20秒程度改善し、restore/buildを含めると妥当な実行時間になりました。

.NET Core 3.0 dotnet test compare on CircleCI

環境

CircleCI での dotnet core ビルドなので、Docker Image で行います。 ビルドする環境なので、dotnet core sdk イメージ上で dotnet test を実行します。

  • dotnet core: 3.0 1
  • Docker Image: mcr.microsoft.com/dotnet/core/sdk:3.0

dotnet test で行っていること

先に dotnet test が行っていることを把握しておきましょう。

dotnet test コマンドは、まだビルドしていないプロジェクトの場合は、restore + build + test が実行されます。

もし事前に restoreやbuildを行っている場合、それぞれオプション引数の --no-restore--no-build を使うことでスキップできます。

これらのオプションを指定した場合、nuget packageが restoreされていない、テストに利用する対象 dll といったスキップするべきではない状況ではテストが失敗するので使いどころは選びます。

dotnet test command - .NET Core CLI | Microsoft Docs

つまり、もしCircleCI でテストが時間かかると思っても、restore / build の分は当然時間がかかります。

この記事の問題は、test で時間がかかることが判明している状況です。

どのような問題なのか

.NET Core 3.0 の C# プロジェクトで、ローカル環境の dotnet test や Visual Studio の Test Explorer で実行してみると1sec 以内に終わるテストがあるとします。

このテストを、CircleCI で Dockerイメージ上で dotnet test すると数十秒余計に時間がかかるのが問題です。 なお、dotnet SDK バージョンによっては即時に終わる中身のないテストであってもdotnet test が終わらずタイムアウトします。(CircleCI の無出力時のデフォルトタイムアウト 10min 以上かかっても終わらない)

CircleCI の config.yml は次のようなニュアンスのものです。

version: 2.1

executors:
  dotnetcore3:
    docker:
      - image: mcr.microsoft.com/dotnet/core/sdk:3.0
    environment:
      DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "true"
      NUGET_XMLDOC_MODE: skip

workflows:
  dev:
    jobs:
      - build

jobs:
  build:
    executor: dotnetcore3
    steps:
      - checkout
      - run: dotnet build
      - run: dotnet test -c Debug

問題の再現するC# プロジェクトのサンプルとして次のプロジェクトがあります。(すでに対策された config.yml がコミットされています)

github.com

問題のCircleCI のテスト結果は次の通りです。

https://app.circleci.com/jobs/github/tasadar2/vstest-issue-2080/11

NOTE: なお、無出力の問題ではないので dotnet test -v n でログをたれ流せばいいかというとそういうわけではありません。

コミュニティの状況

この dotnet test がやけに時間かかる問題は、CircleCI 1.0 のころから CircleCI のユーザーフォーラムでちょくちょく言われていたものです。

幸いにして今年6月 (2019/June) に microsoft/vstest リポジトリで Issue が立って、対処方法が練られています。

github.com

本状況ですが、docker image のバージョンによってずれがあります。

  • mcr.microsoft.com/dotnet/core/sdk:2.2.204 では再現されず
  • mcr.microsoft.com/dotnet/core/sdk でタイムアウトが発生し dotnet test が完走しない
  • mcr.microsoft.com/dotnet/core/sdk:3.0 では 数十秒遅延する

手元のプロジェクトは.NET Core 3.0 で作っており、CircleCI のビルド時間を見ていた時にやけに dotnet test がローカルよりも時間がかかっていることから見つかりました。

対処方法

実はこの問題は、vstest のIssue 前から、x=$(dotnet test); echo $x とすると回避できるというワークアラウンドが見つかっていました。 ただこの方法で回避すると、テスト結果表示がぐちゃぐちゃになるので推奨できません。

またこの回避方法があることから、Console 出力において何かしら問題があることはわかっていました。2

Issue 2080 の中で、コミュニティによって回避方法が2つ見つかっています。

  • --logger:"console;noprogress=true" を指定してprogress 切る 3
  • < /dev/null を足して入力がないとみなす

状況から Progress処理に問題がありそうです。

それぞれ見ていきます。

  • --logger:"console;noprogress=true" は、テスト実行中の状況が見えないため余り好ましくないように思います
  • 標準入力に null を渡す < /dev/null を使うのは、テストが滞りなく実行され出力結果にも影響がないので好ましいでしょう

改善結果

手元のプロジェクトで、4つのUnit Testプロジェクトに対して dotnet test を実行したときの結果を共有します。

Compare A UnitTest B UnitTest C UnitTest D UnitTest
Before 00:25 00:37 00:34 00:59
dotnet test .... < /dev/null 00:05 00:17 00:13 00:35

.NET Core 3.0 dotnet test compare on CircleCI

dotnet test< /dev/null を追加するだけでテスト実行時間がやけに遅い問題が解消しています。

注目すべきはどのテストにおいても、before -> after でおおよそ 20sec の改善が見られます。 症状は、dotnet test の実行ログが出た直後に止まって20sec前後 たってから各テストの進捗が出るのでちょうどこの部分が解消されているのが実感と一致しています。

なお、私のほかのプロジェクトで永遠にフリーズしていた問題も解消しています。

結論

CircleCI で dotnet test を実行するときは、dotnet test < /dev/null にしておくと安パイ。

効果がないならほかの原因かもしれないので、これだけが原因と思わず取り掛かるといいでしょう。(今のところ、私はCircleCI 上の dotnet test はこの問題だけ引っ掛かりました)

コード上に具体的に20sec な場所はないものの、Progress の待ち受けがあるところで Infinite なので ServerConnection 待ちあたりのような追い切れていません。

github.com


  1. dotnet core 2.2 でも同様の症状は確認しています
  2. CI上に限らず、特定の環境で時間がかかるときは標準出力部分に原因が多いのはあるある、なのはご存じのとおりです。
  3. ; はシェルの解釈に影響がでるので引数は必ずダブルクォートでくくりましょう

Nature Remo / Nature Remo mini Remo-2W1 の接続安定を試みる

台風21号の風がいい環境音で作業が捗っています。

さて、外出先からエアコンを消す、家に着く前に少しエアコンをつけておきたいなどというシーンがあります。 また、Google Home を使っていると電気機器の動作も声で制御したくなります。

ここ2年ほどNature Remoを使ってこれらの機器を操作していたのですが、ここ数か月ほど15min~8h経つと機器を操作できなくなっているのでそろそろ対処をします。

目次

環境

現在、Nature Remo / Nature Remo mini Remo-2W1 の2つを使っています。 もともと Nature Remo で発生していた時に、この機器だけの問題かと思い切り分けのためにNature Remo mini Remo-2W1を追加しました。(という題目でおもちゃを増やしました)

Amazon | Nature Remo | Nature, Inc.

Amazon | Nature スマートリモコン Nature Remo mini Remo-2W1

無線は、Aterm WG1400HPをブリッジ動作させて、2.4GHz でRemoとつないでいます。

ルータ/DHCPサーバーに、Yamaha RTX810 を利用しています。

症状

寝て起きたり、帰ってくるとRemo が赤く早い点滅を繰り返しており、RemoアプリやGoogle Homeの操作で機器を操作できなくなります。 Remoを再起動するとしばらく使えるのですが、いつのまにかまた使えなくなっているのが毎日続いています。

公式FAQを見る

公式は大事です。Remo のFAQから、この症状はNature Remoがクラウドと通信できていない状態とあります。

Nature Remoの赤点滅には2種類あります。赤く遅い点滅(1秒間に1回の点滅)と赤く早い点滅(1秒間に数回の点滅)です。

Nature Remoが赤く早い点滅(1秒間に数回の点滅)になっている場合は、Nature Remoがクラウドと通信できていない状態になります。お使いのWi-Fiルーターの推奨接続台数が、ご家庭のWi-Fi機器以上かをご確認の上、以下の手順を順番にお確かめ下さい。

  1. 問題が発生した以前にWi-Fiルーターの各種設定を変更してないかをご確認下さい
  2. Nature Remoを付属のケーブルとアダプターを使い再起動してください
  3. Wi-Fiルーターを再起動してください

引用: Q4-2. Nature Remoが赤点滅して正常に動作しない — Nature

https://nature.global/jp/faq/023nature.global

  • ルーターやWifiに変更を加えていません
  • Remo再起動で一時的に解消しますが、毎日再発します
  • Wifiやルーターの再起動しますが解消しません

Twitter やググっても特にその症状で今困っている人はいないことから、Remo サーバーではなく自宅の環境に起因してそうです。 Google Home以外にも、うちの機器はすべてWifiでつなげていますがどの機器も問題がでていないので、Remo特有のようです。

もう少し定番設定がありそうなものですが、公式FAQちょっと貧弱すぎな感じがあります。

Remoやめて他のを使おうかと探したのですが、Remo以上に魅力的なものがないので対処します。

考えうる対策

ぐぐったところ、いくつか考えられそうです。

  1. SSID Stealth を解除する
  2. IPを固定する

準備実施します。

SSID Stealth を解除する

長い間ステルスなので、何故これが対策になるのかわかりませんが、とりあえず従ってもいいので従います。 が、効果ありませんでした。

セキュリティ対策に使っているわけではないのですが、どうもSSIDがブロードキャストされていると使おうとする人がいるのは確かなので、安定したらステルスに戻す予定です。

IPを固定する

Nature Remo はDHCPで取得するので、端末のmacアドレスに対して固定IPを割り当てるように切り替えます。 Atermをブリッジで動かしているうちの場合はRTX810 がDHCPサーバーです。

Remoのmacアドレスを調べてRTX810にコンフィグを入れましょう。(Web UIは対応していません)

Nature Remoはセットアップ済みなので、macアドレスはアプリから確認できます。

https://nature.global/jp/faq/042nature.global

https://nature.global/jp/faq/049nature.global

RTX810に対してコンフィグを入れます。 RTXでDHCPでmacアドレスに対応して固定IPを割り当てるときは、 dhcp scope bind [DHCP_SCOPE_ID] [IP_FROM_DHCP_RANGE] [MAC] の文法を使います。

ちなみに、dhcp scope bind [DHCP_SCOPE_ID] [IP_FROM_DHCP_RANGE] ethernet [MAC] はNature Remoで固定するには有効に機能しなかったので気を付けてください。

www.rtpro.yamaha.co.jp

DHCPの設定はこんな感じでいいでしょう。

dhcp service server
dhcp server rfc2131 compliant except remain-silent
dhcp scope 1 192.168.11.110-192.168.11.150/24
dhcp scope bind 1 192.168.11.141 aa:bb:cc:dd:ee:ff
dhcp scope bind 1 192.168.11.142 11:22:33:44:55:66

設定したら Nature Remoを再起動してIPを取りに行かせます。 syslog で、指定したmacアドレスに対して指定したIPが割り振られたことを確認します。

2019/10/12 23:17:35: [DHCPD] LAN1(port1) Allocates 192.168.11.141: aa:bb:cc:dd:ee:ff
2019/10/12 23:18:10: [DHCPD] LAN1(port1) Allocates 192.168.11.142: 11:22:33:44:55:66

経過

とりあえず投入して30min経ちましたが2台ともに安定しているように見えます。 一晩待って安定するか見てみましょう。

そういえば、今のGoogle Homeは影響ないのでIP固定していませんが、初期のGoogle Homeでも言われていたのを思い出しました。 Nature Remo以外の機器は何の影響もないので、DHCPで影響あるのは手間もありますが品質の違いを感じてちょっと残念です。

10/13 一晩経っても安定しているのでこれでよさそうです。

10/14 残念ながらだめなようです。赤い点滅はでないものの、アプリからの操作、Google Homeからの操作を行っても信号が発信されない。(発信するときに青い点滅があるのでわかる) 再起動で動くものの、繰り返し発生しているので他の原因がありそう。

追加対応

継続して発生するので、どうしたものかとぐぐっていたら、FaceBook Group にNature Remoのユーザーコミュニティがありました。

www.facebook.com

似たような相談がないか見ていると、YAMAHA SoundBar YAS-109 のコントロールアプリで不安定になるとの情報がありました。 確かにYAMAHA公式にもあり、自分も使っているので可能性はありそうです。

yamaha.custhelp.com

ということで、SoundBar Controller アプリを終了してNature Remoを再起動して様子を見てみましょう。

経過2

2日経過して、とりあえず安定しています。 Yamaha YAS-109 は使わない状態で放置.... 文鎮。

経過3

確定したので、Switch Bot に切り替えました。

経過4

SoundBar Controller Ver.1.0.6 で改善したらしい。もう移行済みだよ!

本件、2019年12月18日公開のSoundBar Controller Ver.1.0.6を適用いただくことで改善いたします。