Amazon ECS のハンズオンでRuby on Railsアプリケーションのコンテナをデプロイしてみた

Amazon ECSを学習する上で、AWS公式ハンズオンは効果的な学習方法のひとつですが、ECSは項目数がとても多く、またマネジメントコンソール上の変更も多いです。今回はハンズオンと現在のコンソールの違いなどをまとめながら、実際のハンズオン内容を紹介します。
2024.06.18

Amazon ECSを学びたい

おのやんです。

みなさん、Amazon ECS(以下、ECS)を学びたいと思ったことはありませんか?私はあります。

ECSを学習する際の選択肢の一つに、AWS公式が出しているハンズオンがあります。ネットワークやアプリケーションなどのさまざまな分野のAWSリソースを、実際にマネジメントコンソールで操作して構築することができます。

私は直近でECSを用いたコンテナWebアプリケーション環境を構築することになり、そのキャッチアップとしてAWS公式のWebアプリケーションハンズオンをひととおり触っていました。

ECSのハンズオンを通して、コンソールが大きく変わっていたり、操作に注意が必要な部分が多々ありました(ハンズオン自体も数年前に公開されたものですし)ので、今回はそちらも踏まえながら改めて操作方法を紹介していきたいと思います。

目指す構成

今回は、こちらのコンテナ構成を目指します。最初にAWS Cloud9(以下、Cloud9)にアクセスし、Cloud9環境下でRuby on Rails(以下、Rails)アプリケーションのコンテナをビルドします。その後、Cloud9からAmazon ECR(以下、ECR)へRailsコンテナイメージをプッシュします。最後に、ECR上のコンテナイメージをもとにVPC上でコンテナをデプロイしていきます。

Dockerイメージの作成・起動

ECSはコンテナを管理するAWSリソースですので、まずコンテナそのものを触ってみます。今回は、Dockerが最初からインストールされているCloud9の環境で、コンテナやイメージを操作していきたいと思います。

AWSのアカウントにログインした後に、Cloud9のコンソール画面から「環境を作成」を押下します。

ハンズオンとは違いますが、今回は名前を少し変えてaws-test-cloud9という名前で環境を作成していきます。なおaws-testという接頭辞は私が個人的に検証する際のAWSリソース命名規則になります。普段から使用しているaws-test-vpcの上で起動させるため命名規則をこちらに合わせています。(後述しますが、ECSハンズオン関係のリソースはecs-testという命名規則で作成します)

環境タイプについては「新しいECSインスタンス」を選択しておきます。

インスタンスタイプはt3.small、プラットフォームはAmazon Linux 2023を選択しておきます。

今回はCloud9環境に対してSSMで接続することにします。この際、既存のVPC・サブネットを選択しておきます。こちらは「VPC設定」のトグルを開いて設定できます。

正常に作成されれば、十数秒〜数分でCloud9環境が作成されます。こちらのCloud9環境一覧画面の「開く」を押下して、Cloud9環境に入ります。

記事執筆時点で、Cloud9の統合開発環境画面はこんな感じになっています。左側のサイドバーにファイルやディレクトリが表示されていて、中央のセクションでコーディング可能です。また画面下部はコンソール画面となっています。

ハンズオンでは、こちらのCloud9コンソール上でコマンドを実行できます。ハンズオンに記載のあるコマンドですが、pythonコマンドに関しては記事執筆時点で有効になっていませんでした。python3コマンドで正常に実行されるようになるので、参考までにコマンドとその実行結果を以下に掲載しておきます。

$ docker --version
Docker version 25.0.3, build 4debf41

$ systemctl status docker.service
● docker.service - Docker Application Container Engine
     Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; preset: disabled)
     Active: active (running) since Tue 2024-06-11 02:18:42 UTC; 15min ago
TriggeredBy: ● docker.socket
       Docs: https://docs.docker.com
    Process: 1625 ExecStartPre=/bin/mkdir -p /run/docker (code=exited, status=0/SUCCESS)
    Process: 1630 ExecStartPre=/usr/libexec/docker/docker-setup-runtimes.sh (code=exited, status=0/SU>
   Main PID: 1636 (dockerd)
      Tasks: 11
     Memory: 482.9M
        CPU: 32.549s
     CGroup: /system.slice/docker.service
             └─1636 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --default->

Jun 11 02:18:40 ip-10-1-2-158.ap-northeast-1.compute.internal systemd[1]: Starting docker.service - D>
Jun 11 02:18:41 ip-10-1-2-158.ap-northeast-1.compute.internal dockerd[1636]: time="2024-06-11T02:18:4>
Jun 11 02:18:41 ip-10-1-2-158.ap-northeast-1.compute.internal dockerd[1636]: time="2024-06-11T02:18:4>
Jun 11 02:18:42 ip-10-1-2-158.ap-northeast-1.compute.internal dockerd[1636]: time="2024-06-11T02:18:4>
Jun 11 02:18:42 ip-10-1-2-158.ap-northeast-1.compute.internal dockerd[1636]: time="2024-06-11T02:18:4>
Jun 11 02:18:42 ip-10-1-2-158.ap-northeast-1.compute.internal dockerd[1636]: time="2024-06-11T02:18:4>
Jun 11 02:18:42 ip-10-1-2-158.ap-northeast-1.compute.internal systemd[1]: Started docker.service - Do>
Jun 11 02:18:42 ip-10-1-2-158.ap-northeast-1.compute.internal dockerd[1636]: time="2024-06-11T02:18:4>

$ aws --version && git --version && mysql --version && python3 -V && php -v && java -version
aws-cli/2.15.58 Python/3.11.8 Linux/6.1.90-99.173.amzn2023.x86_64 exe/x86_64.amzn.2023
git version 2.40.1
mysql  Ver 15.1 Distrib 10.5.23-MariaDB, for Linux (x86_64) using  EditLine wrapper
Python 3.9.16
PHP 8.2.15 (cli) (built: Jan 16 2024 12:19:32) (NTS gcc x86_64)
Copyright (c) The PHP Group
Zend Engine v4.2.15, Copyright (c) Zend Technologies
    with Zend OPcache v8.2.15, Copyright (c), by Zend Technologies
    with Xdebug v3.2.2, Copyright (c) 2002-2023, by Derick Rethans
openjdk version "17.0.11" 2024-04-16 LTS
OpenJDK Runtime Environment Corretto-17.0.11.9.1 (build 17.0.11+9-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.11.9.1 (build 17.0.11+9-LTS, mixed mode, sharing)

ここから、Cloud9環境でコンテナイメージを作成・起動していきます。Cloud9のホーム直下にhandsonというディレクトリを作成して、その下にDockerfileGemfileを作成します。こちらはハンズオン画面のものと同じですが、参考までにこちらにも掲載しておきます。

Dockerfile

# ruby:3.2.1 というベースイメージを取得する
FROM public.ecr.aws/docker/library/ruby:3.2.1

# 必要なパッケージ群を取得する
RUN apt-get update -qq && \
    apt-get install -y nodejs postgresql-client npm && \
    rm -rf /var/lib/apt/lists/\*

# ローカルにあるファイルをコンテナイメージ内にコピーする
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile

# Rails アプリケーションを作成する
RUN bundle install && \
    rails new . -O && \
    sed -i -e "52a\  config.hosts.clear\n  config.web_console.allowed_ips = '0.0.0.0/0'\n  config.action_dispatch.default_headers.delete('X-Frame-Options')" config/environments/development.rb

# Rails を 3000 番ポートで起動する
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0", "-p", "3000"]

Gemfileの内容は以下のようになります。

Gemfile

source 'https://rubygems.org'
gem 'rails', '7.0.4'

これらのファイルが準備できましたら、dockerコマンドを用いてコンテナイメージをビルド・起動していきます。

$ docker build -t rails-app .
[+] Building 148.6s (10/10) FINISHED                                                                            docker:default
 => [internal] load build definition from Dockerfile                                                                      0.0s
 => => transferring dockerfile: 897B                                                                                      0.0s
 => [internal] load metadata for public.ecr.aws/docker/library/ruby:3.2.1                                                 2.1s
 => [internal] load .dockerignore                                                                                         0.0s
 => => transferring context: 2B                                                                                           0.0s
 => [1/5] FROM public.ecr.aws/docker/library/ruby:3.2.1@sha256:b38264d66820f3696906ed5fa51b1317d83215515f4a45500c9bf403  31.2s
 => => resolve public.ecr.aws/docker/library/ruby:3.2.1@sha256:b38264d66820f3696906ed5fa51b1317d83215515f4a45500c9bf403b  0.0s
 => => sha256:b38264d66820f3696906ed5fa51b1317d83215515f4a45500c9bf403bf2c9b47 1.86kB / 1.86kB                            0.0s
 => => sha256:3440a912810a14b912ef9738d7b50bb95673c60cd2af71b16b8ace9730723a76 8.42kB / 8.42kB                            0.0s
 ...
 => [internal] load build context                                                                                         0.0s
 => => transferring context: 144B                                                                                         0.0s
 => [2/5] RUN apt-get update -qq &&     apt-get install -y nodejs postgresql-client npm &&     rm -rf /var/lib/apt/list  47.2s
 => [3/5] WORKDIR /myapp                                                                                                  0.0s
 => [4/5] COPY Gemfile /myapp/Gemfile                                                                                     0.1s
 => [5/5] RUN bundle install &&     rails new . -O &&     sed -i -e "52a\  config.hosts.clear\n  config.web_console.all  56.7s
 => exporting to image                                                                                                   11.2s
 => => exporting layers                                                                                                  11.2s
 => => writing image sha256:e246790d98184975ebe2bfb3c4aac84ae1d6175ad26551244a6ba2207ee35888                              0.0s
 => => naming to docker.io/library/rails-app                 

$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
rails-app    latest    e246790d9818   51 seconds ago   1.37GB

$ docker run -d -p 8080:3000 rails-app:latest

こうすることで、Railsアプリケーションを起動することができます。なお、Cloud9上で起動したRailsアプリケーションは、画面上部のPreviewからPreview Running Applicationを選択することで確認できます。

ネットワークリソースの準備

これで、Cloud9でコンテナイメージを作成・起動する手順を把握できましたので、ここからはAWS上にECS環境を準備していきます。

まずは、ECSコンテナを配置するためのVPCやサブネットを、ハンズオンの手順通りに作成していきます。

VPC作成画面で「VPCなど」を選択し、名前タグの自動生成をオンにしecs-testを入力します。

アベイラビリティゾーンの数は2つに設定し、パブリックサブネットは2つ、プライベートサブネットは0に設定していきます。

VPC・サブネットの設定としては、以上で十分です。これらが設定できたらVPC・サブネットを作成していきましょう。

この作成したパブリックサブネットに対し、追加でパブリックIPに関する設定を追加していきます。サブネット一覧画面で、さきほど作成したパブリックサブネットをひとつ選択します。そして、「アクション」トグルから「サブネットの設定を編集」を押下します。

こちらの「パブリックIPv4アドレスの自動割り当てを有効化」にチェックを入れます。この設定により、パブリックサブネットに配置されたECSコンテナに自動でパブリックIPが割り当てられます。

ECSクラスターの準備

ここからはいよいよECSの設定・作成に移っていきます。ECSのコンソール画面に移動し、「クラスターの作成」ボタンを押下します。

最初にクラスター名を設定します。今回はハンズオンとは別でecs-test-clusterという名前を設定しておきます。

この後ハンズオンではネットワークの設定に入るのですが、2024年6月の執筆現在ではFargateのネットワーク設定はこの画面では行いません。ECSのネットワーク設定は可能です。ハンズオンと画面が大きく異なるポイントですので、ここは注意してください。

今回はハンズオンの方針通り、一旦AWS Fargate(以下、Fargate)のほかにAmazon EC2(以下、EC2)でもコンテナをホストします。オートスケーリンググループは新規で作成するため、「新しいASGの作成」を選択します。またプロビジョニングモデルに関しては「オンデマンド」を選択します。

そのほか、AMIはAmazon Linux 2、インスタンスタイプはt3.medium、クラスター起動数は最小最大ともに1で設定しておきます。 なお、今回のインスタンスプロファイルとしてはecsInsntanceRoleというロールがアタッチされていますが、「新しいロールを作成」を選択した場合は、こちらのecsInsntanceRoleが作成されてECSインスタンスにアタッチされる形になります。

ちなみにecsInstanceRoleにはAmazonEC2ContainerServiceforEC2Roleというマネージドの許可ポリシーがアタッチされています、こちらのポリシーはJSONで表示すると以下のようになります。それぞれCloudWatch Logsへの書き込み、EC2インスタンス名一覧の取得、ECRの読み取り、ECSへの書き込み・タグ付けが許可されています。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeTags",
                "ecs:CreateCluster",
                "ecs:DeregisterContainerInstance",
                "ecs:DiscoverPollEndpoint",
                "ecs:Poll",
                "ecs:RegisterContainerInstance",
                "ecs:StartTelemetrySession",
                "ecs:UpdateContainerInstancesState",
                "ecs:Submit*",
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": "ecs:TagResource",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "ecs:CreateAction": [
                        "CreateCluster",
                        "RegisterContainerInstance"
                    ]
                }
            }
        }
    ]
}

上述の通り、ECSクラスター作成画面ではEC2インスタンスのネットワーク設定はできます。先ほど作成したVPC・サブネットを選択して、セキュリティグループとしてもデフォルトのものを選択しておきます。

以上で、ECSクラスターの設定は完了です。クラスターを作成すると、数分後にこちらのようなクラスター詳細を確認することができます。

ECR・コンテナイメージの準備

それでは、次にCloud9で作成したコンテナイメージを格納するECRリポジトリを作成します。ECRコンソール画面に移動し、「プライベートリポジトリ」画面から「リポジトリを作成」を押下します。

今回はハンズオンに合わせて、rails-appという名前でリポジトリを作成します。

リポジトリが正常に作成されると、こちらのようにrails-appリポジトリが表示されますので、ここのリポジトリ名を押下します。

リポジトリ詳細画面では、プッシュしたコンテナイメージが表示されるのですが、肝心のコンテナイメージはまだデプロイされていません。ですので、それ用のコマンドを取得したいとおもいます。ECRでは、リポジトリ詳細画面からプッシュコマンドを表示できるので、ここを押下してプッシュコマンドを表示します。

各コマンドをCloud9条のコンソールで実行することになるので、メモ帳などにコマンドをコピペしておきましょう。

ここから、Cloud9コンソールに戻ります。ここで、先ほどのコマンドを実行します。先ほどのRail動作確認でイメージのビルドは済んでいますので 、コンテナイメージビルドのコマンドは飛ばして実行したいと思います。

$ cd ~/environment/handson/

$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 245999598778.dkr.ecr.ap-northeast-1.amazonaws.com
WARNING! Your password will be stored unencrypted in /home/ec2-user/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

$ docker tag rails-app:latest <accound_id>.dkr.ecr.ap-northeast-1.amazonaws.com/rails-app:latest

$ docker image ls
REPOSITORY                                                    TAG       IMAGE ID       CREATED         SIZE
<account_id>.dkr.ecr.ap-northeast-1.amazonaws.com/rails-app   latest    df6561c12cae   5 minutes ago   1.37GB
rails-app                                                     latest    df6561c12cae   5 minutes ago   1.37GB

$ docker push <accound_id>.dkr.ecr.ap-northeast-1.amazonaws.com/rails-app:latest
The push refers to repository [<accound_id>.dkr.ecr.ap-northeast-1.amazonaws.com/rails-app]
9c7d49bc6884: Pushed 
63f799c0c615: Pushed 
1f8b160a522e: Pushed 
14b679ac81a0: Pushed 
2ee809ddc8d1: Pushed 
2da1c0f2e13d: Pushed 
6ec70191b5d9: Pushed 
d4514f8b2aac: Pushed 
5ab567b9150b: Pushed 
a90e3914fb92: Pushed 
053a1f71007e: Pushed 
ec09eb83ea03: Pushed 
latest: digest: sha256:6b26fa6708e67e92790e341a7458461c358b05581cf3626cf6dc4e02823593aa size: 2841

コンテナイメージを正常にプッシュできると、rails-appリポジトリにてコンテナイメージを確認できるようになります。

EC2へのデプロイ

次に、ECSタスクの作成を行なっていきます。ECSのタスク定義コンソールを開いて、「新しいタスク定義の作成」を押下します。

タスク定義名はaws-test-taskとします。インフラストラクチャの要件ではFargateとEC2インスタンスの両方を用いるので、起動タイプはともにチェックを入れます。タスクサイズはCPU:.25 vCPUメモリ:.5 GBを選択します。タスクロールはなしで、タスク実行ロールは新しいロールの作成(または ecsTaskExecutionRole)を選択します。

コンテナの設定項目では、名前はrails-app、イメージURIはECRに作成したプライベートリポジトリのURIを、必須コンテナは「はい」を選択しておきます。また、コンテナポートは3000で設定しておきます。

ログ収集に関しては、権限周りの設定が必要になるためハンズオン用に無効化しておきます。

以上が設定できたら、タスク定義を作成しておきましょう。

またコンテナ用に、いったんセキュリティグループを作成しておきます。名前は今回はecs-test-alb-sgで設定し、VPCは先ほど作成したECS用のVPCで設定します。また、インターネットからのHTTPインバウンド通信を許可しておきます。

これらが準備できたら、タスク定義からECSサービスを作成していきたいと思います。

ECSのタスク定義コンソールから先ほどのタスク定義を選択し、「デプロイ」トグルから「サービスの作成」を押下します。

ECSクラスターでは、さきほど作成したecs-test-clusterを選択し、コンピューティング設定では「キャパシティープロバイダー設定」を選択しておきます。また、デフォルトのクラスターを使用するよう選択しておきます。

デプロイ設定では、リビジョンは最新のものを(今回はECRにコンテナイメージを1回プッシュしただけなので、1が最新)、サービス名はecs-test-ec2-role、必要なタスクは2で入力しておきます。

ネットワーク設定では、ECSように作成したVPC、サブネットを選びます。なお、セキュリティグループではdefaultとecs-test-ec2-sgの2つを選んでおきましょう。

ロードバランシング項目では、コンテナにアクセスを分配するALBの設定ができます。基本的にはハンズオンの時と設定項目は変わらないですが、コンソール画面の入力欄の順番や配置が微妙に異なっていますので注意してください。

以上が設定できたら、ECSサービス・タスクをデプロイしましょう。正常にデプロイできると、こちらのようにステータスが「アクティブ」なECSサービスが確認できます。

今回のecs-test-ec2-serviceでは、EC2インスタンス上で2つのRailsコンテナが稼働しています。こちらはタスクとして確認できます。

試しにALBのドメインをブラウザに入力してみると、ECSコンテナとして起動したRailsアプリケーションにアクセスすることができます。

Fargateへのデプロイ

先ほどはEC2上にRailコンテナをデプロイしました。これを、今度はFargate上にデプロイしていきたいと思います。サービス詳細画面の「サービス」タブから「作成」ボタンを押下します。

コンピューティング項目のキャパシティープロバイダ戦略はそのままにして、キャパシティープロバイダー自体をFargateにします。

アプリケーションサービスとしては「サービス」、タスク定義とリビジョンはaws-test-tesk、最近リビジョンを選択します。サービス名はecs-test-fargate-serviceで設定し、タスク数は2で進めます。

ネットワークの部分では、先ほどと同じくECS用のVPC・サブネット、セキュリティグループを設定しておきます。

ロードバランシングは、Fargate用に新しく作成します。設定自体は先ほどと同様で問題ありません。

リスナー・ターゲットグループもEC2番と同様に作成します。なおターゲットグループはecs-test-fargate-tgという名前で設定しておきます。

以上で、Fargate版のECSサービスは設定完了です。この設定でサービスを作成すると、EC2ホスティングとは別にFargateホスティングを行うサービスがデプロイされます。

Fargate用ALBのドメインをブラウザに入力すると、このようにFargateにデプロイしたRailsアプリケーションへアクセスすることができます。

ECSは手を動かして学ぶと効果的

個人的な感想ですが、ECSは他のAWSサービスと比べても設定項目数が多いため、手を動かして学ぶのがより効果的なサービスであると思っています。

ハンズオン作成当時と比べてECSコンソールも微妙に異なっていますし、ここも含めて試行錯誤することで、ECSの概念や構築方法が理解できると思います。では!