AppStream 2.0 Elastic Fleets を使ってRDP踏み台サーバーを作る方法

2021.11.27

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

しばたです。

DevelopersIOでは過去にAppStream 2.0のFleetをRDP接続の踏み台サーバー代わりに使う記事が公開されています。

本記事ではこの仕組みを先日リリースされたElastic Fleetsを使って再実装してみました。

この方式のメリット

単純にセキュアな踏み台が必要なだけであればAWSでは既にSSM Session Managerが存在しており、つい先日のアップデートでSSM Fleet ManagerでRDP接続もできる様になっています。

このためSSMが使える環境であればそもそもの話として踏み台サーバーが不要です。

この方式が価値を発揮するのは前掲の記事にもある様にPublicサブネットおよびVPC Endpointすらない完全にPrivateな環境に対して接続したい場合です。

現状SSM用のVPC Endpointは決して安価とは言えず気軽に導入できるものではありません。
踏み台を利用する頻度にも依りますがAppStream 2.0を使うのはコスト的にもメリットとなります。

従来より簡易に環境を作ることのできるElastic Fleetsはこの様な用途に最適です。

構築方法

より簡易に構築するためにAWS CLIを使います。
(残念ながらAppStream 2.0はCloudFromationやその他IaCツールのサポートが十分とは言えない状況です)

AWS CLIはVer.2.4.1(およびVer.1.22.10)以降であればElastic Fleets関連の機能を利用できます。

今回はAWS CLI 2.4.1 on PowerShell 7.2.0な環境を実行環境としました。

C:\> $PSVersionTable | Select-Object -ExpandProperty PSVersion

Major  Minor  Patch  PreReleaseLabel BuildLabel
-----  -----  -----  --------------- ----------
7      2      0

C:\> aws --version
aws-cli/2.4.1 Python/3.8.8 Windows/10 exe/AMD64 prompt/off

0. 前提条件

今回は私の検証アカウントに下図の様な完全にPrivateなVPC環境を用意します。

このVPC内に接続確認用のWindows Server 2019 EC2を1台用意しておき、今回作るAppStream 2.0 Elastic FleetsからRDP接続していきます。
VPC環境、接続用EC2の作成手順については割愛します。

1. 事前準備

Elastic Fleetsを利用するためのS3やVHDファイルなどの準備を行います。

通常のElastic Fleetsであれば公開するアプリケーションをVHDファイルの形で定義する必要がありますが、今回はWindows標準のRDPクライアント(C:\WINDOWS\system32\mstsc.exe)を使うためVHDファイルは不要です。
ただし、AppStream 2.0のApp block定義においてはVHDファイルやセットアップスクリプトは指定必須項目となっているためダミーの空ファイルを用意してやる必要があります。

まずは最初にリソース保存用のS3を作ります。
既存のものを使っても良いですが今回はelastic-fleets-sample-20211122という名前のバケットを新規に作ります。

aws s3コマンドで以下の様な感じでバケット作成、バケットポリシーを設定してやります。

# Create S3 bucket
$bucketName = 'elastic-fleets-sample-20211127'
aws s3 mb "s3://$bucketName" --region ap-northeast-1
# Set public access block
aws s3api put-public-access-block --bucket $bucketName `
    --public-access-block-configuration 'BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true'
# Set bucket policy for AppStream 2.0 service access
$policy = @"
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowAppStream2.0ToRetrieveObjects",
            "Effect": "Allow",
            "Principal": {
                "Service": "appstream.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::$bucketName/*"
        }
    ]
}
"@ -replace '"','\"'
aws s3api put-bucket-policy --bucket $bucketName --policy $policy

エラー無くバケットが作成されていればOKです。

続けてVHDファイル(ダミー)とセットアップスクリプト(ダミー)を用意しS3バケットにアップロードしてやります。
以下のコマンドを実行すると空のdummy.vhdxdummy.ps1を用意しバケットの/rdp/配下にアップロードします。

# Upload dummy vhdx and setup script
$bucketName = 'elastic-fleets-sample-20211127'
$appBlockName = 'rdp'
try {
    $tempFile = [IO.Path]::GetTempFileName()
    aws s3 cp $tempFile "s3://$bucketName/$appBlockName/dummy.vhdx"
    aws s3 cp $tempFile "s3://$bucketName/$appBlockName/dummy.ps1"
} finally {
    if (Test-Path -LiteralPath $tempFile) {
        Remove-Item -LiteralPath $tempFile
    }
}

あとは公開アプリケーションで使うアイコン画像を用意してアップロードします。
アイコン画像は各自適当にご用意ください。

# Upload icon image file
$iconFilePath = 'C:\temp\my-rdp-icon.png' # Iconファイルは自分で用意する
aws s3 cp "$iconFilePath" "s3://$bucketName/$appBlockName/app-icon.png"

S3バケットが以下の様になっていればOKです。

(/rdp/dummy.vhdxdummy.ps1app-icon.pngがある)

(AppStream 2.0サービスからのGetを許可するバケットポリシーが設定されている)

2. App blockの作成

App blockの作成はaws appstream create-app-blockコマンドで可能です。
ちょっと長いですが以下の様な感じで各パラメーターを設定してやります。

$bucketName = 'elastic-fleets-sample-20211127'
$appBlockName = 'rdp'
aws appstream create-app-block --name $appBlockName `
    --display-name 'Remote Desktop Service client' `
    --source-s3-location "S3Bucket=$bucketName,S3Key=$appBlockName/dummy.vhdx" `
    --setup-script-details "ScriptS3Location={S3Bucket=$bucketName,S3Key=$appBlockName/dummy.ps1},ExecutablePath=C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe,ExecutableParameters=C:\AppStream\AppBlocks\$appBlockName\dummy.ps1,TimeoutInSeconds=60"

エラー無く終了すればOKで結果はこんな感じになります。

3. Applicationの作成

Applicationの作成はaws appstream create-applicationコマンドを使います。
こちらは以下の様な感じで各パラメーターを設定してやります。

$bucketName = 'elastic-fleets-sample-20211127'
$appBlockName = 'rdp'
aws appstream create-application --name 'rdp' `
    --display-name 'Remote Desktop Service client' `
    --icon-s3-location "S3Bucket=$bucketName,S3Key=$appBlockName/app-icon.png" `
    --launch-path 'C:\WINDOWS\system32\mstsc.exe' `
    --working-directory 'C:\WINDOWS\system32\' `
    --platforms 'WINDOWS_SERVER_2019' `
    --instance-families 'GENERAL_PURPOSE' `
    --app-block-arn $(
        aws appstream describe-app-blocks --output json | ConvertFrom-Json
            | Select-Object -ExpandProperty AppBlocks
            | Where-Object { $_.Name -eq $appBlockName }
            | Select-Object -ExpandProperty Arn 
    )

--launch-pathに直接C:\WINDOWS\system32\mstsc.exeを指定してやります。
また紐づけるApp blockはARNで指定してやる必要があるのですが、App blcokの情報を取得するaws appstream describe-app-blocksコマンドがこなれておらず--name--filetersパラメーターが無いため気合でARNを取得しています。

作成した結果はこんな感じ。

4. Elastic Fleetの作成

Fleetの作成はaws appstream create-fleetコマンドを使います。
このコマンド自体は以前からありますが、Elastic Fleets向けにいくつかのパラメーターが更新されています。

# Create new Elastic Fleet
$fleetName = 'my-rdp-elastic-fleet'
$fleetSubnetId1 = 'subnet-1234567890'   # Set your vpc subnet ids
$fleetSubnetId2 = 'subnet-1111111111'   # Set your vpc subnet ids
$fleetSecurityGroupId = 'sg-1234567890' # Set your fleet security group id
aws appstream create-fleet --name "$fleetName" `
    --display-name 'My RDP Elastic Fleet' `
    --fleet-type 'ELASTIC' `
    --instance-type 'stream.standard.small' `
    --platform 'WINDOWS_SERVER_2019' `
    --max-concurrent-sessions 5 `
    --vpc-config "SubnetIds=$fleetSubnetId1,$fleetSubnetId2,SecurityGroupIds=$fleetSecurityGroupId" `
    --no-enable-default-internet-access `
    --stream-view 'APP'

--fleet-typeパラメーターに新しくELASTICが増えておりElastic Fleetsを指定可能となっています。
スケーリング設定は--max-concurrent-sessionsパラメーターで最大セッション数の指定のみになっています。
その他のパラメーターは従来のものとだいたい同じですが微妙に異なる点もあるため詳細はドキュメントをご覧ください。

そして、このコマンドを実行した直後の状態ではまだFleetのみが存在しApplicationとの紐づけがありません。
aws appstram associate-application-fleetコマンドを使いFleetで公開するApplicationを設定してやります。

# Associate applications to elastic fleet
$fleetName = 'my-rdp-elastic-fleet'
$applicationName = 'rdp'
aws appstream associate-application-fleet --fleet-name $fleetName `
    --application-arn $(
        aws appstream describe-applications --output json | ConvertFrom-Json
            | Select-Object -ExpandProperty Applications
            | Where-Object { $_.Name -eq $applicationName }
            | Select-Object -ExpandProperty Arn 
    )

Application作成時と同様でApplicationのARN取得は気合いでなんとかします。

出来上がった内容に問題がなければaws appstram associate-application-fleetコマンドでFleetを開始しておきます。

# Start elastic fleet
$fleetName = 'my-rdp-elastic-fleet'
aws appstream start-fleet --name $fleetName

これで無事Fleetまでの準備が整いました。

5. 後作業

あとのStackの作成+Fleetとの紐づけ、ユーザー登録といった作業は従来のFleetと同じです。 環境に合わせてよしなに設定してください。

また、RDP接続する先のEC2のセキュリティグループはFleetに割り当てたセキュリティグループからの接続を許可する様にしておきます。

動作確認

AppStream 2.0環境に接続するとRDP Clientのアプリケーションが公開されていますので選択してやります。

RDP Client(mstsc.exe)が起動しますのでよしなに動作確認用のEC2に接続を試みます。

(分かりやすさのためネイティブアプリケーションモード不使用)

無事完全Privateな環境にあるEC2に接続できました。

【補足】キーバインドについて

Elastic Fleetsではベースイメージに手を加えないためRDP Clientは英語版となります。
Fleet自体に対する入力メソッドはAppStream 2.0のRegional Settingsで変更できるものの、RDP Client → 接続先EC2 の部分ではこの変更は利かず英語キーバインドとなるのでご注意ください。
(英語キーバインドでも日本語入力は可能。言語切り替えがShift + Caps Lockになる)

ベースOSの仕様に依存する部分を許容できない場合は従来どおりImage Builderからカスタムイメージを作る様にしてください。

参考資料 : AWSブログ

参考資料としてAWS Desktop and Application Streaming Blogの以下の記事を紹介しておきます。

こちらはLinuxインスタンスを使ったElastic Fleetsでより堅牢な踏み台サーバーを作る記事となっています。
Linuxインスタンス向けのApp blockやApplicaiton設定例が詳しく記載されているので参考にしてみてください。

ちなみにWindowsインスタンスのRDP向けの設定についても記述があるのですが、かなり端折られているので私の記事のほうが役に立つと思います。
(というか、こちらのブログのWindows向けの記述が足りなかったのが本記事を書こうと思った直接の動機だったりします...)

参考資料 : Tera Termの公開手順

最初に紹介したDevelopersIOの記事ではRDP Client以外にTera Termも公開アプリケーションに登録されていました。
おまけとして以前の記事の内容をベースにTera Termの公開手順も記載しておきます。

VHDファイル、セットアップスクリプト

VHDファイルはこんな感じでC:\temp\app.vhdxにTera Term(Ver.4.106)の内容をまとめることができます。

# 要管理者権限
mkdir C:\temp\
cd C:\temp\

# Hyper-V moduleが無い環境もあるのでdiskpartコマンドでVHD(vhdx)作成を頑張る
try {
    $vhdPath = "C:\temp\app.vhdx"     # VHDファイルへの絶対パス
    $logPath = "C:\temp\diskpart.log" # diskpart実行ログ
    $vhdSize = 512 # VHDの最大サイズ (MB)
    $tempFileName = "create_vhd_$(Get-Date -Format "yyyyMMdd_HHmmss").txt"
@"
create vdisk file=$vhdPath maximum=$vhdSize type=expandable
select vdisk file=$vhdPath
attach vdisk
convert mbr
create partition primary
format fs=ntfs quick
detach vdisk
exit
"@ | Out-File ".\$tempFileName" -Encoding oem
    diskpart /s $tempFileName > $logPath
} finally {
    if (Test-Path -LiteralPath ".\$tempFileName") {
        Remove-Item -LiteralPath ".\$tempFileName"
    }
}

# VHDのマウント
$imagePath = 'C:\temp\app.vhdx'
$mountDrive = 'Z:\'
$vhdx = Mount-DiskImage -ImagePath $imagePath -StorageType VHDX -NoDriveLetter
Get-Disk -DeviceId $vhdx.Number | Get-Partition | Add-PartitionAccessPath -AccessPath $mountDrive

# Tera Termのダウンロードと展開
$ProgressPreference = 'SilentlyContinue'
$params = @{
    Uri     = 'https://ja.osdn.net/frs/redir.php?m=nchc&f=ttssh2%2F74780%2Fteraterm-4.106.zip'
    OutFile = 'Z:\teraterm.zip'
}
Invoke-WebRequest @params
Expand-Archive -LiteralPath Z:\teraterm.zip -DestinationPath Z:\
Move-Item -Path Z:\teraterm-4.106\* -Destination Z:\
# 不要ファイルの削除
Remove-Item -LiteralPath Z:\teraterm-4.106\
Remove-Item -LiteralPath Z:\teraterm.zip

# VHDのアンマウント
Dismount-DiskImage -ImagePath 'C:\temp\app.vhdx'

セットアップスクリプトはこんな感じ。
C:\Program Files\TeraTerm\にVHDをマウントします。

setup.ps1

#
# This script is for AppStream 2.0 Elastic fleet
#

# mount vhdx file
$appBlockName = 'teraterm'
$appBlockVHDPath = "C:\AppStream\AppBlocks\$appBlockName\app.vhdx"
$mountPoint = 'C:\Program Files\TeraTerm\'
if (-not (Test-Path -LiteralPath $mountPoint)) {
    mkdir $mountPoint
}
$vhdx = Mount-DiskImage -ImagePath $appBlockVHDPath -StorageType VHDX -NoDriveLetter
Get-Disk -DeviceId $vhdx.Number | Get-Partition | Add-PartitionAccessPath -AccessPath $mountPoint

アイコンファイルは自分で用意してください。

App block、Applicationの作成

App blockとApplicationはこんな感じ作れます。

# App blockの作成
$bucketName = 'elastic-fleets-sample-20211127'
$appBlockName = 'teraterm'
aws appstream create-app-block --name $appBlockName `
    --display-name 'Remote Desktop Service client' `
    --source-s3-location "S3Bucket=$bucketName,S3Key=$appBlockName/app.vhdx" `
    --setup-script-details "ScriptS3Location={S3Bucket=$bucketName,S3Key=$appBlockName/setup.ps1},ExecutablePath=C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe,ExecutableParameters=C:\AppStream\AppBlocks\$appBlockName\setup.ps1,TimeoutInSeconds=60"

# Applicationの作成 
#  * app-icon.png は自分で用意してね
$bucketName = 'elastic-fleets-sample-20211127'
$appBlockName = 'teraterm'
aws appstream create-application --name 'teraterm' `
    --display-name 'Tera Term' `
    --icon-s3-location "S3Bucket=$bucketName,S3Key=$appBlockName/app-icon.png" `
    --launch-path 'C:\Program Files\TeraTerm\ttermpro.exe' `
    --working-directory 'C:\Program Files\TeraTerm\' `
    --platforms 'WINDOWS_SERVER_2019' `
    --instance-families 'GENERAL_PURPOSE' `
    --app-block-arn $(
        aws appstream describe-app-blocks --output json | ConvertFrom-Json
            | Select-Object -ExpandProperty AppBlocks
            | Where-Object { $_.Name -eq $appBlockName }
            | Select-Object -ExpandProperty Arn 
    )

あとはFleetに登録してやればOKです。

最後に

ざっとこんな感じです。

AppStream 2.0を踏み台サーバー代わりに使うといったシンプルな用途にElastic Fleetsは非常に有用です。
Image Builderを一切使わずあっという間にFleetの準備が可能となりました。
加えて従来のFleetとは異なりDesired Capacityの概念が無い(Desired Capacity=0相当にできる)のも魅力です。

動作確認時に補足した様にベースOSに手を加えないことによる制約もありますので、構築・メンテナンスの容易さとカスタマイズ性を天秤にかけたうえでElastic Fleetsを選択すると良いでしょう。