Windows Container on Fargateで遊ぶ準備をしてみた

本投稿はAWS Containers Advent Calendar 2021の9日目の記事です


ECS on FargateでもWindowsコンテナのサポートが始まりました。コンテナ関連テクロジーのアップデート速度は本当に速いですね。

こちらの投稿では、Windowsコンテナを始める一歩として、サンプルアプリを使ってWindowsコンテナをビルドし、Fargate + ALBで起動させる所まで試してみたいと思います

# Windows Containers with Amazon ECS on AWS Fargate

リリース時に出ている[AWSブログ(英語)](https://aws.amazon.com/jp/blogs/containers/running-windows-containers-with-amazon-ecs-on-aws-fargate/)では、マネージメントコンソール上からの操作でWindowsコンテナを起動する流れが説明されています。手っ取り早くWindowsコンテナ試してみたい方は、そちらをみてみてください。

ブログ記事の中でも触れられていますが、 Windows コンテナ on Fargateで現在サポートされているWindows Serverファミリーとしては、以下の2パターンです。

* Windows Server 2019 Core
* Windows Server 2019 Full

先ほどのブログ記事でも触れられているのですが、Windowsコンテナが起動するホスト側のOS環境とコンテナ側のOS環境は一致させておく必要がある為、
例えば、Windows Server 2022のコンテナイメージをFargate上で起動させることはできません。その為、Windows Server 2016やWindows Server 2022の Windows コンテナを利用したい場合は、EC2を利用する必要がありますね。


# Windows コンテナの基本のあれこれ

## Isolation方式
Windowsコンテナのそもそもの仕組みとして、コンテナ化方式は以下のパターンがあります。

* Process Isolation (プロセス分離モード)
* Hyper-V Isolation Mode (Hyper-V分離モード)

Process Isolationは規定のIsolationモードであり、コンテナホストのOSのカーネルを共有しながら、ユーザモードプロセスとしてWindowsコンテナを稼働させます。つまり、コンテナホストOSとコンテナ上のOSのバージョンが一致している必要があることになります。一方、Hyper-V分離モードは、その言葉通り、Hyper-Vハイパーバイザーを利用してContainer毎のisolationを実現しています。その為、Hyper-V分離モードの場合はコンテナホストOSとバージョンが一致していないWindowsコンテナを起動することができます。

## 利用可能なベースOSイメージ
WindowsコンテナはベースOSイメージが提供されています。ベースOSイメージに加えて、.NETアプリ向けなどの用途向けなどがあり、要件にあわせて使い分けていく必要がありそうです。高機能なイメージはイメージサイズも比較的増える傾向にある様です。

* Nano Server
    * mcr.microsoft.com/windows/nanoserver:1809
    * .Net Coreアプリケーション向けのベースOSイメージ
    * 手元の環境で257MBほど
* Windows Server Core
    * mcr.microsoft.com/windows/servercore:ltsc2019
    * LTSC/SACのServer CoreベースのOSイメージ。IISも利用可能
    * 手元の環境で5.74GBほど
* Windows
    * mcr.microsoft.com/windows:1809
    * Server CoreベースOSイメージでも不足している依存関係がある場合に利用するもの
    * 手元の環境で15.1GBほど。。。

# それではやってみよう
今回はこんな環境を作って遊んでみました。そもそも手元の環境がMacなので、EC2インスタンスとしてWindows Server2019を作りました。

![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63007/6aa6f560-defe-1886-2a9c-e9ac4b2d3fbe.png)



BuildServerを作った意図としては、

* イメージのサイズが大きいので、Buildの際はキャッシュを効かせたい
* コンテナホストのOSバージョンとコンテナイメージのOSバージョンを明示的にコントロールしたい

といったあたりです。


## 参考にしたアプリ
GitHubで公開されている[こちら](https://github.com/dotnet/dotnet-docker)を利用しました。リポジトリ内の`samples/aspnetapp/Dockerfile.windowsservercore-iis-x64`がIISを利用したASP.NETアプリのDockerファイルになっています。

こちらのDockerファイルはWindows Server 2022ベースになっている為、Fargate上で動かす場合、利用するコンテナイメージをWindows Server 2019ベースに変更する必要があります。具体的には、以下の2箇所を修正すると、Fargateで起動することができる様になります。オリジナルなファイルの全体は[こちら](https://github.com/dotnet/dotnet-docker/blob/main/samples/aspnetapp/Dockerfile.windowsservercore-iis-x64)から確認してみてください。

```diff

diff --git a/samples/aspnetapp/Dockerfile.windowsservercore-iis-x64 b/samples/aspnetapp/Dockerfile.windowsservercore-iis-x64
index 27a4ad09..9e85175f 100644
--- a/samples/aspnetapp/Dockerfile.windowsservercore-iis-x64
+++ b/samples/aspnetapp/Dockerfile.windowsservercore-iis-x64
@@ -1,7 +1,7 @@
 # escape=`
 
 # https://hub.docker.com/_/microsoft-dotnet
-FROM mcr.microsoft.com/dotnet/sdk:6.0-windowsservercore-ltsc2022 AS build
+FROM mcr.microsoft.com/dotnet/sdk:6.0.100-windowsservercore-ltsc2019 AS build
 WORKDIR /source
 
 # copy csproj and restore as distinct layers
@@ -15,7 +15,7 @@ WORKDIR /source/aspnetapp
 RUN dotnet publish -c release -o /app --no-restore
 
 # final stage/image
-FROM mcr.microsoft.com/dotnet/aspnet:6.0-windowsservercore-ltsc2022
+FROM mcr.microsoft.com/dotnet/aspnet:6.0.0-windowsservercore-ltsc2019
 
 # Only needed for this sample because the sample project is targeting .NET 5
 ENV DOTNET_ROLL_FORWARD=LatestMajor

--
```

## CodeBuildの設定
CodeBuildは、ソースコードリポジトリにあるbuildspec.ymlの内容に応じてビルド処理を実行してくれます。今回は、CodeBuildからSSMのRunCommandを実行してビルドサーバ上でイメージを作成する様にしてみました。buildフェーズでは、RunCommandのJob IDをもとに、ビルドが完了するまで、RunCommandのステータスをチェックする様にしています。

```yml
version: 0.2

phases:
  pre_build:
    commands:
      - echo "codeRepoName ${codeRepoName}"
      - echo "imageRepoName ${imageRepoName}"
      - echo "awsAccountId ${awsAccountId}"
      - echo "buildInstance ${buildInstanceId}"
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image using by SSM RunCommand
      - |
        sh_command_id=$(aws ssm send-command \
          --instance-ids ${buildInstanceId} \
          --document-name "AWS-RunRemoteScript" \
          --parameters '{"sourceType":["S3"],"sourceInfo":["{\"path\":\"https://s3.amazonaws.com/<bucket_name>/imageBuild.ps1\"}"],"commandLine":["imageBuild.ps1 '"${codeRepoName} ${imageRepoName} ${awsAccountId}"'"]}' \
          --output text --query "Command.CommandId" \
          --region ap-northeast-1)
      - |
        while true; \
          do \
            sh_command_status=$(aws ssm list-command-invocations \
              --command-id $sh_command_id \
              --details \
              --output text --query "CommandInvocations[0].Status" \
              --region ap-northeast-1); \
            echo "CommandStatus $sh_command_status"; \
            if [ $sh_command_status = "Success" -o $sh_command_status = "Failed" ]; then \
              break; \
            fi \
            sleep 5; \
          done
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
```

実に雑ですね。真似しちゃいけない。


## RunCommandで実行しているスクリプト
RunCommandでは、S3上にあるスクリプトを指定すると、実行時にスクリプトをダウンロードして実行してくれます。単にソースコードを持ってきて、イメージ作成&pushを行なっています。
こちらもまた雑な内容です。

```powershell

$codeRepoName= $Args[0]
$imageRepoName=$Args[1]
$awsAccountId=[String] $Args[2]

$codeRepoURI = "https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/" + $codeRepoName
$imageRegistry = $awsAccountId + ".dkr.ecr.ap-northeast-1.amazonaws.com" 
$imageRepoURI = $imageRegistry + "/" + $imageRepoName

Write-Output ("codeRepoURI: " + $codeRepoURI)
Write-Output ("imageRepoURI: " + $imageRepoURI)
Write-Output ("imageRegistry: " + $imageRegistry)


Set-Location "C:\"
$PATH="C:\" + $codeRepoName
If(!(test-path $PATH))
{
	git config --global credential.helper "!aws codecommit credential-helper $@"
	git config --global credential.UseHttpPath true
	git clone $codeRepoURI
    If(!($?)){
        exit 1
    }
}
Set-Location $PATH
if(!($?)){
    exit 2
}
git pull

Set-Location "samples\aspnetapp"
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin $imageRegistry
if(!($?)){
    exit 3
}
docker build -t "$($imageRepoName):latest" -f Dockerfile.windowsservercore-iis-x64 .
if(!($?)){
    exit 4
}
docker tag "$($imageRepoName):latest" "$($imageRepoURI):latest"
docker push "$($imageRepoURI):latest"
if(!($?)){
    exit 5
}

exit 0
```

# Faragetにデプロイ！
ここまでくると、いつもの手順でTask定義上で、ECR上にカスタムのWindowsコンテナイメージを指定するだけです。

![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63007/afca4959-b202-07ff-4bb2-339c22b457aa.png)

![b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63007/a0b975e0-d965-fde0-1da3-9682b36acb36.png)


# 最後に
SystemManagerすげー