App2ContainerがLinux .NETをサポートしたので、.NET6 + Amazon Linux2からApp Runnerへデプロイしてみた

2021.12.11

いわさです。

AWS App2ContainerでLinuxの.NETアプリケーションがサポートされました。

AWS App2Container (A2C) now supports containerization of .NET running on Linux

従来は以下がサポートされていましたが.NET+Linuxが加わった形となります。

  • Java + Linux
  • .NET + Windows

FAQのサポートされているアプリケーションにはまだ記載がありませんが、そのうち追加されそうですね。

Q: What application types are supported by App2Container?

App2Container (A2C) currently supports the following application types: 1) ASP.NET (.NET 3.5+) web applications running in IIS 7.5+ on Windows. A2C packages these applications with Windows containers. 2) Java applications running on Linux standalone JBoss, Apache Tomcat, and generic Java applications (Spring Boot, IBM WebSphere, Oracle WebLogic, etc.). A2C packages Java applications with Linux containers.

本日は、ASP.NET CoreアプリケーションをAmazon Linux2でセットアップし、App2Containerを使ってApp Runnerへ移行してみました。

App2Containerなど事前準備

.NET SDKインストール

まずはAmazon Linux2へ.NETをインストールします。
せっかくなので今回は最近登場した.NET6でやってみたいと思います。

CentOSベースでインストールします。

sh-4.2$ sudo rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm
Retrieving https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm
Preparing...                          ################################# [100%]
Updating / installing...
   1:packages-microsoft-prod-1.0-1    ################################# [100%]
sh-4.2$ sudo yum install dotnet-sdk-6.0
Loaded plugins: extras_suggestions, langpacks, priorities, update-motd
packages-microsoft-com-prod                                                                                          | 3.0 kB  00:00:00
packages-microsoft-com-prod/primary_db                                                                               | 492 kB  00:00:00
Resolving Dependencies
--> Running transaction check

...

Installed:
  dotnet-sdk-6.0.x86_64 0:6.0.100-1

Dependency Installed:
  aspnetcore-runtime-6.0.x86_64 0:6.0.0-1   aspnetcore-targeting-pack-6.0.x86_64 0:6.0.0-1  dotnet-apphost-pack-6.0.x86_64 0:6.0.0-1
  dotnet-host.x86_64 0:6.0.0-1              dotnet-hostfxr-6.0.x86_64 0:6.0.0-1             dotnet-runtime-6.0.x86_64 0:6.0.0-1
  dotnet-runtime-deps-6.0.x86_64 0:6.0.0-1  dotnet-targeting-pack-6.0.x86_64 0:6.0.0-1      netstandard-targeting-pack-2.1.x86_64 0:2.1.0-1

Complete!
sh-4.2$ dotnet --version
6.0.100

App2Containerインストール

.NET Linux向けのチュートリアルはまだ無いので、以下のJava + Linuxの手順で進めてみましょう。
.NETセットアップ以外はほぼ同じはずです。

sh-4.2$ sudo curl -o AWSApp2Container-installer-linux.tar.gz https://app2container-release-us-east-1.s3.us-east-1.amazonaws.com/latest/linux/AWSApp2Container-installer-linux.tar.gz
sh-4.2$ sudo tar xvf AWSApp2Container-installer-linux.tar.gz
sh-4.2$ sudo ./install.sh
sh-4.2$ sudo app2container --version
app2container version 1.9

Dockerインストール

App2ContainerでDockerビルドする際に必要になります。
こちらも事前にインストールしておきます。

こちらはECSのAmazon Linux2へセットアップする手順を使いましょう。

sh-4.2$ sudo amazon-linux-extras install docker
Installing docker

...

sh-4.2$ docker --version
Docker version 20.10.7, build f0df350

移行アプリの用意

事前準備はOKなので、次に移行対象のWebアプリを用意したいと思います。

dotnet newでASP.NET CoreのWebアプリを作ってそれをそのまま移行元に使いましょう。

sh-4.2$ dotnet new webapp --name iwasatest
The template "ASP.NET Core Web App" was created successfully.
This template contains technologies from parties other than Microsoft, see https://aka.ms/aspnetcore/6.0-third-party-notices for details.

Processing post-creation actions...
Running 'dotnet restore' on /home/ssm-user/hoge/iwasatest/iwasatest.csproj...
  Determining projects to restore...
  Restored /home/ssm-user/hoge/iwasatest/iwasatest.csproj (in 119 ms).
Restore succeeded.

まずはローカルでリリースビルドして実行・確認だけしておきます。

sh-4.2$ dotnet publish -c Release --self-contained true -r linux-x64
Microsoft (R) Build Engine version 17.0.0+c9eb9dd64 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  Restored /home/ssm-user/hoge/iwasatest/iwasatest.csproj (in 8.93 sec).
  iwasatest -> /home/ssm-user/hoge/iwasatest/bin/Release/net6.0/linux-x64/iwasatest.dll
  iwasatest -> /home/ssm-user/hoge/iwasatest/bin/Release/net6.0/linux-x64/publish/

sh-4.2$ dotnet /home/ssm-user/hoge/iwasatest/bin/Release/net6.0/linux-x64/iwasatest.dll &
[1] 3518
sh-4.2$ info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /home/ssm-user/hoge/iwasatest/

sh-4.2$ curl --insecure https://localhost:5001/
<!DOCTYPE html>
<html lang="en">
   ...
</html>

良さそうですね!

移行

あとは、中山順博さんの以下の記事(Java + Linux)と同じ流れですね。

検出と分析

一点注意なのが、dotnet runだと検出はされますが分析時にエラーになりますのでdotnetで実行されているか注意してください。

sh-4.2$ sudo app2container inventory
{
                "dotnet-generic-5afe04c8": {
                                "processId": 1825,
                                "cmdline": "dotnet run ",
                                "applicationType": "dotnet-generic",
                                "webApp": ""
                }
}

sh-4.2$ sudo app2container analyze --application-id dotnet-generic-5afe04c8
Failed Analyzing application dotnet-generic-5afe04c8

⚠️  App2Container only supports containerization of applications with listening ports.
Error: App2Container only supports containerization of applications with listening ports.
sh-4.2$ sudo app2container inventory
{
                "dotnet-generic-62230333": {
                                "processId": 3518,
                                "cmdline": "dotnet /home/ssm-user/hoge/iwasatest/bin/Release/net6.0/linux-x64/iwasatest.dll ",
                                "applicationType": "dotnet-generic",
                                "webApp": ""
                }
}
sh-4.2$ sudo app2container analyze --application-id dotnet-generic-62230333
✔ Created artifacts folder /root/app2container/dotnet-generic-62230333
✔ Generated analysis data in /root/app2container/dotnet-generic-62230333/analysis.json
👍 Analysis successful for application dotnet-generic-62230333

💡 Next Steps:
1. View the application analysis file at /root/app2container/dotnet-generic-62230333/analysis.json.
2. Edit the application analysis file as needed.
3. Start the containerization process using this command: app2container containerize --application-id dotnet-generic-62230333

コンテナ化

app2container containerizeします。
Dockerが起動されていないとエラーになるので、これより前にstartしておきましょう。

sh-4.2$ sudo service docker start
Redirecting to /bin/systemctl start docker.service
sh-4.2$ sudo app2container containerize --application-id dotnet-generic-62230333
✔ AWS prerequisite check succeeded
✔ Docker prerequisite check succeeded
✔ Extracted container artifacts for application
✔ Entry file generated
✔ Dockerfile generated under /root/app2container/dotnet-generic-62230333/Artifacts
✔ Generated dockerfile.update under /root/app2container/dotnet-generic-62230333/Artifacts
✔ Generated deployment file at /root/app2container/dotnet-generic-62230333/deployment.json
✔ Deployment artifacts generated.
✔ Pre-validation succeeded.
👍 Containerization successful. Generated docker image dotnet-generic-62230333

💡 You're all set to test and deploy your container image.

Next Steps:
1. View the container image with "docker images" and test the application with "docker run --name dotnet-generic-62230333 -Pit dotnet-generic-62230333".
2. When you're ready to deploy to AWS, adjust the appropriate fields in /root/app2container/dotnet-generic-62230333/deployment.json to generate the desired deployment artifact. Note that by default "createEcsArtifacts" is set to true.
3. Generate deployment artifacts using "app2container generate app-deployment --application-id dotnet-generic-62230333".

生成された構成ファイルですが、デフォルトだとECSにデプロイするようになっています。
下記はApp Runnerへデプロイ先を変更するためにハイライト部分をデフォルトから変更したものになります。
FireLensのパラメータをOFFにするのを初回忘れました。気をつけてください。

deployment.json

{
       "a2CTemplateVersion": "1.0",
       "applicationId": "dotnet-generic-62230333",
       "imageName": "dotnet-generic-62230333",
       "exposedPorts": [
              {
                     "localPort": 5000,
                     "protocol": "tcp"
              },
              {
                     "localPort": 5001,
                     "protocol": "tcp"
              }
       ],
       "environment": [],
       "ecrParameters": {
              "ecrRepoTag": "latest"
       },
       "ecsParameters": {
              "createEcsArtifacts": false,
              "ecsFamily": "dotnet-generic-62230333",
              "cpu": 2,
              "memory": 4096,
              "dockerSecurityOption": "",
              "publicApp": true,
              "stackName": "a2c-dotnet-generic-62230333-ECS",
              "resourceTags": [
                     {
                            "key": "example-key",
                            "value": "example-value"
                     }
              ],
              "reuseResources": {
                     "vpcId": "",
                     "reuseExistingA2cStack": {
                            "cfnStackName": "",
                            "microserviceUrlPath": ""
                     },
                     "sshKeyPairName": "",
                     "acmCertificateArn": ""
              },
              "gMSAParameters": {
                     "domainSecretsArn": "",
                     "domainDNSName": "",
                     "domainNetBIOSName": "",
                     "createGMSA": false,
                     "gMSAName": ""
              },
              "deployTarget": "FARGATE",
              "dependentApps": []
       },
       "fireLensParameters": {
              "enableFireLensLogging": false,
              "logDestinations": [
                     {
                            "service": "cloudwatch",
                            "regexFilter": "^.*.$",
                            "streamName": "All-Logs"
                     }
              ]
       },
       "eksParameters": {
              "createEksArtifacts": false,
              "stackName": "a2c-dotnet-generic-62230333-EKS",
              "reuseResources": {
                     "vpcId": "",
                     "reuseExistingA2cStack": {
                            "cfnStackName": ""
                     },
                     "sshKeyPairName": ""
              },
              "gMSAParameters": {
                     "domainSecretsArn": "",
                     "domainDNSName": "",
                     "domainNetBIOSName": "",
                     "createGMSA": false,
                     "gMSAName": ""
              },
              "resourceTags": [
                     {
                            "key": "example-key",
                            "value": "example-value"
                     }
              ],
              "dependentApps": []
       },
       "appRunnerParameters": {
              "createAppRunnerArtifacts": true,
              "stackName": "a2c-dotnet-generic-62230333-AppRunner",
              "serviceName": "a2c-dotnet-generic-62230333",
              "autoDeploymentsEnabled": true,
              "resourceTags": [
                     {
                            "key": "example-key",
                            "value": "example-value"
                     }
              ]
       }
}

デプロイ

この後も対話形式で表示される手順に従ってデプロイするだけです。
今回はパイプラインのセットアップは行わず、CloudFormationのデプロイだけ行いました。

sh-4.2$ sudo app2container generate app-deployment --application-id dotnet-generic-62230333
Note - multiple ports are listed in deployment.json. Only the first port will be used in your App Runner deployment.
✔ AWS prerequisite check succeeded
✔ Docker prerequisite check succeeded
✔ Created ECR repository 550669467088.dkr.ecr.ap-northeast-1.amazonaws.com/dotnet-generic-62230333
✔ Pushed docker image 550669467088.dkr.ecr.ap-northeast-1.amazonaws.com/dotnet-generic-62230333:latest to ECR repository
✔ Generated AWS App Runner CloudFormation template at /root/app2container/dotnet-generic-62230333/AppRunnerDeployment/apprunner.yml
👍 CloudFormation templates and additional deployment artifacts generated successfully for application dotnet-generic-62230333

💡 You're all set to use AWS CloudFormation to manage your application stack.

Next Steps:
1. Edit the CloudFormation template as necessary.
2. Create an application stack using the AWS CLI or the AWS Console. AWS CLI command:

        aws cloudformation deploy --template-file /root/app2container/dotnet-generic-62230333/AppRunnerDeployment/apprunner.yml --capabilities CAPABILITY_IAM --stack-name a2c-dotnet-generic-62230333-AppRunner


3. Set up a pipeline for your application stack using app2container:

        app2container generate pipeline --application-id dotnet-generic-62230333
sh-4.2$ aws cloudformation deploy --template-file apprunner.yml --capabilities CAPABILITY_IAM --stack-name a2c-dotnet-generic-62230333-AppRunner --region ap-northeast-1

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - a2c-dotnet-generic-62230333-AppRunner

無事、Linuxローカル環境にあったASP.NET CoreプロジェクトをApp2Containerを使ってApp Runnerへコンテナ化して移行することが出来ました。

まとめ

本日は、App2Containerを使ってLinux上でホストされているASP.NET Coreアプリケーションをコンテナ化しApp Runnerへ移行してみました。

App2Containerの実行の流れ自体はJava + Linuxと変わりませんでした。特に詰まるところも.NET向けに考慮すべきところもなく移行が出来ました。
ASP.NET CoreのLinuxは、Visual StudioでDockerファイルが生成されるのもあってほぼほぼコンテナで動くことを前提にしているのだろうと思っていたので、Windows版よりもApp2Containerを使うシーンはもしかすると低いかもしれません。
ただ、Windows版よりも移行元アプリケーションへの制限が低いと思うので、コンテナ化に挑戦したい方はApp2Containerも試してみてください。