tech.guitarrapc.cóm

Technical updates

PowerShellでマルチプラットフォームに動くモジュールの作成と継続的なテスト

この記事は、PowerShell Advent Calendar 2018 の 22日目です。

qiita.com

今年は、PowerShell Coreについて本を書いたのですが、その中で書ききれなかった.NET Core と .NET Framework の両方で動くPowerShellモジュールの実用的なサンプルです。

目次

目的

このサンプルを作った目的は、単純に自分が書いていて自分のために用意した環境が汎用的に使えるものだったのでオープンにしただけです。(正直)

ただ、その背景にはいくつか動機があります。

  • 最近.NET は .NET Coreでしか作らないこと
  • PowerShell モジュールを作ろうという機会があった
  • CIで継続的に実行可否をテストしたい
  • ビルド環境につかれたのでDockerでビルドしたい

これらの例は本で提供するには紙面の見せ方も難しく、あの本でやるにはスコープを外していました。 今回の記事はそういう意味では本の補足記事になります。

Utf8BomHeader

非常にこじんまりとしたモジュールを作りました。実装は Utf8BomHeader.psm1 ファイル1つ、PowerShell Gallery で公開するためにUtf8BomHeader.psd1 を一つ追加しただけです。

github.com

このモジュールはファイルのUtf8Bomを扱うだけのもので、Windows 10、MacOS (mojave), ubuntu上で動作を確認しています。PowerShell 5.1以上、PowerShell Core 6.0 以上で動作します。

動機

Windows と Linux の相互対応、あるいはWindowsでクロスプラットフォームで動くものを動かす時、そのどちらでも課題になるのがファイルのエンコーディングです。 特に、Utf8 の BOMはWindowsでBOMが意図せずつくことがあったりして厄介度が上がっているように思います。

皆さんも Docker for Windows でDockerにファイルをmountで渡したときにファイルエンコーディングで怒られた経験をお持ちなのではないでしょうか? あるいは、GoやPythonでクロスプラットフォームに動く前提で作られたものが求めるファイルエンコーディングも通常は Utf8noBomです。

モジュールができること

このモジュールが提供するのは4機能だけです。

  • Test : utf8bom の有無をテストします。BOMがあれば trueです
  • Get : ファイルを先頭から読んでバイナリを表示します。デフォルトは3バイトです(BOM分)
  • Remove : ファイルのBOMを削除します
  • Add : ファイルにBOMを追加します
  • Coimpare : 2ファイル間でそれぞれのBOM結果を出します

実装について

少し実装について触れましょう。 当初PowerShell のみで書いていたのですが、ファイルをストリームで読むためのDisposeのために関数用意するのも冗長になるのも嫌です。 サンプルとして、Test-Utf8BomHeade のPowerShell 版は次のようにFileStream を開いてヘッダを読むことになります。

gist.github.com

本でも書きましたが、リソースの自動破棄は簡単な Using-Dispose 当りの糖衣関数を書いてもいいのですが、あまりスクリプトブロック内部で例外を起こしたくもありません。

そのため、今回のモジュールはインラインで定義した C# で実装を書いて、PowerShell はAdd-Typeでオンザフライにコンパイルしています。

https://github.com/guitarrapc/Utf8BomHeader/blob/94af0c001495a9a49a6bce53f207f5df09cf54a5/src/Utf8BomHeader.psm1#L1-L104

C# はNET Coreで動作することだけ気を付ければたいがい問題ありません。読み込んだC# クラスをPowreShell から呼び出すようにすると先ほどの実装が次のようになります。

gist.github.com

やりたいことだけ、という感じでシンプルになります。 さて、インラインでC#コードを文字列で書くのはよみにくいですね? 私も同意です、PowerShell の外に C# コードを定義したファイルはGitHub上でコードハイライトが利いたり、VS Codeで単品で動かせるメリットがあります。しかしビルド、展開、利用を考えるとファイルは少ない方が取り扱いが圧倒的に楽なため、インラインに定義できるならそうするのをオススメします。

今回はC# で実装を書きましたが、もちろんPowerShell で書くのほうが楽なことも多いです。気を付けるといいのはPowerShell Core と Windows PowerShellで両方動くようにするための抽象化は余り頑張らないようにした方が「書くのが苦しくない」ということです。いちいち互換性を気を付けることも、互換性の抽象化レイヤーを書くのもどちらもやりたいことではありません。私は苦しいのが嫌なので楽をするためにもC# で書きました。C# なら言語レベルで .NET Framework / .NET Core で気を付けることはかなり少ないので選択したということです。

CI

今回はCIとして Appveyor を用いました。 2つのパターンでビルドを用意しました。

  • appveyor 提供の ubuntu18.04 での .NET Coreを使ったビルド
  • Microsoft/PowerShell 提供のDockerを使ったビルド

自前のDockerイメージを使うとどこのCIという縛りはありません。

PowrShell でコンテナを使ったビルド? 大げさでしょうか?ビルド、テストにコンテナを使うことで、普通のプログラミング言語のビルドと同様にビルド環境を選ばなくなります。自分のコードの実行を各種環境で検証することも容易になり、ビルドしたらもうその環境はクリーンなDisposableが担保されます。いろんな環境への可搬性を考慮するとコンテナを用いないという選択はないでしょう。

Appveyor 提供のビルドイメージを使ったビルド

さて、AppVeyor はビルド環境イメージが定義されています。

www.appveyor.com

このイメージにはあらかじめ開発ツールがインストールされていおり、PowerShell Core も ubuntuイメージに含まれています。

www.appveyor.com

仮にappveyor提供のubuntuイメージでやる場合、これぐらいのカジュアルな内容でいけます。 今回は、cd を使っているあたりなんというかそういう感じって感じです。

gist.github.com

では独自のdockerイメージではどうなるでしょうか?

独自Dockerイメージを用いた場合のビルド

Dockerイメージは、Microsoft/PowerShell 提供のmcr.microsoft.com/powershell:6.1.0-ubuntu-18.04を使います。Pesterだけ入れて、プロジェクトを突っ込みましょう。

gist.github.com

appveyor.yml は次のようになります。

gist.github.com

先ほどのAppveyor提供のubuntuイメージくらべると、各種コマンド部分がそのままDockerコンテナでの実行に代わっただけです。こういった簡便な違いで済む割に、コンテナ内部の依存性はDockerfileで統一されるので敷居は低いでしょう。

ただし、mcr.microsoft.com/powershell:6.1.0-ubuntu-18.04 で1つ気になるのが、dotnet が叩けず、Publish-Moduleが実行できないことです。 Artifactなので外に出せばいいので別にいいというわけにもいかない場合があるのでちょっとどうしようかという感じです。

Appveyor のdockerを用いる場合の注意

AppveyorでDocker Imageをビルドする時は、services定義とbefore_build を使うのがプラクティスになっています。dockerは、linux image でdockerサービスの起動をかけることで利用できるので、servicesセクションを appveyor.yml 冒頭に定義しておきます。

services:
- docker

install: よりも後でしかservice startさればいため、docker buildbeffore_build:あたりでします。この実行タイミングはドキュメントになくてここで説明されています。

Build failed: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? / Problems / Discussion Area - AppVeyor Support

テスト

テストは、PowerShell の標準的なテストツールである Pester を用いたユニットテストとしました。 ここまで小さい機能だと、ユニットテストが使うシーンそのままなのでシナリオテストにもなります。

Utf8BomHeader/Utf8BomHeader.Tests.ps1 at master · guitarrapc/Utf8BomHeader · GitHub

重要なのはテストを書くことではなく、継続的にテストが回ることです。 テストはCIと一緒にやることで初めてスタート地点に立つのはPowerShell モジュールでも変わりません。

なお、テストカバレッジは Peseterで出せるので、適当にこんなコードでREADME.md を書き換えたりもできます。

gist.github.com

あとは適当に remote push しましょう。

environment:
  access_token:
    secure: zYCOwcOlgTzvbD0CjJRDNQ==

on_success:
  - git config --global credential.helper store
  - ps: Add-Content "$HOME\.git-credentials" "https://$($env:access_token):x-oauth-basic@github.com`n"
  - git config --global user.email "Your email"
  - git config --global user.name "Your Name"
  - git commit ...
  - git push ...

成果物の取得

ビルドして生成された成果物は Artifacts でアップロードしてあげればいいでしょう。

https://ci.appveyor.com/project/guitarrapc/utf8bomheader/branch/master/artifacts

ビルド結果

ログがすぐに吹き飛ぶのでキャプチャで。

Build started
git clone -q --branch=master https://github.com/guitarrapc/Utf8BomHeader.git /home/appveyor/projects/utf8bomheader
git checkout -qf ecf403c5fa0975688503c2c74a15a4837ed62e93
Running "install" scripts
pwsh --version
PowerShell 6.1.1
Starting 'services'
Starting Docker
Running "before_build" scripts
docker build -t utf8bomheader_build:latest .
Sending build context to Docker daemon  175.6kB
Step 1/5 : FROM mcr.microsoft.com/powershell:6.1.0-ubuntu-18.04
6.1.0-ubuntu-18.04: Pulling from powershell
124c757242f8: Pulling fs layer 
Status: Downloaded newer image for mcr.microsoft.com/powershell:6.1.0-ubuntu-18.04
 ---> bcea40fb0698
Step 2/5 : WORKDIR /app
 ---> Running in e03532abc50f
Removing intermediate container e03532abc50f
 ---> 40e5cd66d690
Step 3/5 : COPY . .
 ---> a391219f8b2e
Step 4/5 : RUN pwsh -c Install-Module Pester -Scope CurrentUser -Force
 ---> Running in 20dab6bbeaf2
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 Installing package 'Pester'                                                        Downloaded 0.00 MB out of 0.69 MB.                                              [                                                                    ]                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      Removing intermediate container 20dab6bbeaf2
 ---> 8bee724ae30b
Step 5/5 : ENTRYPOINT [ "pwsh", "-c" ]
 ---> Running in a365d19ff2e5
Removing intermediate container a365d19ff2e5
 ---> 138aa32dca7b
Successfully built 138aa32dca7b
Successfully tagged utf8bomheader_build:latest
docker image ls
REPOSITORY                     TAG                  IMAGE ID            CREATED                  SIZE
utf8bomheader_build            latest               138aa32dca7b        Less than a second ago   369MB
mcr.microsoft.com/powershell   6.1.0-ubuntu-18.04   bcea40fb0698        2 months ago             367MB
Running "build_script" scripts
docker run --rm -v "${APPVEYOR_BUILD_FOLDER}/publish:/app/publish" utf8bomheader_build:latest "./build_psd1.ps1 -Version ${APPVEYOR_BUILD_VERSION}"
    Directory: /app/publish
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----         12/22/18   7:16 PM                Utf8BomHeader
7z a "${APPVEYOR_BUILD_FOLDER}/publish/Utf8BomHeader_${APPVEYOR_BUILD_VERSION}.zip" "${APPVEYOR_BUILD_FOLDER}/publish/Utf8BomHeader/"
7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=C.UTF-8,Utf16=on,HugeFiles=on,64 bits,2 CPUs Intel(R) Xeon(R) CPU @ 2.30GHz (306F0),ASM,AES-NI)
Scanning the drive:
  0M Scan  /home/appveyor/projects/utf8bomheader/publish/                                                         1 folder, 4 files, 17125 bytes (17 KiB)
Creating archive: /home/appveyor/projects/utf8bomheader/publish/Utf8BomHeader_1.0.69.zip
Items to compress: 5
  0%    
Files read from disk: 4
Archive size: 5053 bytes (5 KiB)
Everything is Ok
Running "test_script" scripts
docker run --rm utf8bomheader_build:latest "Invoke-Pester -CodeCoverage src/Utf8BomHeader.psm1"
Executing all tests in '.'
Executing script /app/tests/Utf8BomHeader.Tests.ps1
  Describing Utf8BomHeader
    [+] Test should be pass for bom 1.2s
    [+] Test should be fail for bomless 75ms
    [+] Add should append BOM 45ms
    [+] Add should not operate when BOM already exists 44ms
    [+] Add -Force should append BOM even already exists 26ms
    [+] Remove should not contains BOM 33ms
    [+] Remove should not operate when BOM not exists 20ms
    [+] Remove -Force should remove BOM even already not exists 38ms
    [+] Compare should return == when both contains BOM 25ms
    [+] Compare should return <= when reference not contains BOM 28ms
    [+] Compare should return => when difference not contains BOM 16ms
    [+] Compare should return <> when reference & difference not contains BOM 17ms
Tests completed in 1.58s
Tests Passed: 12, Failed: 0, Skipped: 0, Pending: 0, Inconclusive: 0 
Code coverage report:
Covered 44.62% of 65 analyzed Commands in 1 File.
Missed commands:
File               Function             Line Command
----               --------             ---- -------
Utf8BomHeader.psm1 Add-Utf8BomHeader     130 if (!$Force) {...
Utf8BomHeader.psm1 Add-Utf8BomHeader     131 $header = [FileHeader]::Read($F...
Utf8BomHeader.psm1 Add-Utf8BomHeader     132 if ($header -eq $bomHex) {...
Utf8BomHeader.psm1 Add-Utf8BomHeader     133 Write-Verbose "Bom already exis...
Utf8BomHeader.psm1 Add-Utf8BomHeader     137 Write-Verbose "-Force paramter ...
Utf8BomHeader.psm1 Add-Utf8BomHeader     139 [FileHeader]::Write($File, $Out...
Utf8BomHeader.psm1 Remove-Utf8BomHeader  175 if (!$Force) {...
Utf8BomHeader.psm1 Remove-Utf8BomHeader  176 $header = [FileHeader]::Read($F...
Utf8BomHeader.psm1 Remove-Utf8BomHeader  177 if ($header -ne $bomHex) {...
Utf8BomHeader.psm1 Remove-Utf8BomHeader  178 Write-Verbose "Bom already miss...
Utf8BomHeader.psm1 Remove-Utf8BomHeader  182 Write-Verbose "-Force paramter ...
Utf8BomHeader.psm1 Remove-Utf8BomHeader  184 [FileHeader]::Remove($File, $Ou...
Utf8BomHeader.psm1 Get-Utf8BomHeader     218 $header = [FileHeader]::Read($F...
Utf8BomHeader.psm1 Get-Utf8BomHeader     219 Write-Output $header
Utf8BomHeader.psm1 Test-Utf8BomHeader    258 $header = [FileHeader]::Read($F...
Utf8BomHeader.psm1 Test-Utf8BomHeader    259 Write-Output ($header -eq $bomHex)
Utf8BomHeader.psm1 Test-Utf8BomHeader    259 $header -eq $bomHex
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  281 if ($PSBoundParameters.Contains...
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  282 [System.IO.FileStream]$stream
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  284 $stream = $File.OpenRead()
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  285 $header = (1..3 | ForEach-Objec...
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  285 1..3
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  285 ForEach-Object {$stream.ReadByt...
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  285 $stream.ReadByte().ToString("X2")
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  286 Write-Output ($header -eq $bomHex)
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  286 $header -eq $bomHex
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  289 $stream.Dispose()
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  293 [System.IO.FileStream]$stream
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  295 $stream = [System.IO.FileStream...
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  296 $header = (1..3 | ForEach-Objec...
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  296 1..3
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  296 ForEach-Object {$stream.ReadByt...
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  296 $stream.ReadByte().ToString("X2")
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  297 Write-Output ($header -eq $bomHex)
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  297 $header -eq $bomHex
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  300 $stream.Dispose()
Collecting artifacts...
Found artifact 'publish/Utf8BomHeader_1.0.69.zip' matching 'publish/Utf8BomHeader_1.0.69.zip' path
Uploading artifacts...
[1/1] publish/Utf8BomHeader_1.0.69.zip (5053 bytes)...100%
Running "deploy_script" scripts
pwsh -file ./deploy_pagallery.ps1 -NuGetApiKey ${PS_GELLERY_RELEASE_API} -BuildBranch master -ModuleName Utf8BomHeader
"Appveyor" deployment has been skipped as "APPVEYOR_REPO_TAG_NAME" environment variable is blank
Build completed

まとめ

PowerShell Core 使っていくといいです。本にも書きましたが、PowerShell Coreは十分実用に到達して、すでにWindows PowerShell が及ばないレベルまでまともに動けるようになっています。

特に私の場合は、クロスプラットフォームで動作することを仕事でも私生活でも扱っているので、Windows PowerShell はたまに触るたびに本当につらいです。モジュールを作った動機もBOMごるぁ、だったのです。特にWindows で、PowerShell で書いていた、書きたい時にマルチプラットフォーム向けのファイルを吐き出したい時は抜群に使いやすいでしょう。

そして、CIは回しましょう。 PowerShell にはビルドがないのでCIはいらない? そんなことを言うのは3年前まではスルーでしたが、今は最低限でも担保したいテストを書いて、CIで継続的に回す方が自分にとって楽になることが多くなっています。

今後PowerShell のネタが PowerShell Core が大半を占めるようになることを祈ります。(個人の勝手なお願い

PS. 本当はAWS Lambda Runtime で PowerShell 動かして見せる。というのを書くかと思ったのですが、「毎年アドベントカレンダーを書いた後にもっとアドベントカレンダーは軽いネタをやるものだ。」と言われたので、今年はそのことを思い出して軽くしました。

オフトピック : もしも.NET Core + PowerShell Core を独自イメージにいれるなら

もし自分でインストールする場合に備えて、.NET Core / PowerShell Core のインストールステップも公開しておきます。.NET Core がubuntu で公開されているやり方にひと手間必要だったのでどぞ。

gist.github.com

.NET Core のMicrosoft Docページにもありますが、libicu60が必要なのですが .NET Coreのインストールページの説明にはなくてほげ、って感じです。apt-get install libicu60 でも入らないですし罠感はあります。

PowerShellっぽく陸上自衛隊のイラク派遣日報をまとめてダウンロードしてみる

面白い記事があったので、私もやってみます。

blog.daruyanagi.jp

毎度毎度、PowerShellっぽさとは何かなぁ思うのですが、PowerShell実践ガイドブックでもWebサイトのステータス監視などを書いたので、良い題材な気がします。

目次

C# だとどう書くのか

C#ならAngleSharpを使って次のようなコードでダウンロード処理を行うことができます。 私の自宅では、60Mbpsを維持して5分でおわりました。

gist.github.com

PowerShellだとどうなるでしょうか。

元記事の処理

元記事では次のように書いています。

$source = "https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html"
$folder = "C:\Users\Hideto\pdf"

$result = Invoke-WebRequest $source -UseBasicParsing
$urls = $result.Links.href | Get-Unique |  where { $_ -match ".pdf" }

foreach ($url in $urls)
{
    $file = ($url  -split "/")[-1]
    Invoke-WebRequest -Uri $url -OutFile (Join-Path $folder $file)
}

PowerShell 6.0で、ここから何処まで手早く書きつつ、素早く処理できるか考えてみましょう。

必要な処理を抜き出す

必要な処理は、次の4つとわかります。

  • サイトの構造からリンクURLを抜き出す
  • PDFのパスのみを取得する
  • 重複があればはじく
  • ダウンロードする

インライン処理

URLや、パスに関しては、処理を括弧でくくることでインライン処理しつつプロパティにアクセスできます。 変数に保持したいかどうか次第ですが、書き捨てでコンソールで書くならこんなのでもいいでしょう。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href

パイプラインとメソッド形式の選択

さて、PDFに絞る方法ですが、パイプラインの入力をStringクラスのEndsWithメソッドで絞るのが楽でしょう。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf")}

コレクション入力を都度処理するときは、パイプラインで書くのも楽ですがメソッド形式で書くという選択もあります。 メソッド形式については過去の記事をどうぞ。

tech.guitarrapc.com

PowerShell実践ガイドブックでは書かなかったのですが、.ForEach().Where() メソッドは、foreach構文同様にコレクションを処理する時に一度メモリにため込むため、パイプラインよりも高速に動作します。ただしメモリに保持するということは、膨大な大きさのコレクションではメモリを大量に使うため対象のサイズに注意が必要です。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.Where({$_.EndsWith(".pdf")})

なお、一行が長くなって改行したい場合は、.で改行します。.WhereではなくWhereとなるので気を付けましょう。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.
Where({$_.EndsWith(".pdf")})

あるいは{(で改行するのもいいでしょう。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.Where({
$_.EndsWith(".pdf")})

メソッド形式で数をフィルタ (Select-Object -First 1 に相当する処理は、Whereメソッドで指定します。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.
Where({$_.EndsWith(".pdf")}, "First", 1)

こういう処理を書いている時は、1個だけ試したい、というのはあるあるですからね。

一意に絞る

URLの数を見てみると、1220個ありますが、PDFを末尾に持つURLで絞ると870個です。 しかし、ここには重複したURLが含まれているため、一意(ユニーク)なURLに絞りましょう。 PDFのURLは順不同に並んでいるため、Get-UniqueではなくSort-Object -Uniqueをする必要があります。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf")} | 
Sort-Object -Unique

これで、435個に絞られました。

URLからファイル名を取る

元記事では、$file = ($url -split "/")[-1]と書いており/で分割してできた配列の最後*1をとっています。 PowerShellらしいといえばらしいのですが、Split-Pathを使うと配列を意識せず/の最後をとれます。

Split-Path PDFのURLパス -Leaf

PDFファイルのURL、ファイル名まで取れたのを確認してみましょう。 1つだけ試すならこれで。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.
Where({$_.EndsWith(".pdf")}, "First", 1).
ForEach({Split-Path $_ -Leaf})

パイプラインならこうです。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf") | 
Select-Object -First 1 | 
ForEach {Split-Path $_ -Leaf}

ダウンロードする

1つだけダウンロードするならこうです。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.
Where({$_.EndsWith(".pdf")}, "First", 1).
ForEach({Invoke-WebRequest -Uri $_ -OutFile (Split-Path $_ -Leaf)})

全てなら、一意に絞りましょう。

((Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.
Where({$_.EndsWith(".pdf")}) | 
Sort-Object -Unique).
ForEach({Invoke-WebRequest -Uri $_ -OutFile (Split-Path $_ -Leaf)})

パイプラインでも、同様に書けます。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf")} | 
Sort-Object -Unique | 
ForEach {Invoke-WebRequest -Uri $_ -OutFile (Split-Path $_ -Leaf)}

また、パイプラインの場合は-PipelineVariable を使ってメソッドよりも柔軟に書くことができます。 -PipelineVariableは、自動変数を一度変数に受けて明示的な変数に割り当てることを不要にするので非常に便利です。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf")} | 
Sort-Object -Unique -PipelineVariable pdf | 
ForEach{Invoke-WebRequest -Uri $pdf -OutFile (Split-Path $_ -Leaf)}

もちろんこれでダウンロードできるのですが、1つ1つのファイルをダウンロードしたら次に行く同期処理です。 そのため、ダウンロードに時間がかかり435ファイルで6分かかります。

次にこれを高速化してみましょう。

非同期ダウンロードで高速化する

PowerShellの非同期技法は、大きく2つです。 Job機能を使うか、.NETの非同期構文です。

Jobを使った非同期処理

PowerShellらしさ、となるとJob機能ですがオーバーヘッドが大きい処理のため、必ずしも適切ではないケースがあります。今回のような「膨大な数」がまさにそのケースで、こういったときには.NETランタイムを使うと非同期処理がしやすいです。

Jobに変数を渡す時は、スクリプトブロックにparam句を使って -ArgumentList パラメーターを使って渡しますが、実行すると全くダウンロードされないことが分かるでしょう。 これがJobの問題点で、ダウンロードのような処理を大量のジョブでリソース割り当てを行うと大概うまくいきません。 ただの分散処理なら問題ないのですが、ダウンロードは陥りやすいでしょう。

$jobs = (Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf")} | 
Sort-Object -Unique | 
ForEach{Start-Job{param($x,$file,$path) cd $path; Invoke-WebRequest -Uri $x -OutFile $file} -ArgumentList $_,(Split-Path $_ -Leaf),$pwd}
Receive-Job $jobs -Wait

Taskを使った非同期処理

.NETのTaskを使ってみましょう。HttpClient経由でダウンロードします。 先ほどのC#の処理からダウンロード部分を抜き出してヒア文字列としたら、Add-Typeでクラスをコンパイル/読み込みします。 あとは、uri一覧をPowerShellで取得してダウンロードを呼び出すだけです。

おおよそC#と同程度の時間で終わります。

gist.github.com

C#側にダウンロード、非同期ロジックを任せることができるので、PowerShellのコードがシンプルなことがわかります。

まとめ

たぶんPowerShellっぽさは、「型を必要になるまで意識せずに」、「適当にコマンドつなげたら書ける」の2点だと思います。 なので、同期処理の場合はワンライナーにしましたし、するのは違和感ないと思います。 一方で、Jobが入った瞬間難しい見た目ですね。.NETランタイム使うのも、唐突に新しい考えが入った印象が強いと思います。 自分が書きやすく、未来の自分が読むのも困らないように書くにはコツがあります。

  • コマンドレットのエイリアスはあまり使わない(Whereや%のような頻出以外は、なるべくフルで)
  • パラメーターを必ず用いる
  • パスやディレクトリを直接触らない

あとは、シェルっぽいやり方として

  • 変数にすることでDRYができるならするが、DRYにならずメンテもしないなら書き捨てる
  • コマンドはパイプラインでつなげていく

見た目が難しくなる = 読み下しが難しくなる要素は、スクリプトブロックなどの「読み手に解釈」を求めるものがあるかもしれません。 渡す順番が処理に影響する、スコープが影響するのは難しいでしょう。

  • スクリプトブロックでparamを使い、-ArgumentListパラメーターで渡す

数が少ないなら同期で十分です。 もし数が多く、非同期で書きたい場合はJobか.NETのTaskを使うといいでしょう。

gist.github.com

参考

PowerShell のStart-Jobに非同期数制御があれば、また話はべつなのですが.... 自宅の回線がネックになってるので、余り速度が速くなりませんでしたが900Mbpsとか超えている環境では顕著に変わるでしょう。

コマンドの長さと、Invoke-WebRequest がパイプラインからの入力を受け付けないのがネックで長いですね。

*1:PowerShellは配列の最後の要素に-1でアクセスできます

『PowerShell実践ガイドブック』の先行レビューアーを募集します!

刊行される「PowerShell実践ガイドブック」(通称:貝殻本)ですが、編集さんにかけあったところ先行レビューという形で書籍をプレゼントできることになりました!

tech.guitarrapc.com

マイナビBOOKSのPC書籍編集部ブログで、応募フォームが用意されています!

book.mynavi.jp

レビュワー募集終了のお知らせ

締め切りを迎えて、抽選により5名様にレビュワーの連絡と書籍が発送されました。 レビューされるのを心待ちにしています!

お願い

当選された方は、アマゾンやブログ(はてな、note、medium、など場所は問いません)でのレビュー+TwitterなどのSNSでの告知をお願いします。 書籍到着から2週間以内にレビューを公開しご連絡ください、拡散します!

「意気込み」とは、ですが、読みたいんだーと思っていただけるの本当にうれしいです。 その読みたい理由とか教えていただけると嬉しいなぁと思います。

以下、応募サイトの抜粋です!

募集内容

発売に先立って『PowerShell実践ガイドブック』をプレゼントします。当選された方は、レビューをお願いします。Amazon.co.jpのレビューやブログ(はてな、note、mediumなどなど、場所は問いません)でのレビュー+TwitterなどのSNSでの告知をお願いします。 書籍到着から2週間以内にレビューを公開し、ご連絡ください。マイナビブックスや著者などが拡散します。

募集人数

5名

募集締切

2018年5月21日 23:59

応募方法

こちらの申し込みフォームから ・名前(ハンドル) ・メールアドレス ・意気込み ・レビュー場所(の予定) を記入して、ご応募ください。 「意気込み」欄には、PowerShellをどのように使っているか、あるいは、これから学びたいと思っている理由など、本書を読みたい!という(熱い)気持ちを自由にお書きください。

当選発表

当選者にはマイナビ出版からメールをお送りします。送付先をお知らせいただければ、書籍を発送いたします。

お問い合わせ

本件についてのお問い合わせは、books-hen3[at]mynavi.jp宛てにお送りください。 ※上記の[at]のところは@に変更してお送りください。

ご応募をお待ちしています!

「PowerShell実践ガイドブック」という本を書きました!

このたび、PowerShell実践ガイドブックという本を執筆・刊行する運びとなりました。

本日から予約開始、2018/5/30日に発売です。

※ Amazonの紹介文がおかしいのは修正予定です。 修正されました。

Amazonなどのオンライン書店で予約が始まってます。 Kindle版もありますが、まだAmazonでは発売情報が出ていませんのでお待ちください。 5/22 Kindle版も予約開始しています。

また、マイナビ出版でもPDFを販売しています。

book.mynavi.jp

PowerShell Coreを扱った本は、世界では2017年に発売された6.0リリース前の本のみで、国内でも初めてになります。

こんな人におすすめです

  • ふだんのちょっとしたPC作業を自動化をしてみたい方
  • システム管理者としてPowerShell使っていく、使いこなしたい方
  • 開発者からみてPowerShellをどう使うか気になる方

テーマは、PowerShell Core (PowerShell 6.0) をメインに据えた、PowerShellを実際に使っていくときのガイドとなる本です。 この本は過去の「PowerShellを触り始めた直後の自分」がほしかった本でもあり、今の自分が幅広い利用者を想定して書き出せる限界ギリギリを絞りました。 コードパズルに近いマニアックな内容にならず、プログラマーでないとわからないということがないように気を使っています。

Twitterアンケートでも本当に幅広い方がPowerShellを使っていて嬉しい反面、最新の情報や実践を見据え体系的にまとまった情報を得にくいと感じていました。

私自身がPowerShellを学び始めたときに困ったのが「情報がない」です。 PowerShell 6.0になって、Windowsだけでなく、macOSやLinuxでも動かせるようになりました。 とはいえ、PowerShell 6.0をどうやって導入するのか、何に使えるのか、どうやって動かすのか、スクリプトってなに、関数? 高度な関数?実際に使ってみてハマることは?という、基本的なことすらも分かりにくいと感じていました。 PowerShellに関する記事を数多く書いて来ましたが、「本によるまとまった情報の取り込み力」が必要と考えての一冊です。

本書が、そんな悩みを解決できる一助になれば、幸いです。

Windows PowerShell In Action や Windows PowerShell Cookbook との違い

当初案のタイトルは、PowerShell In Action でした。 まさにテーマが In Action「実際に使う」ということだったので被ってることにしょんぼりすることになるとは思いませんでした。

Windows PowerShell イン アクション も Windows PowerShell クックブックも何度か推薦するぐらい素晴らしい良著で私の大好きな本です。

PowerShell実践ガイドブックは、この2冊と明確に立ち位置が違います。

  • PowerShell Core (現時点の最新である PowerShell 6.0) をメインとした内容になっている
  • 体系的な基本機能も網羅しているが、常に実践を意識した内容になっている
  • 実際にPowerShellを使うにあたっての操作や直面する課題を対象にしている

これまで学んできたことを含めて全ページ書き下ろしているので、お気に入りの一冊に加えていただけると嬉しいです。

章構成と立ち読み

大まかな構成は、まずは触ってみる(1章)→コンソール操作と基本機能(2章)→管理操作(3章)→スクリプティングと自動化(4章)→実践とTips(5章)となっています。

本書の前書き「はじめに」のセクションをテキストで出すことを編集者さんに許可もらったので、ブログで一部を公開します。 「はじめに」の全文はマイナビ出版様より立ち読みとして公開予定もあるそうです。

本書の想定する読者

本書は、これからPowerShellを触ってみたい方、システム管理者、開発者の三者を想定しています。 これから触る方には、日常の中で繰り返し手作業で行っていることを自動化する足がかりになるでしょう。 特に、筆者自身が今までコマンドプロンプトやPowerShellをちょっとだけ起動したことはあったけれども「さらに習得したい」と考えたときに手に取りたかったものとして本書を執筆しました。

システム管理に求められる問題領域は、コンピューターシステムの構成や運用に始まり、ネットワーク、コンピューターセキュリティなど多くのリソースに及びます。さらに、クラウド管理、データベース管理、Webシステム管理などが求められることも少なくありません。開発者としても、多くのビルド、デプロイ、Application Programming Interface(API)を通した各種サービス操作を日常的に行います。

両者に共通するのは、多岐にわたる知識と解決です。限られた時間の中で解決するために、多くの操作を自動化する必要に迫られます。操作自体はシンプルな処理の連続であることが多く、コマンド1つ、あるいはパイプラインでつないで、はたまたスクリプトで自動化されます。

PowerShellは自動化に際して大きな力を発揮します。特に構造化データの取り扱いの容易さが大きな力になるでしょう。経験の有無にかかわらず、繰り返し作業やうんざりする作業をPowerShellを使って自動化したい方にとって本書が力になると考えています。

本書で得られること

本書を通して、PowerShellを使うことに対して「知らない」に起因する心理的なハードルの軽減すること、そして、PowerShellの網羅的な内容を知ることができるように気を配りました。本書は詳細機能の説明を無理に詰め込むことはせず、本書を読み進めることによってPowerShell 6.0の概要の把握から、C#コマンドレットの作成の入り口までの導入を目指しています。

本書は5章に分かれています。各章はテーマを持ち、実践的なTipsを交えつつ紹介しています。

第1章では、起動するのも初めて、これからまさに触ろうと思ったときに知りたかった「何ができるか」を平易に伝えることを念頭に置きました。Visual Studio Codeを使ったPowerShellのデバッグにも触れています。他言語の経験者の方やすでに慣れ親しんでいる方からすると、すでに知っていること、普段から行っていることの繰り返しに近いため、第2章から読み進めてもいいでしょう。

第2章では、PowerShellのコンソール操作に触れつつ、しっかりと基礎を学ぶことを目標に置いています。 PowerShellは、操作することを自動化に結び付けやすいため、多くの方にとってはこのコンソール操作をどうするかが知りたいことだと思います。

第3章では、クロスプラットフォームなシステム管理操作がテーマです。操作ログやリモート操作を始めとして、PowerShellでどのようにシステム管理を行うかを主題にしています。

第4章では、スクリプティングを網羅しつつ、理解できるようにしました。スクリプト、関数、クラス構文を始めとした機能の利用から、実際にモジュールを作って公開することまで、手を動かしながら学べます。

第5章では、さらに一歩進んだPowerShellの利用に触れています。実際にPowerShellを使って気付く悩みがちなこと、より実践的なTipsに踏み込んでいます。

PowerShellは画面がテキストだけということもあり、コマンド操作経験がない方がいきなり触るには難しいと考えています。そこで、第1章は、平易な言葉と例を用いて、可能な限りわかりやすい表現を心がけました。第2章以降は、オブジェクトなどの概念を順に追いかけつつ、手を動かしながら理解していく内容になっています。初めてPowerShellを触る場合でも、C#や.NETランタイムの知識や経験は必要ないように配慮して書きましたが、第2章以降ではそれを知っていることが理解を助けるでしょう。

本書で得られないこと

PowerShellは、管理や自動化といったタスクを達成しようと思ったときに使うであろうツールです。本書が目指すのは、ツールの扱い方であり使い方ではありません。したがって、標準だけでも500を越えるコマンドの1つひとつの説明はしないので、どんなときにどのような管理をするのかというユースケース、Acrive Directoryのアカウントの操作方法などは本書で説明しません。

また、本書をもってプログラマーになることは目的としていません。スクリプト環境としてVisualStudio Code(以降、VS Code)を使って説明しますが、PowerShell ISEやVS Codeの使い方やスニペット、GUIアプリケーションの作成などは触れません。

線引き

「PowerShellは何でもできるわけではない」といってしまうのは簡単です。やりたいことを探していて、要件をまったく満たしておらず使えないということもあります。コマンド1つで実行できて極めて便利なこともあるでしょう。しかし、可能ではあるものの、PowerShellで行うには複雑になってしまって難しいこともあります。

本書では、こういったPowerShellだけでは扱いにくいことをどのように扱うか、あなた自身が学んでいけるようにガイドを示します。

Windows PowerShell と PowerShell Core

2018年現在、PowerShellには、2006年に発表されたWindows PowerShellと2018年に正式版を迎えたPowerShell Coreが存在しています。

PowerShell 6.0は、PowerShell Coreの最新リリースバージョンです。Windows PowerShellのコードを元に.NET Coreをベースとしてさまざまな改善が図られており、Windows PowerShellを置き換えることを視野に開発されています。パフォーマンスや使い勝手の大幅な改善を目的に後方互換性をなくした機能もあり、.NET Frameworkには存在するものの.NET Coreにない機能は削られてもいます。

本書はPowerShell Coreの最新リリースである「PowerShell 6.0」を前提として紹介していますが、Windows PowerShellからみた変更点にも極力触れています。Windows PowerShellについて知識をお持ちの方は、PowerShell Coreでどのように変化したのかにも注目してください。

AzureAD ログインと Docker for Windows Shared Drive によるボリュームマウント

Docker は普段 macOS で使っているのですが、そういえば Docker for Windows がGAされて久しいです。ふつうに Linux コンテナが動かせるのは WSL とは違うコンテナとしての良さに満ちてていいものです。

docker run -it centos /bin/bash

とはいえ Docker for Windows は、触るたびにHyper-V をはじめとしていくつかの特徴的な挙動で難しく感じます。

さて、今回は AzureAD でログインしている Windows において、Docker の Shared Drive が上手く使えない症状に遭遇したので、解消方法をメモしておきます。

見つけるまで結構悩んでしまったので、もしほかの方が同様の症状にあった時にうまくいくヒントとなることを祈ります。

更新

2020年6月 時点で、最新 Docker Desktop for Windows にて修正されています。やったね!

github.com

目次

概要

Docker はホストのドライブをコンテナで共有する機能があります。Docker for Windows の場合は、SMB (TCP445) を使います。(SMB/CIFS)

これにより、ホストのファイル更新をコンテナと共有が可能だったり、ログ処理したりが可能になります

環境

Docker for Windows 17.09.1-ce-win42

Share Drive 方法

Docker for Windows > Settings > Shared Drives に行きます。ここで共有したいドライブを有効にします。

  • シェアしていない状態

  • シェアした状態

うまくいきましたか? やりましたね!完璧です。そのまま docker run -v を楽しみましょう。

docker run --rm -v c:/Users:/data alpine ls /data

単純にこれで済む場合は、local User でログインしている Administrators 権限を持っているケースか、AD でログインしていて適切に権限が付与されているケースが経験あります。

注意点

Network Connection Profile

もし自ネットワークプロファイルが private でない場合は、Private にしておきます。ただ、Hyper-V のプロファイルはパブリックネットワークで構いません。

自宅や職場で guest or public netrowk は、相応の理由がない場合はちょっと困ったことを引き起こしやすいです。(Firewall ルールからしてセキュアに守られる)

Hyper-V ではない、自分のイーサネットやWifi のネットワークコネクションのプロファイルを プライベートネットワークに変更する場合は、PowerShell 上で以下でさくっとできます。*1

# 対象のId を確認しましょう。
Get-NetConnectionProfile
$id = "対象のConnectionIdをどうぞ"
Set-NetConnectionProfile -NetworkCategory Private -InterfaceIndex $id

うまくホストのネットワークが プライベートになりましたか?

Firewall

Firewall の警告が出た場合はこのケースがあり得ます。

Firewall で弾かれる場合は TCP 445 のコンテナip 10.0.75.1からのアクセスになっている DockerSmbMount のエントリが許可になっていますか?

このコンテナIP 10.0.75.1 は、Docker for Windows > Settings > Network にあるデフォルトの値です。自分でIPを設定されている場合はよきように解釈してください

もしアンチマルウェアを使っていらしゃる場合は、そのソフトのブロックを解除してあげるか White list に追加するといいでしょう。

これで共有ができましたか?

AzureAD の場合

さて、本題です。

AzureAD でログインしたUserにおいては、そのままでは Shared Drives が失敗します。*2Driveのシェアを実行 > 認証を入力 > 数秒で Share がはずれる。という状態です。

ログを見ると原因がわかります。

[11:19:49.606][NamedPipeClient][Info   ] Sending Version()...
[11:19:49.607][NamedPipeClient][Info   ] Received response for Version
[11:19:49.607][NamedPipeServer][Info   ] Version()
[11:19:49.608][NamedPipeClient][Info   ] Sending Mount(C, AzureAD\UserName:**********, Docker.Core.Settings)...
[11:19:49.607][NamedPipeServer][Info   ] Version done in 00:00:00.
[11:19:49.609][NamedPipeServer][Info   ] Mount(C, AzureAD\UserName:**********, Docker.Core.Settings)
[11:19:49.629][SambaShare     ][Info   ] Mount C
[11:19:49.679][Cmd            ][Info   ] この共有リソースは存在しません。
[11:19:49.679][Cmd            ][Info   ] NET HELPMSG 2310 と入力すると、より詳しい説明が得られます。
[11:19:49.681][SambaShare     ][Info   ] "C" is not shared
[11:19:49.681][SambaShare     ][Info   ] Creating share "C:\" as "C" with Full Control to "AzureAD\UserName"
[11:19:49.740][Cmd            ][Info   ] C が共有されました。
[11:19:49.777][Cmd            ][Info   ] 共有名             C
[11:19:49.777][Cmd            ][Info   ] パス               C:\
[11:19:49.777][Cmd            ][Info   ] 注釈
[11:19:49.777][Cmd            ][Info   ] 最大ユーザー数     制限なし
[11:19:49.777][Cmd            ][Info   ] ユーザー
[11:19:49.777][Cmd            ][Info   ] キャッシュ         キャッシュは無効
[11:19:49.778][Cmd            ][Info   ] アクセス許可       AzureAD\UserName, FULL
[11:19:49.778][Cmd            ][Info   ] コマンドは正常に終了しました。
[11:19:49.780][SambaShare     ][Info   ] "C" is shared
[11:19:50.700][SambaShare     ][Info   ] Username: UserName
[11:19:50.700][SambaShare     ][Info   ] Host IP: 10.0.75.1
[11:19:50.700][SambaShare     ][Info   ] Cifs options: noperm,iocharset=utf8,nobrl,mfsymlinks,vers=3.02,domain=AzureAD
---- 中略 ----
[11:19:52.190][SambaShare     ][Error  ] Unable to mount C drive: 10.0.75.1 (10.0.75.1:445) open
umount: can't unmount /c: No such file or directory
umount: can't unmount /C: No such file or directory
rmdir: '/c': No such file or directory
rmdir: '/C': No such file or directory
mount error(13): Permission denied
Refer to the mount.cifs(8) manual page (e.g. man mount.cifs)
mount: mounting //10.0.75.1/C on /c failed: Invalid argument

[11:19:52.192][SambaShare     ][Info   ] Removing share C
[11:19:52.244][SambaShare     ][Info   ] Mount C
[11:19:52.273][Cmd            ][Info   ] この共有リソースは存在しません。
[11:19:52.273][Cmd            ][Info   ] NET HELPMSG 2310 と入力すると、より詳しい説明が得られます。
[11:19:52.275][SambaShare     ][Info   ] "C" is not shared
[11:19:52.275][SambaShare     ][Info   ] Creating share "C:\" as "C" with Full Control to "UserName"
[11:19:52.300][ApiProxy       ][Info   ] proxy >> GET /images/sha256:9e4f13a0901e7cdc0c16babf4ebec822828ecae42947c79b69c51e2e22e0470e/json
[11:19:52.300][ApiProxy       ][Info   ] Dial Hyper-V socket 45ef270d-7ba5-4a0c-9633-f0d79d3b0f30:23a432c2-537a-4291-bcb5-d62504644739
[11:19:52.300][ApiProxy       ][Info   ] Successfully dialed Hyper-V socket 45ef270d-7ba5-4a0c-9633-f0d79d3b0f30:23a432c2-537a-4291-bcb5-d62504644739
[11:19:52.302][ApiProxy       ][Info   ] proxy << GET /images/sha256:9e4f13a0901e7cdc0c16babf4ebec822828ecae42947c79b69c51e2e22e0470e/json
[11:19:52.309][Cmd            ][Info   ] システム エラー 1332 が発生しました。
[11:19:52.309][Cmd            ][Info   ] アカウント名とセキュリティ ID の間のマッピングは実行されませんでした。
[11:19:52.311][SambaShare     ][Error  ] Failed to create share "C:\" as "C" with Full Control to "UserName" with code: 2
[11:19:52.341][Cmd            ][Info   ] この共有リソースは存在しません。
[11:19:52.343][NamedPipeClient][Info   ] Received response for Mount
[11:19:52.341][Cmd            ][Info   ] NET HELPMSG 2310 と入力すると、より詳しい説明が得られます。
[11:19:52.343][SambaShare     ][Info   ] "C" is not shared
[11:19:52.343][NamedPipeServer][Info   ] Mount done in 00:00:02.7347295.
[11:19:52.449][ApiProxy       ][Info   ] proxy >> GET /images/sha256:9e4f13a0901e7cdc0c16babf4ebec822828ecae42947c79b69c51e2e22e0470e/json
[11:19:52.449][ApiProxy       ][Info   ] Dial Hyper-V socket 45ef270d-7ba5-4a0c-9633-f0d79d3b0f30:23a432c2-537a-4291-bcb5-d62504644739
[11:19:52.449][ApiProxy       ][Info   ] Successfully dialed Hyper-V socket 45ef270d-7ba5-4a0c-9633-f0d79d3b0f30:23a432c2-537a-4291-bcb5-d62504644739
[11:19:52.450][ApiProxy       ][Info   ] proxy << GET /images/sha256:9e4f13a0901e7cdc0c16babf4ebec822828ecae42947c79b69c51e2e22e0470e/json

原因は アカウント名とセキュリティ ID の間のマッピングは実行されませんでした。 がまさにそれです。要は no mapping between security id ですが、Windows においてこれは Security上の アクセスIdが一致していないことを示します。一度 C: とドライブ共有できているにもかかわらず、なのがなかなか厄介です。 何がセキュリティIdと違うのかというと、 認証時に AzureAD ログインしているときに入れないといけない AzureAD\ が原因です。では、ということで AzureAD\ を除いてユーザー名のみにすると、そんなIdはないので共有自体が失敗します。セキュリティIdが一致していない、厄介。AzureAD で Windows ログインすることはかなり快適なのですが、こういう問題はたびたびあります。

問題がわかれば対処方法が思いつきます。

対処方法は、AzureAD\UserName の UserName だけの ローカルユーザーを作りましょう。ユーザー作成後、Administrators を与えます。これで、AzureAD\UserName で共有後に、UserName も見つかるので正常にドライブ共有が可能になります。

PowerShell ならこうです。

# ここでポップアップがでるので、追加したいユーザーのUsername と Password をいれます。
$credential = Get-Credential
New-LocalUser -AccountNeverExpires -PasswordNeverExpires -UserMayNotChangePassword -Name $credential.UserName -Password $credential.Password
Add-LocalGroupMember -Group Administrators -Member $credential.UserName

もしGUI でやりたい場合、UserName をご自身のユーザー名に置き換えてください

参考

*1:管理者権限がいります。ADならDomainAuthenticated なので問題ないはずですが Firewall ルールが問題になる可能性はあります

*2:私は5度やって5度失敗したのでそういうものでしょう