しばたテックブログ

気分で書いている技術ブログです。

AzurePSDriveの実装を調べてみた

azure-mokumoku.connpass.com

先週ちょっと東京へ行く用事があり、それに合わせてAzureもくもく会@新宿に参加した際に調べた内容をまとめたエントリです。

AzurePSDriveとは

AzurePSDriveはAzure CloudShell(PowerShell)を起動した際のカレントロケーションがPS Azure:\>となるアレを提供する仕組みです。

f:id:stknohg:20180829203208p:plain

PowerShellのドライブはファイルシステム以外にも階層構造をもつデータであれば拡張して扱うことが可能であり、AzurePSDriveではAzureのサブスクリプションとそれに紐づく各リソースをドライブとして扱うことができる様になります。

類似のものとしてはIISの設定を行うWebAdminstrationモジュールのIISドライブや、SqlServerモジュールSQL Serverドライブなどがあり、こちらの方がイメージがつかみやすいかもしれません。

ソースコード

AzurePSDriveはGitHubでそのソースが公開されています。

github.com

英語ですが基本的な概念や使い方もこちらに記載されています。

AzurePSDriveの実装について

ここから本題に入ります。
Azure Cloud Shellの環境で実際にAzurePSDriveを使いながらその実装を追っていきます。

Simple Hierarchy in PowerShell(SHiPS)

AzurePSDriveでは独自のドライブを実装するのにSimple Hierarchy in PowerShell(SHiPS)と呼ばれるPowerShellモジュールを利用しています。

github.com

このモジュールは従来独自の実装が必要であったPSドライブを簡易に作成するためのモジュールであり、SHiPSDirectorySHiPSLeafといったクラスを継承したクラスを作ることで簡単に階層構造を表現することができる様になります。

詳しい話と実装例がぎたぱそ先生の本(4.9.14 SHiPSとクラスを使ったPSドライブの自作)に書いてますので気になる人はぜひご覧ください。(宣伝)

PowerShell実践ガイドブック ~クロスプラットフォーム対応の次世代シェルを徹底解説~

PowerShell実践ガイドブック ~クロスプラットフォーム対応の次世代シェルを徹底解説~

以下同書のサンプルコードを例として紹介します。

# SHiPSの実装例
#   https://github.com/guitarrapc/Book-PowerShell-Samples/blob/master/Book-PowerShell-Samples/Chapter4/4.9_Class/PSDrive/SolarSystem.psm1 より抜粋
using namespace Microsoft.PowerShell.SHiPS

# SHiPSDirectory を継承したクラスでコンテナとなる要素を定義
class SolarSystem : SHiPSDirectory {

    # ・・・中略・・・

}

# SHiPSLeaf を継承したクラスで枝要素を定義
class Moon : SHiPSLeaf {
    
    # ・・・中略・・・
}

Azure PowerShellモジュールと独自の型定義

AzurePSDriveは内部でAzureのリソースにアクセスする際にAzure PowerShell moduleの機能を利用しています。
Windows PowerShell、PowerShell Core両方に対応しており、現時点のバージョン(Ver.0.8.7)では、

  • AzureRM.Profile (AzureRM.Profile.NetCore)
  • AzureRM.Resources (AzureRM.Resources.NetCore)
  • AzureRM.Compute (AzureRM.Compute.NetCore)
  • AzureRM.Websites (AzureRM.Websites.NetCore)
  • Azure.Storage (Azure.Storage.NetCore)

が依存モジュールとなっています。

また、内部的にはAzure PowerShell moduleのオブジェクトをそのまま使っている部分があるのですが、一部オブジェクトについてはドライブ独自の型に差し替えられています。

例として、改めて後述しますが、AllResources要素ではGet-AzureRmResourceの結果である Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.PSResource型のオブジェクトをそのまま使っているのですが、この型がAzurePSDriveではAzurePSDriveResourceTypeになります。

細かい説明は省きますが、実装としては以下の様にして取得したオブジェクトの型を差し替えています。

# AzurePSDriveResource.psm1 より一部抜粋
& "$script:AzureRM_Resources\Get-AzureRmResource" | %{ $_.psobject.typenames.Insert(0, "AzurePSDriveResourceType"); $_ }

CloudShellログイン時点

ここからはAzure Cloud Shell(PowerShell)の実際の画面を基に各階層の内容について説明していきます。

最初にAzure Cloud Shellにログインした時点では最上位の階層であるAzureドライブがカレントロケーションとなります。

f:id:stknohg:20180829203208p:plain

Get-PSDriveを使うとこのAzureドライブの情報を見ることができます。

PS Azure:\> Get-PSDrive | Format-Table -AutoSize

Name     Used (GB) Free (GB) Provider    Root               CurrentLocation
----     --------- --------- --------    ----               ---------------
/            15.01     33.40 FileSystem  /                      home/stknohg
Alias                        Alias
Azure                        SHiPS       AzurePSDrive#Azure
Env                          Environment
Function                     Function
Variable                     Variable

ファイルシステムなどのドライブの中にAzureドライブが存在し、SHiPSプロバイダーとして定義されていることが分かります。

第1階層 : サブスクリプション

AzureドライブでGet-ChildItem(dir)を実行すると子要素を一覧することができ、たとえば私のアカウントだと以下の様に表示されます。

PS Azure:\> dir


    Directory: Azure:


Mode SubscriptionName SubscriptionId                       TenantId                             State
---- ---------------- --------------                       --------                             -----
+    stknohg          ********-****-****-****-************ ********-****-****-****-************ Enabled

最初の階層はログインしたアカウントに紐づくサブスクリプションが列挙されます。

各サブスクリプションはSubscription型として定義されており、内部的にはACC_TID環境変数が定義されていればそのテナントID、ACC_TID環境変数が定義されていない場合は(Get-AzureRmTenant)[0].IdとなるテナントIDに紐づくサブスクリプションをGet-AzureRmSubscriptionで取得しています。

ちなみに現在Azure Cloud Shellは全てLinux(Ubuntu)のコンテナになってしまったため、lsコマンドは/bin/lsなのでAzureドライブの子要素を取得するのには使えませんので注意してください。

第2階層 : リソース分類

つづけてSet-Location(cd)でサブスクリプション(ここではstknohg)に移動し、子要素を一覧すると以下の様になります。

PS Azure:\> cd ./stknohg/
PS Azure:\> dir


    Directory: Azure:/stknohg


Mode Name
---- ----
+    AllResources
+    ResourceGroups
+    StorageAccounts
+    VirtualMachines
+    WebApps

SubScriptionの子要素は以下で固定されています。

  • AllResources
  • ResourceGroups
  • StorageAccounts
  • VirtualMachines
  • WebApps

それぞれ名前から予測はつくと思いますが詳細は後述します。

第3階層(AllResouces)

ここからはSubScriptionの各子要素について説明します。
最初にAllResources要素についてです。

この要素はAllResources型で表現され、内容としては単純にGet-AzureRmResourceの結果を返しているだけになります。
さらなる子要素はありません。

通常Get-AzureRmResourceの結果は Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.PSResource型ですが、表向きの型をAzurePSDriveResourceTypeに更新しており、Get-Memberで取得できる型名は後者となっています。

第3階層(ResourceGroups)

ResourceGroups要素がおそらくAzurePSDriveで扱うメインの要素になるかと思います。

この要素はResourceGroups型で表現され、子要素にResourceGroup型のオブジェクトを持ちます。

実装としては、Get-AzureRmResourceGroupGet-AzureRmResourceを内部で使い、各リソースを呼出してリソースの階層構造を表現しています。
ここから第4階層(ResourceProvider)、第5階層(ResourceType)と続くのですが説明が冗長になる*1ので本エントリではこのくらいにしておきます。

第3階層(StorageAccounts)

ここからのStorageAccounts、VirtualMachines、WebAppsについてはリソースの中でもよく使うであろうものを特別視したものと思われます。
(もしかしたら子要素の表現が難しいものを特別視している可能性もありますが、ちょっと断言できません...)

この要素はStorageAccounts型で表現され、Get-AzureRmStorageAccountの結果を子要素にStorageAccount型として持ちます。

StorageAccountの子要素として各ストレージの種類に応じた、

  • Blobs
  • Files
  • Tables
  • Queues

がありそれぞれの情報にアクセスすることができます。
内部的にはAzure.Storage (Azure.Storage.NetCore)モジュールの各コマンドレットが使われ、その結果を利用しています。

第3階層(VirtualMachines)

この要素はVirtualMachines型で表現され、 Get-AzureRmVMの結果をAzurePSDriveVM型として子要素に持ちます。

これ以上の階層は無く、単純にVMの一覧が取得できて終わりの様です。

第3階層(WebApps)

この要素はWebApps型として表現され、Get-AzureRmWebAppの結果をAzurePSDriveWebApp型として子要素に持ちます。

この要素もこれ以上の階層は無く、単純にWebAppの一覧が取得できて終わりです。

最後に

とりあえずこんな感じです。

私自身Azureをさほど使いこなしていないため、ソースを読み間違えている部分があるかもしれません。
もし本エントリの内容におかしな点がありましたらフィードバックしていただけると嬉しいです。

*1:調べきれなかったとも言います...

PowerShellで外部プロセスのコマンドラインを取得する

何気に.NET Framework/.NET Coreには外部プロセスのコマンドラインを取得する方法が無く、このため、PowerShellからコマンドラインを取得しようとする場合少し手間をかけてやる必要があります。

PowerShellで外部プロセスのコマンドラインを取得する

簡単な関数Get-ProcessCommandlineを作ってGistに上げました。
使用例と併せてご覧ください。

厳密に対応バージョンを考慮していませんが、Windows PowerShell、PowerShell Core両方の大抵の環境で動くはずです。

gist.github.com

簡単な解説

各プラットフォーム毎の簡単な解説を補足しておきます。

Windowsの場合

Windowsにおいて外部プロセスのコマンドラインを取得するには、ReadProcessMemoryなどのプロセス情報を読み取るWin32 APIを使うかWMIを使う必要があります。

PowerShellからであればWMIを扱うほうが圧倒的に楽なので先述の関数でもWMIを使っています。
Win32_ProcessクラスにCommandLineというズバリなプロパティがあるのでこれを取得するだけでOKです。

# Windowsでは Win32_Process クラスの CommandLine プロパティからコマンドラインを取得可能
$proc = Get-WmiObject -Class Win32_Process -Filter "ProcessId = $Id" -Property "ProcessId", "CommandLine"
if ($null -eq $proc) {
    return ""
}
return $proc.CommandLine

# PowerShell Core なら Get-CimInstance を使う
$proc = Get-CimInstance -Class Win32_Process -Filter "ProcessId = $Id" -Property "ProcessId", "CommandLine"
if ($null -eq $proc) {
    return ""
}
return $proc.CommandLine

Linuxの場合

Linuxの場合プロセスの情報を/proc/から取得することができます。
プロセスのコマンドラインは/proc/[プロセスID]/cmdlineに記述されているのでGet-Contentで内容を読み取ってやればOKです。

注意すべきところはコマンドラインのデリミタがヌル文字(\0)である点くらいでしょうか。

# Linuxでは /proc/[プロセスID]/cmdline からコマンドラインを取得可能
if (-not (Test-Path -LiteralPath "/proc/$Id")) {
    return ""
}
return @(Get-Content -LiteralPath "/proc/$Id/cmdline")[0] -replace "\0", " "

macOSの場合

残念ながらmacOSには/proc/が存在せずLinuxと同じ様にはいきません。
macOSのシステムプログラミングには全く詳しくないため大した調査は出来なかったのですが、

stackoverflow.com

を見る限りではCで頑張らないとダメな様です。

ちょっと本末転倒な感じもありますが、PowerShellからだとpsコマンドの結果を抜くのが一番手っ取り早く確実です。
(これならはじめからpsコマンドだけで良いのでは?というお気持ちです...)

psコマンドで特定の要素だけ取得するには-oオプションを、取得結果からカラムヘッダーを除外するには=をつけてやればよいため、ps -o commnand=の様に指定してやればコマンドラインを取得できます。

# macOSの場合は psコマンドを使ってコマンドラインを取得可能
return (/bin/ps -o command= -p $Id)

PowerShell CoreのSnapパッケージが提供されました

既に各所で報じられ日本語情報も多いですが、思っていた以上にニュースになっている様なので本ブログでもとりあげてみます。

公式情報

細かい話についてはPowerShell Teamのこちらのブログエントリをご覧ください。

blogs.msdn.microsoft.com

Snapsについて

Snapsの公式サイトはコチラ。

snapcraft.io

Wikipediaによれば、

Snappyとはカノニカルが設計・開発したソフトウェアデプロイメントシステムかつパッケージ管理システムであり、元々はUbuntu Phoneオペレーティングシステム用に設計・開発された。
Snappyのパッケージは 'Snap' と呼ばれ、Snapを使うツールは 'Snapd' と呼ばれる。Snapは様々なLinuxディストリビューションで動作するので、ディストリビューションの上流のソフトウェアデプロイメントに依存しない。Snappyのシステムは携帯電話、クラウド、IoTやデスクトップパソコン向けに設計されている。

とあり、ディストリビューションに依存しない新しいパッケージマネージャーだそうです。

正直アーキテクチャなどでわからない部分は多いのですが、ざっと試した限りだとMicrosoft StoreとDesktop Bridgeの組み合わせに近い様に感じました。
(実際この例えが適切なのかは自信がありません...)

また、呼称について、今はSnappyでなくSnapsと呼ばれている?*1様です。
このあたりの事情については調べてみてもあまり情報を得ることができませんでした...

インストール方法

実際にSnapパッケージをインストールする手順を紹介します。

1. snapdのインストール

最初にsnapdをインストールする必要があります。

docs.snapcraft.io

に各ディストリビューション毎のインストール手順が紹介されています。
おおむね各パッケージマネージャーからのインストールに対応している様です。

今回はUbuntu 18.04の環境で動作確認をしたため、デフォルトでsnapdがインストール済みでありこの手順を行うことはありませんでした。

2. PowerShell Coreのインストール

Snapパッケージの管理はsnapコマンドから行います。
以下の様に--classicオプションを付けてsnap installコマンドを実行します。

# Stableバージョンのインストール
snap install powershell --classic

# Previewバージョンのインストール
snap install powershell-preview --classic

Ubuntu 18.04の環境で試すと下図の様になります。

f:id:stknohg:20180806163506p:plain

インストール時だけルート権限を要求されます。

f:id:stknohg:20180806163534p:plain

インストールが終わるとこんな感じです。

f:id:stknohg:20180806163556p:plain

現時点でUbuntu 18.04向けのdebパッケージが提供されていないPowerShell Core 6.0.3ですが、Snapパッケージであればこの様にインストール可能です。

f:id:stknohg:20180806163620p:plain

なお、--classicパラメーターを付けないとエラーとなります。

# --classic無しだとエラー
~$ snap install powershell
error: This revision of snap "powershell" was published using classic confinement and thus
       may perform arbitrary system changes outside of the security sandbox that snaps are usually
       confined to, which may put your system at risk.

       If you understand and want to proceed repeat the command including --classic.

この--classicパラメーターは、classic confinement policyと呼ばれる方式で公開されたアプリケーションをインストールするのに必要なパラメーターです。
Snapパッケージはデフォルトでファイルパスなどが分離された環境で動作する方式(strict confinement policy)となっているのですが、classic confinement policyではファイルシステムのルートが/になる等システム全体へのアクセスが許可されたモードになるそうです。
PowerShell Coreはシェルとしてシステム全体にアクセスする必要があるためclassic confinement policyで公開されています。

confinement policyについては

に詳しい説明があります。

従来のパッケージについて

最初のPowerShell Team Blogに記載されていますが、Snapパッケージが提供されたからといって従来のパッケージマネージャー向けのパッケージの提供を止めるといった事は無いそうです。
PowerShellを利用するための選択肢を増やし、利用者がより簡易にアプリケーションを管理できる様にするためのサポートといったところの様です。

設定情報など

最後に補足としてSnapパッケージとしてインストールされたPowerShell Coreの情報を幾つかか確認して終わりにします。

検証環境

先ほどと同じUbuntu 18.04の環境です。

パス情報

実行バイナリであるpwsh/snap/bin/pwshにありました。

~$ which pwsh
/snap/bin/pwsh

環境変数

PowerShell Coreを起動した後に見える環境変数には、以下の様にSNAP_で始まるSnapパッケージ情報を持つものが追加されています。

PS /> dir env:\SNAP*

Name                           Value
----                           -----
SNAP_USER_DATA                 /home/shiba/snap/powershell/7
SNAP                           /snap/powershell/7
SNAP_REEXEC
SNAP_NAME                      powershell
SNAP_VERSION                   6.0.3
SNAP_COOKIE                    HkSK6dTCpnVqHy4D9fY3WbPWwK1GdMmgC1rYHpnn076t
SNAP_ARCH                      amd64
SNAP_DATA                      /var/snap/powershell/7
SNAP_COMMON                    /var/snap/powershell/common
SNAP_CONTEXT                   HkSK6dTCpnVqHy4D9fY3WbPWwK1GdMmgC1rYHpnn076t
SNAP_REVISION                  7
SNAP_USER_COMMON               /home/shiba/snap/powershell/common
SNAP_LIBRARY_PATH              /var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void

PATH絡みの環境変数はこんな感じ。

PS /> $env:PATH -split ':'
/snap/powershell/7/opt/powershell
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin
/usr/games
/usr/local/games
/snap/bin

PS /> $env:PSModulePath -split ':'
/home/shiba/.local/share/powershell/Modules
/usr/local/share/powershell/Modules
/snap/powershell/7/opt/powershell/Modules

PowerShell Coreの実体は/snap/powershell/[パッケージのバージョン番号]/opt/配下にあり、モジュールパスは他のパッケージ版と同じになっています。

/snap/powershell/配下の構成

treeコマンドで/snap/powershell/配下を確認するとこんな感じでした。
必要なライブラリなどがバージョンごとに分離された環境にまとめられていることがわかります。

~$ sudo tree -d -L 5 /snap/powershell/
/snap/powershell/
├── 7
│   ├── etc
│   │   ├── gss
│   │   │   └── mech.d
│   │   └── ldap
│   ├── lib
│   │   └── x86_64-linux-gnu
│   ├── meta
│   │   └── gui
│   ├── opt
│   │   └── powershell
│   │       ├── Modules
│   │       │   ├── Microsoft.PowerShell.Archive
│   │       │   ├── Microsoft.PowerShell.Host
│   │       │   ├── Microsoft.PowerShell.Management
│   │       │   ├── Microsoft.PowerShell.Security
│   │       │   ├── Microsoft.PowerShell.Utility
│   │       │   ├── PSDesiredStateConfiguration
│   │       │   ├── PSReadLine
│   │       │   ├── PackageManagement
│   │       │   └── PowerShellGet
│   │       ├── en-US
│   │       │  
│   │      (├── ※ pwsh はココ)
│   │       │  
│   │       └── ref
│   ├── snap
│   │   └── gui
│   └── usr
│       ├── lib
│       │   ├── sasl2
│       │   └── x86_64-linux-gnu
│       │       ├── krb5
│       │       ├── openssl-1.0.0
│       │       └── sasl2
│       └── share
│           ├── doc
│           │   ├── libasn1-8-heimdal
│           │   
│           │   ・・・ (中略) ・・・
│           │   
│           │   └── zlib1g
│           ├── lintian
│           │   └── overrides
│           └── man
│               └── man5
└── current -> 7

*1:Snappyの呼称はSnappy Ubuntu Coreのディストリビューション全体を指すように推移している様に見受けられたのですがいまいちわかりませんでした。