RunspacePoolを使って、PowerShellを非同期実行

知る人ぞ知る「技」の使い方

Windowsのスクリプト言語として、もはやなくてはならないPowerShell。普段使っているように、既存のコマンドドレッドを単純に呼び出すといったことだけでも、その便利さは十分体感できるものです。しかし、便利なのはたくさん用意されているコマンドだけではなく、知る人ぞ知る様々な「技」がたくさんあります。今回はその技の1つ、PowerShellの非同期実行の方法をご紹介します。

非同期実行とは?

あれこれ説明するよりも実際に見てみましょう。処理時間のかかる2つのPowerShellのコマンドの後に、テキストファイルを作成するコマンドを実行します。

Get-Service | SELECT *
Get-Process | SELECT *
New-Item -Path "C:\PSTest\Test.txt" -ItemType File

通常のPowerShellの実行結果からすると、テキストファイルが実際に作成されるのは、時間のかかる二つのコマンドが実行されてから、終了するまでしばらく時間が経過した後となってしまいます。

一方、非同期で実行した場合は、スクリプト実行直後にテキストファイルが作成されます。

これが非同期実行です。同期実行は複数の命令を投げた場合に、1つずつ上から順次に処理して結果が出るまで待機します。終了したら次の命令を実行してゆくことになります。非同期実行はリソースの空いている限り、命令が同時に実行されていきます。

言うまでもありませんが、非同期実行の最大の効果はその処理速度です。最近のハードウェアのリソースは一度に複数の仮想化環境をサポートするくらい高性能なものとなっています。CPU、メモリ、ハードディスク、ネットワークなど、これらのリソースを使うのに、1つずつ仕事をさせるというのではとてももったいない使い方です。ハードウェアの本領が発揮されません。リソースがゆるすかぎりたくさんの仕事をさせてこそ本当の実力が分かります。

また、非同期実行とはいっても、命令の渡し方にしても、CPUだけ、ハードディスクだけといったように一つのリソースに偏るような命令の渡し方ではなく、まんべんなく偏りのない命令の渡し方というのもポイントになります。

PowerShellの非同期実行といっても、方法は一つだけではなく、いくつか方法があります。「バックグランドジョブ」「PowerShell Workflow」「asyncとawait」などがあります。今回は.Net FameworkでPowerShellのために提供されていると言ってもよい「System.Management.Automation.Runspaces Namespace」の「RunspacePoolクラス」を利用したPowerShellの非同期実行の方法を紹介します。RunspacePoolの中で実行されたPowerShellのコマンドは非同期に実行することが可能になります。

非同期実行 PowerShellサンプル

まずは具体的なコードです。

:	$PSCmds = @(					
2:							
3:	"Get-Service | SELECT *",				
4:							
5:	"Get-Process | SELECT *",				
6:							
7:	"New-Item -Path 'C:\PSTest\Test.txt' -ItemType File"	
8:							
9:	)						
10:							
11:	$res = AsyncPowershell $PSCmds				
12:							
13:	function AsyncPowershell($Cmds)			
14:	{						
15:	    try {						
16:	        $MaxRunspace = $Cmds.Length			
17:	        $RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, $MaxRunspace)       
18:	        $RunspacePool.Open()				
19:		   					
20:	        $aryPowerShell  = New-Object System.Collections.ArrayList
21:	        $aryIAsyncResult  = New-Object System.Collections.ArrayList
22:	        for ( $i = 0; $i -lt $MaxRunspace; $i++ )		
23:	        {						
24:	            $Cmd = $Cmds[$i]				
25:	            $PowerShell = [PowerShell]::Create()		
26:		   $PowerShell.RunspacePool = $RunspacePool	
27:	            $PowerShell.AddScript($Cmd)			
28:	            $PowerShell.AddCommand("Out-String")		
29:	            $IAsyncResult = $PowerShell.BeginInvoke()	
30:	                      				
31:	            $aryPowerShell.Add($PowerShell)		
32:	            $aryIAsyncResult.Add($IAsyncResult)		
33:	        }						
34:							
35:	        while ( $aryPowerShell.Count -gt 0 )		
36:	        {						
37:	            for ( $i = 0; $i -lt $aryPowerShell.Count; $i++ )	
38:	            {					
39:	                $PowerShell = $aryPowerShell[$i]		
40:	                $IAsyncResult = $aryIAsyncResult[$i]   	
41:							
42:	                if($PowerShell -ne $null)		
43:	                {					
44:	                    if($IAsyncResult.IsCompleted)		
45:	                    {				
46:	                        $Result = $PowerShell.EndInvoke($IAsyncResult)
47:	                        Write-host $Result		
48:	                        $PowerShell.Dispose()		
49:	                        $aryPowerShell.RemoveAt($i)	
50:	                        $aryIAsyncResult.RemoveAt($i)	
51:	                        {break outer}			
52:	                    }				
53:	                }          				
54:	            }					
55:	            Start-Sleep -Milliseconds 100		
56:	        }						
57:	    } catch [Exception] {				
58:	        Write-Host $_.Exception.Message;		
59:	    } finally {					
60:	        $RunspacePool.Close()				
61:	    }						
62:	}						 

上から見ていきましょう。

AsyncPowershellという関数に、配列文字列として複数のPowerShellのコマンドを渡しています。

非同期実行は、RunspacePool を作って、開いて利用します。try~catch finallyで確実に一度開いたRunspacePoolを使い終わったら必ず閉じるようにしておきます。開きっぱなしにしておくと、いつかリソースが枯渇してしまいRunspacePoolを開くことができなくなってしまうのでご注意を。

RunspacePoolを作成します。引数として渡したコマンドの数だけRunspacePoolを作成し$RunspacePoolに格納します。

最小値を1、最大値を実行させたいコマンド数としています。CreateRunspacePool()として、動的に RunspacePoolを作成して実行することもできますが、この場合処理速度がやや遅くなります。また、CreateRunspacePool(1, 1)のように、 RunspacePoolが実行したい命令数より少ない場合は同期処理と同じようにRunspacePoolが空き次第、次の命令を実行ということになります。

非同期処理のメリットを最大限に発揮するには、あらかじめRunspacePoolの数を不足ないように設定して実行する必要があります。

この2つのArrayListは、それぞれのPowerShellコマンドとその実行結果を格納するために定義します。最終的にそれぞれのコマンドが終了次第、随時結果を出力するために利用します。

for{ }で引数で渡されたコマンドを1つずつ実行していきます。コマンドの実行は、System.Management.Automation.PowerShellクラスから行います。このクラスは.Net FrameworkからPowerShellのコマンドを実行するためのクラスです。.Net Frameworkのクラスとして提供されているので、System.Management.Automation.Runspacesクラス同様、もちろんVBやC#からも利用することが可能です。

$PowerShell = [PowerShell]::Create()でPowerShellのオブジェクトを作成します。$PowerShell.RunspacePoolには先に定義した$RunspacePoolを指定します。これによってPowerShellのコマンドがRunspacePool内で非同期に実行されます。

AddScriptで実行するコマンドを指定します。

$PowerShell.AddCommand(“Out-String”) は実行結果としての戻り値をテキストとして取得できるようにパイプラインとして追加しておきます。

$IAsyncResult = $PowerShell.BeginInvoke() はBeginInvokeでコマンドを実行して、実行結果としてIAsyncResultを取得して$aryIAsyncResultに格納します。

$aryPowerShellの中には実行したPowerShellオブジェクトが格納されています。

このwhileの中で実行したコマンドの結果を取得します。実行結果を取得し次第、$aryPowerShellから1つずつPowerShellオブジェクトを削除してゆき、最終的に$aryPowerShellが空になれば終了となります。 実行結果は非同期実行の場合は順番に取得するのではなく、終了したものから随時取得していきます。$aryIAsyncResultには実行結果が格納されています。

実行されたPowerShellのオブジェクト$PowerShell = $aryPowerShell[$i]ごとに、実行結果$IAsyncResult = $aryIAsyncResult[$i]を確認します。

実行結果を取得するごとに、if($IAsyncResult.IsCompleted)でコマンドが終了したかどうかを判断しています。

コマンドが終了していたら、$Result = $PowerShell.EndInvoke($IAsyncResult)で実行結果を取得して、Write-host $Resultで実行結果を出力します。終了したPowerShellオブジェクトと実行結果オブジェクトは削除して、再度ループに入ります。$aryPowerShellが空になるまで繰り返されます。

大まかな処理の流れです。

1. RunspacePoolの作成
2. RunspacePoolのオープン
3. RunspacePool内でコマンドを一つずつ実行
4.実行したPowerShellと実行結果をオブジェクトとしてArrayListに格納
5.コマンドの数だけ繰り返す
6.すべてのコマンドが起動完了
7.格納したArrayList内のオブジェクトをLoopで一つずつチェック
8.コマンドの実行結果がIsCompletedか否かの判断
9.コマンドが終了した場合結果を取得
10.実行結果を出力
11.出力完了したオブジェクとはArrayListから削除
12.すべてのコマンドの結果を取得し終わるまで繰り返す
13. ArrayListが空になれば終了
14. RunspacePoolを閉じる

非同期実行のポイント

RunspacePoolを使ってのPowerShellのコマンド実行は、まず、すべてのコマンドをRunspacePool内で起動さてしまうこと。すべて起動し終わってから、コマンドの実行結果は、Loopで実行状態を確認しつつ、随時取得してゆくという部分になります。

BeginInvokeの直後に、EndInvokeをしてしまうと、同期実行と同様に、一つのコマンドが終了するまで、待機状態になってしまうのでご注意ください。

リモートでの実行

以上、ご紹介したのは、ローカルで実行する場合のサンプルとなります。PowerShellの非同期実行はもちろんリモートからでも可能となっています。

1:	$PSCmds = @(					
2:							
3:	"Get-Service | SELECT *",				
4:							
5:	"Get-Process | SELECT *",				
6:							
7:	"New-Item -Path 'C:\PSTest\Test.txt' -ItemType File"	
8:							
9:	)						
10:							
11:	$res = AsyncPowershell $PSCmds "Server01" "Administrator" "PASSWORD"
12:							
13:	function AsyncPowershell($Cmds, $Server, $AdminAccount, $AdminPassword)
14:	{						
15:	    try {						
16:	        $MaxRunspace = $Cmds.Length			
17:							
18:	        $SecurePassword = ConvertTo-SecureString $AdminPassword -AsPlainText -Force
19:	        $Cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $AdminAccount, $SecurePassword;
20:	        $RunspaceConnectionInfo = New-Object System.Management.Automation.Runspaces.WSManConnectionInfo
21:	        $RunspaceConnectionInfo.Credential = $Cred		
22:							
23:	        $RunspaceConnectionInfo.ComputerName = $Server	
24:	        $RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, $MaxRunspace, $RunspaceConnectionInfo)       
25:	        $RunspacePool.Open()				
26:		   					
27:	        $aryPowerShell  = New-Object System.Collections.ArrayList
28:	        $aryIAsyncResult  = New-Object System.Collections.ArrayList
29:	        for ( $i = 0; $i -lt $MaxRunspace; $i++ )		
30:	        {						
31:	            $Cmd = $Cmds[$i]				
32:	            $PowerShell = [PowerShell]::Create()		
33:	            $PowerShell.RunspacePool = $RunspacePool	
34:	            $PowerShell.AddScript($Cmd)			
35:	            $PowerShell.AddCommand("Out-String")		
36:	            $IAsyncResult = $PowerShell.BeginInvoke()	
37:	                      				
38:	            $aryPowerShell.Add($PowerShell)		
39:	            $aryIAsyncResult.Add($IAsyncResult)		
40:	        }						
41:							
42:	        while ( $aryPowerShell.Count -gt 0 )		
43:	        {						
44:	            for ( $i = 0; $i -lt $aryPowerShell.Count; $i++ )	
45:	            {					
46:	                $PowerShell = $aryPowerShell[$i]		
47:	                $IAsyncResult = $aryIAsyncResult[$i]   	
48:							
49:	                if($PowerShell -ne $null)		
50:	                {					
51:	                    if($IAsyncResult.IsCompleted)		
52:	                    {				
53:	                        $Result = $PowerShell.EndInvoke($IAsyncResult)
54:	                        Write-host $Result		
55:	                        $PowerShell.Dispose()		
56:	                        $aryPowerShell.RemoveAt($i)	
57:	                        $aryIAsyncResult.RemoveAt($i)	
58:	                        {break outer}			
59:	                    }				
60:	                }          				
61:	            }					
62:	            Start-Sleep -Milliseconds 100		
63:	        }						
64:	    } catch [Exception] {				
65:	        Write-Host $_.Exception.Message;		
66:	    } finally {					
67:	        $RunspacePool.Close()				
68:	    }						
69:	}						 

リモート実行で異なるのは、RunspacePoolに認証情報を追加する部分だけです。

呼び出す関数に、実行対象のサーバー名、アカウント、パスワードの引数を追加しています。

ConvertTo-SecureStringでパスワードをセキュア状態に変更してから、System.Management.Automation.Runspaces.WSManConnectionInfoのConnectionInfoオブジェクトを作成して、Credentialとして引き渡します。 CreateRunspacePoolの第三の引数として、ConnectionInfoオブジェクトを指定して、RunspacePoolを作成してOpenします。

これでリモートでの非同期実行の準備が完了しました。

VB、C#でもPowerShellを非同期実行 ~Hyper-Vプロビジョニング~

先にも少し紹介しましたが、PowerShellの非同期実行は、.Net FrameworkのSystem.Management.Automation.PowerShellクラスとSystem.Management.Automation.Runspaces クラスを利用して実現しています。

従ってVBやC#からもPowerShellのコマンドを非同期で実行することが可能です。

この最大のメリットは、筆者の守備範囲であるHyper-Vのプロビジョニングにおいて発揮されます。Hyper-VのプロビジョニングはすべてWMIをつかってVisualStudioで開発することが可能です。しかし、やはり簡単なPowerShellで、かつ、開発効率のよいVisualStudioを使ってプロビジョニングシステムを開発したいというのは当然の発想といえます。そこで、System.Management.Automation.PowerShellクラスを利用して、VisualStudioでVBやC#からPowerShellのコマンドを実行させます。

さらに、Hype-Vの操作には時間のかかる操作がいくつかあります。たとえば、仮想マシンのエクスポートや、LiveMigrationがこれに該当します。数十ギガにおよぶディスクのコピーが発生する場合は、高速な共通ストレージなどが無い環境では相当な時間を要するものになります。そんなときに、是非おすすめしたいのが、System.Management.Automation.Runspaces クラスを利用したPowerShellの非同期実行なのです。

VBとC#のサンプルも紹介しておきます。時間のかかる仮想マシンのエクスポートと当時に、他の仮想マシンを起動しつつ、Hyper-V上の仮想マシン一覧情報を取得しています。

VB PowerShell非同期実行サンプル

  1:      '非同期のコマンド指定
   2:      Dim strAsyncCmds() As String = {"Export-VM ?Name VM01 ?Path D:\Exp", "Start-VM VM02", "Get-VM", "Start-VM VM03"}
   3:   
   4:      'コマンドを実行
   5:      AsyncPowershell(strServer, strAccount, strPassword, strAsyncCmds)
   6:   
   7:      Public Sub AsyncPowershell(ByVal strServer As String, ByVal strAccount As String, ByVal strPassword As String, ByVal strCmds() As String)
   8:          Dim objRunspacePool As RunspacePool = Nothing
   9:          Try
  10:              'パスワードを暗号化
  11:              Dim objSecureString As New SecureString()
  12:              For Each c As String In strPassword.ToCharArray()
  13:                  objSecureString.AppendChar(c)
  14:              Next
  15:              Dim objPSCredential As New PSCredential(strAccount, objSecureString)
  16:   
  17:              'WinRMを利用してリモートでPowerShellを実行
  18:              Dim objUri As New Uri("http://" + strServer + ":5985/wsman")
  19:              Dim objWSManConnectionInfo As New WSManConnectionInfo(objUri, "http://schemas.microsoft.com/powershell/Microsoft.PowerShell", objPSCredential)
  20:   
  21:              Dim intRunspace As Integer = strCmds.Length
  22:              objRunspacePool = RunspaceFactory.CreateRunspacePool(1, intRunspace, objWSManConnectionInfo)
  23:              'objRunspacePool.ApartmentState = Threading.ApartmentState.STA
  24:              'objRunspacePool.ThreadOptions = PSThreadOptions.UseNewThread
  25:              objRunspacePool.Open()
  26:   
  27:              Dim aryPowerShell As New ArrayList
  28:              Dim aryIAsyncResult As New ArrayList
  29:              Dim strCmd As String = ""
  30:              For i As Integer = 0 To strCmds.Length - 1
  31:                  strCmd = strCmds(i)
  32:   
  33:                  Dim objPowerShell As PowerShell = PowerShell.Create()
  34:                  objPowerShell.RunspacePool = objRunspacePool
  35:                  objPowerShell.AddScript(strCmd)
  36:                  objPowerShell.AddCommand("Out-String")
  37:                  'PowerShellコマンド実行
  38:                  Dim objIAsyncResult As IAsyncResult = objPowerShell.BeginInvoke()
  39:   
  40:                  aryPowerShell.Add(objPowerShell)
  41:                  aryIAsyncResult.Add(objIAsyncResult)
  42:              Next
  43:   
  44:              Do While (aryPowerShell.Count > 0)
  45:                  For i As Integer = 0 To aryPowerShell.Count - 1
  46:   
  47:                      Dim objPowerShell As PowerShell = aryPowerShell(i)
  48:                      Dim objIAsyncResult As IAsyncResult = aryIAsyncResult(i)
  49:   
  50:                      If Not IsNothing(objPowerShell) Then
  51:                          If objIAsyncResult.IsCompleted Then
  52:                              Dim objResultCollection As PSDataCollection(Of PSObject) = objPowerShell.EndInvoke(objIAsyncResult)
  53:   
  54:                              'PowerShellコマンドの実行結果を出力
  55:                              For Each objResult As PSObject In objResultCollection
  56:                                  Debug.WriteLine(objResult.ToString)
  57:                              Next
  58:   
  59:                              'PowerShellのエラーを出力
  60:                              Dim objErrors As Collection(Of ErrorRecord) = objPowerShell.Streams.Error.ReadAll
  61:                              For Each objErrorRecord As ErrorRecord In objErrors
  62:                                  Debug.WriteLine(objErrorRecord.Exception.Message)
  63:                              Next
  64:   
  65:                              objPowerShell.Dispose()
  66:                              aryPowerShell.RemoveAt(i)
  67:                              aryIAsyncResult.RemoveAt(i)
  68:                              Exit For
  69:                          End If
  70:                      End If
  71:   
  72:                  Next
  73:                  Threading.Thread.Sleep(100)
  74:              Loop
  75:          Catch ex As Exception
  76:              Debug.WriteLine(ex.ToString)
  77:          Finally
  78:              objRunspacePool.Close()
  79:          End Try
  80:      End Sub

C# PowerShell非同期実行サンプル

 1:      //非同期のコマンド指定
   2:      string[] strAsyncCmds = {"Export-VM ?Name VM01 ?Path D:\\Exp", "Start-VM VM02", "Get-VM", "Start-VM VM03"};
   3:         
   4:      //コマンドを実行
   5:      AsyncPowershell(strServer, strAccount, strPassword, strAsyncCmds);
   6:   
   7:      static void AsyncPowershell(string strServer, string strAccount, string strPassword, string[] strCmds)
   8:      {
   9:          //パスワードを暗号化
  10:          SecureString objSecureString = new SecureString();
  11:          foreach (char c in strPassword.ToCharArray()) { objSecureString.AppendChar(c); }
  12:          PSCredential objPSCredential = new PSCredential(strAccount, objSecureString);
  13:   
  14:          //WinRMを利用してリモートでPowerShellを実行
  15:          Uri objUri = new Uri("http://" + strServer + ":5985/wsman");
  16:          WSManConnectionInfo objWSManConnectionInfo = new WSManConnectionInfo(objUri, "http://schemas.microsoft.com/powershell/Microsoft.PowerShell", objPSCredential);
  17:   
  18:          int intRunspace = strCmds.Length;
  19:          using (RunspacePool objRunspacePool = RunspaceFactory.CreateRunspacePool(1, intRunspace, objWSManConnectionInfo))
  20:          {
  21:              //objRunspacePool.ApartmentState = Threading.ApartmentState.STA
  22:              //objRunspacePool.ThreadOptions = PSThreadOptions.UseNewThread
  23:              objRunspacePool.Open();
  24:   
  25:              ArrayList aryPowerShell = new ArrayList();
  26:              ArrayList aryIAsyncResult = new ArrayList();
  27:              string strCmd = "";
  28:              for (int i = 0; i <= strCmds.Length - 1; i++)
  29:              {
  30:                  strCmd = strCmds[i];
  31:   
  32:                  PowerShell objPowerShell = PowerShell.Create();
  33:                  objPowerShell.RunspacePool = objRunspacePool;
  34:                  objPowerShell.AddScript(strCmd);
  35:                  objPowerShell.AddCommand("Out-String");
  36:                  //PowerShellコマンド実行
  37:                  IAsyncResult objIAsyncResult = objPowerShell.BeginInvoke();
  38:   
  39:                  aryPowerShell.Add(objPowerShell);
  40:                  aryIAsyncResult.Add(objIAsyncResult);
  41:              }
  42:   
  43:              while ((aryPowerShell.Count > 0))
  44:              {
  45:   
  46:                  for (int i = 0; i <= aryPowerShell.Count - 1; i++)
  47:                  {
  48:                      PowerShell objPowerShell = (PowerShell)aryPowerShell[i];
  49:                      IAsyncResult objIAsyncResult = (IAsyncResult)aryIAsyncResult[i];
  50:   
  51:                      if ((objPowerShell != null))
  52:                      {
  53:                          if (objIAsyncResult.IsCompleted)
  54:                          {
  55:                              PSDataCollection<PSObject> objResultCollection = objPowerShell.EndInvoke(objIAsyncResult);
  56:   
  57:                              //PowerShellコマンドの実行結果を出力
  58:                              foreach (PSObject objResult in objResultCollection)
  59:                              {
  60:                                  Debug.WriteLine(objResult.ToString());
  61:                              }
  62:   
  63:                              //PowerShellのエラーを出力
  64:                              Collection<ErrorRecord> objErrors = objPowerShell.Streams.Error.ReadAll();
  65:                              foreach (ErrorRecord objErrorRecord in objErrors)
  66:                              {
  67:                                  Debug.WriteLine(objErrorRecord.Exception.Message);
  68:                              }
  69:   
  70:                              objPowerShell.Dispose();
  71:                              aryPowerShell.RemoveAt(i);
  72:                              aryIAsyncResult.RemoveAt(i);
  73:                              break;
  74:                          }
  75:                      }
  76:                  }
  77:                  System.Threading.Thread.Sleep(100);
  78:              }
  79:          }
  80:      }

以上、今回は知る人ぞ知る、PowerShellの「技」の一つをご紹介しました。PowerShellの非同期実行によって、より効率的なサーバー管理などにの役立てていただければ幸いです。

PowerShellとVisualStudioによるHyper-Vプロビジョニングにも是非お試しください。

著書の紹介欄

Hyper-Vで本格的なサーバー仮想環境を構築。仮想環境を設定・操作できる!

できるPRO Windows Server 2016 Hyper-V

◇Hyper-Vのさまざまな機能がわかる ◇インストールからの操作手順を解説 ◇チェックポイントやレプリカも活用できる Windows Server 2016 Hyper-Vは、仮想化ソフトウェア基盤を提供する機能であり、クラウドの実現に不可欠のものです。 本書では、仮想化の基礎知識から、Hyper-Vでの仮想マシンや仮想スイッチの設定・操作、プライベートクラウドの構築、Azureとの連携などを解説します。

初めてのWindows Azure Pack本が発売

Windows Azure Pack プライベートクラウド構築ガイド

本書は、Windows Azure PackとHyper-Vを利用し、企業内IaaS(仮想マシン提供サービス)を構成するための、IT管理者に向けた手引書です。試用したサーバーは、最小限度の物理サーバーと仮想マシンで構成しています。Windows Azure Packに必要なコンポーネントのダウンロード、実際にプライベートクラウド構築する過程を、手順を追って解説しています。これからプライベートクラウドの構築を検討するうえで、作業負担の軽減に役立つ一冊です。

ブログの著者欄

樋口 勝一

GMOインターネットグループ株式会社

1999年6月GMOインターネットグループ株式会社に入社。Windows Serverをプラットフォームとしたサービス開発から運用・保守まで幅広く担当。講演登壇や出版、ネット記事連載などでマイクロソフト社と強い信頼関係を構築。「マイクロソフトMVPアワード」を15度受賞し、インターネットソリューションのスペシャリストとして活躍。

採用情報

関連記事

KEYWORD

採用情報

SNS FOLLOW

GMOインターネットグループのSNSをフォローして最新情報をチェック