ECSとFargateで本格的なWebアプリをデプロイしよう

このハンズオンでは、Docker Composeで3層アーキテクチャを構築しようで構築したノートアプリケーションを、AWSのECS/Fargateにデプロイする方法について実際にハンズオン形式で手を動かしながら体験します。

  • ECRへのコンテナイメージのプッシュ
  • ALBとターゲットグループの作成
  • ECSクラスター、タスク定義、サービスの作成
  • オートスケーリングの設定と動作確認
  • CloudWatch Logsによるログの確認

1. 事前準備

Gitの基礎知識を習得していることを前提とします。自信のない方は先に以下の講座を実施してください。

このハンズオンでは、以下のツールやアカウントが必要です。まだ準備できていない場合は、リンク先の手順に沿って準備をお願いします。

2. ハンズオンの概要

このハンズオンでは、前の講座で使用したノートアプリケーション(Nginx + FastAPI + MySQL)を、AWSのECS/Fargateにデプロイします。

2.1 APIとデータベース

以下は、API層とデータベース層の構成図です。

ユーザからのリクエストはインターネットゲートウェイ(note-app-igw)を通じてVPC内に入り、パブリックサブネットに配置されたALBnote-app-api-alb)が受け付けます。ALBは、プライベートサブネットに配置されたFargateタスク(APIサーバ)にリクエストを振り分けます。Fargateタスクは2つのアベイラビリティゾーンに分散配置されているため、片方のAZに障害が発生してもサービスを継続できます。

APIサーバは同じプライベートサブネット内のRDS(MySQL)に接続してデータの読み書きを行います。RDSもプライベートサブネットに配置されているため、インターネットから直接アクセスされることはありません。

プライベートサブネット内のFargateタスクは、NATゲートウェイnote-app-natgw)を経由してECRリポジトリ(note-app-api)からDockerイメージを取得します。

2.2 フロントエンド

以下は、フロントエンド層の構成図です。

フロントエンドもAPI層と同じ構成です。パブリックサブネットに配置されたALBnote-app-front-alb)がユーザからのリクエストを受け付け、プライベートサブネットに配置されたFargateタスク(Webサーバ / Nginx)に振り分けます。

フロントエンドのFargateタスクは、ブラウザに表示するHTML・CSS・JavaScriptを配信します。ブラウザ上のJavaScriptからAPI用ALBのDNS名を通じてAPIサーバにリクエストを送ることで、ノートの作成・取得・更新・削除といった操作を実現します。

2.3 ECS関連

以下は、ECSの論理的な構成を示した図です。

ECSクラスターnote-app-cluster)の中に、フロントエンド用とAPI用の2つのECSサービスを作成します。各ECSサービスは対応するタスク定義に基づいてFargateタスクを起動し、タスク定義はそれぞれのECRリポジトリに格納されたDockerイメージを参照します。

ECSサービスが指定した数のタスクを常時維持するため、タスクが異常終了した場合でも自動的に新しいタスクが起動され、サービスが継続されます。

3. 環境構築

3.1 サンプルリポジトリのクローン

このハンズオンで使用するノートアプリケーションのソースコードは、GitHubのサンプルリポジトリに用意しています。コマンドプロンプト(Windows)またはターミナル(Mac)を開き、任意の場所で以下のコマンドを実行します。

git clone https://github.com/gevanni-academy/sample-note-api.git

クローンが完了したら、Visual Studio Codeを起動し、「ファイル」→「フォルダーを開く」から、クローンしたsample-note-apiフォルダを開いてください。以降の操作は、Visual Studio Codeのターミナルから行います。

3.2 CloudFormationによる環境構築

このハンズオンでは、ECS/Fargateの構築に集中するため、前提となるネットワークやデータベースのリソースはCloudFormationで一括作成します。

📝 CloudFormationとは
CloudFormationは、AWSのリソースをテンプレート(YAML/JSON)で定義し、自動的に作成・管理できるサービスです。手動でリソースを一つずつ作成する代わりに、テンプレートをアップロードするだけで必要なリソースをまとめて構築できます。
IaC(Infrastructure as Code)ツールとしてはTerraformも広く使われていますが、Terraformは事前にインストールが必要です。一方、CloudFormationはAWSマネジメントコンソールからそのまま実行できるため、今回のハンズオンではCloudFormationを使用します。
なお、このハンズオンではCloudFormationテンプレートの内容を理解する必要はありません。テンプレートをそのままアップロードして環境を構築してください。

CloudFormationで作成されるリソースは以下のとおりです。

リソース 名前 説明
VPC note-app-vpc 10.0.0.0/16のアドレス範囲を持つ仮想ネットワーク
パブリックサブネット note-app-public-subnet-1, 2 ALBを配置(2AZ構成)
プライベートサブネット note-app-private-subnet-1, 2 Fargateタスク・RDSを配置(2AZ構成)
インターネットゲートウェイ note-app-igw パブリックサブネットからのインターネットアクセス用
NATゲートウェイ note-app-natgw プライベートサブネットからECRへのイメージ取得用
ルートテーブル note-app-public-rtb パブリックサブネット用のルーティング
ルートテーブル note-app-private-rtb プライベートサブネット用のルーティング(NATゲートウェイ経由)
セキュリティグループ note-app-alb-sg ALB用(HTTP:80を許可)
セキュリティグループ note-app-api-sg APIタスク用(ALBからの8000を許可)
セキュリティグループ note-app-front-sg フロントタスク用(ALBからの80を許可)
セキュリティグループ note-app-rds-sg RDS用(APIタスクからの3306を許可)
DBサブネットグループ note-app-db-subnet-group RDS用のサブネットグループ
RDSインスタンス note-app-db MySQLデータベース(notedb)
💡 ポイント
Fargateタスクをプライベートサブネットに配置するのは、インターネットからタスクに直接アクセスさせないためのセキュリティ上のベストプラクティスです。ユーザからのアクセスはALB(パブリックサブネット)が受け付け、ALBからプライベートサブネット内のタスクに転送します。
ただし、プライベートサブネット内のタスクはそのままではインターネットに接続できないため、ECRからのDockerイメージの取得が失敗します。これを解決するためにNATゲートウェイを配置し、プライベートサブネットからインターネットへのアウトバウンド通信(外向きの通信)を可能にしています。
このハンズオンではコストを抑えるためゾーナルNATゲートウェイを1つのAZにのみ配置しています。業務ではリージョン全体で共有できるリージョナルNATゲートウェイを使うと構成がシンプルになりますが、リージョン内のAZ数に応じた時間料金がかかるため、コスト要件に応じて使い分けてください。

AWSマネジメントコンソールの検索ボックスで「CloudFormation」と入力し、CloudFormationのダッシュボードを開きます。

「スタックの作成」>「新しいリソースを使用(標準)」をクリックします。

スタックの作成の設定を行っていきます。下記表に従い、設定を行ってください。設定後、次へをクリックしてください。

設定項目 設定の基準
テンプレートの準備 既存のテンプレートを選択 事前に作成したテンプレートファイルを使用するため
テンプレートソース テンプレートファイルのアップロード ローカルのテンプレートファイルを直接アップロードするため
テンプレートファイルのアップロード 以下のYAMLテンプレートを保存したファイル ハンズオン環境を構築するテンプレートを指定するため

アップロードするのは下記ファイルです。こちらをtemplate.ymlのような任意の名前でローカル環境に保存し、それをアップロードしてください。

AWSTemplateFormatVersion: '2010-09-09'
Description: ECS/Fargate Hands-on Environment (VPC, Security Groups, RDS)

Parameters:
  DBMasterPassword:
    Type: String
    NoEcho: true
    Description: Password for RDS master user (admin)
    MinLength: 8

Resources:
  # ==================== VPC ====================
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: note-app-vpc

  # ==================== Subnets ====================
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: note-app-public-subnet-1

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.2.0/24
      AvailabilityZone: ap-northeast-1c
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: note-app-public-subnet-2

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.3.0/24
      AvailabilityZone: ap-northeast-1a
      Tags:
        - Key: Name
          Value: note-app-private-subnet-1

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.4.0/24
      AvailabilityZone: ap-northeast-1c
      Tags:
        - Key: Name
          Value: note-app-private-subnet-2

  # ==================== Internet Gateway ====================
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: note-app-igw

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  # ==================== Route Table ====================
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: note-app-public-rtb

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: VPCGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable

  # ==================== NAT Gateway ====================
  NatGatewayEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: note-app-natgw-eip

  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGatewayEIP.AllocationId
      SubnetId: !Ref PublicSubnet1
      Tags:
        - Key: Name
          Value: note-app-natgw

  # ==================== Private Route Table ====================
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: note-app-private-rtb

  PrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway

  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRouteTable

  # ==================== Security Groups ====================
  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for ALB
      GroupName: note-app-alb-sg
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: note-app-alb-sg

  APISecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for API tasks
      GroupName: note-app-api-sg
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: note-app-api-sg

  APIFromALBIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref APISecurityGroup
      IpProtocol: tcp
      FromPort: 8000
      ToPort: 8000
      SourceSecurityGroupId: !Ref ALBSecurityGroup

  FrontSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for Front tasks
      GroupName: note-app-front-sg
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: note-app-front-sg

  FrontFromALBIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref FrontSecurityGroup
      IpProtocol: tcp
      FromPort: 80
      ToPort: 80
      SourceSecurityGroupId: !Ref ALBSecurityGroup

  RDSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for RDS
      GroupName: note-app-rds-sg
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: note-app-rds-sg

  RDSFromAPIIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref RDSSecurityGroup
      IpProtocol: tcp
      FromPort: 3306
      ToPort: 3306
      SourceSecurityGroupId: !Ref APISecurityGroup

  # ==================== DB Subnet Group ====================
  DBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupName: note-app-db-subnet-group
      DBSubnetGroupDescription: Subnet group for note app RDS
      SubnetIds:
        - !Ref PrivateSubnet1
        - !Ref PrivateSubnet2

  # ==================== RDS ====================
  RDSInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      DBInstanceIdentifier: note-app-db
      Engine: mysql
      EngineVersion: '8.0'
      DBInstanceClass: db.t3.micro
      AllocatedStorage: 20
      StorageType: gp2
      MasterUsername: admin
      MasterUserPassword: !Ref DBMasterPassword
      DBName: notedb
      DBSubnetGroupName: !Ref DBSubnetGroup
      VPCSecurityGroups:
        - !Ref RDSSecurityGroup
      PubliclyAccessible: false
      AvailabilityZone: ap-northeast-1a

Outputs:
  VPCId:
    Description: VPC ID
    Value: !Ref VPC
  PublicSubnet1Id:
    Description: Public Subnet 1 ID
    Value: !Ref PublicSubnet1
  PublicSubnet2Id:
    Description: Public Subnet 2 ID
    Value: !Ref PublicSubnet2
  PrivateSubnet1Id:
    Description: Private Subnet 1 ID
    Value: !Ref PrivateSubnet1
  PrivateSubnet2Id:
    Description: Private Subnet 2 ID
    Value: !Ref PrivateSubnet2
  RDSEndpoint:
    Description: RDS Endpoint Address
    Value: !GetAtt RDSInstance.Endpoint.Address
  ALBSecurityGroupId:
    Description: ALB Security Group ID
    Value: !Ref ALBSecurityGroup
  APISecurityGroupId:
    Description: API Security Group ID
    Value: !Ref APISecurityGroup
  FrontSecurityGroupId:
    Description: Front Security Group ID
    Value: !Ref FrontSecurityGroup

「次へ」をクリックし、スタックの詳細を設定します。

設定項目 設定の基準
スタック名 ecs-handson ハンズオン用のスタックであることを識別するため
DBMasterPassword 任意のパスワード(8文字以上) RDSへの接続パスワード。後ほど使用するため控えておくこと

「次へ」をクリックします。

スタックオプションはデフォルトのまま「次へ」をクリックします。

確認画面で内容を確認し、「送信」をクリックします。

CloudFormationのスタックが作成されます。ステータスがCREATE_IN_PROGRESSとなり、作成中であることがわかります。スタックの作成には10〜15分ほどかかります。

ステータスがCREATE_COMPLETEになれば、リソースの作成が完了です。

3.3 CloudFormation出力値の確認

スタックの詳細画面で「出力」タブをクリックしてください。この後のハンズオンで使用する値が表示されます。以下の表を参考に、各値を控えておいてください。

キー 意味 ハンズオンでの用途
VPCId 作成されたVPCのID ECSサービス・ALBの作成時にVPCを指定するため
PublicSubnet1Id パブリックサブネット1のID ALBの配置先として指定するため
PublicSubnet2Id パブリックサブネット2のID ALBの配置先として指定するため
PrivateSubnet1Id プライベートサブネット1のID ECSサービスの作成時にタスクの配置先として指定するため
PrivateSubnet2Id プライベートサブネット2のID ECSサービスの作成時にタスクの配置先として指定するため
RDSEndpoint RDSの接続先ホスト名 APIコンテナの環境変数(DB_HOST)に設定するため
ALBSecurityGroupId ALB用セキュリティグループのID ALBの作成時にセキュリティグループを指定するため
APISecurityGroupId APIタスク用セキュリティグループのID APIのECSサービス作成時にセキュリティグループを指定するため
FrontSecurityGroupId フロントタスク用セキュリティグループのID フロントのECSサービス作成時にセキュリティグループを指定するため

また、スタック作成時に指定した DBMasterPassword(RDSのパスワード)も、後でAPIコンテナの環境変数に設定するため控えておいてください。

4. アプリケーション層の構築

FastAPIのAPIサーバをECS/Fargateにデプロイします。

4.1 イメージのビルド

ECS/Fargateでアプリケーションをデプロイするには、まずDockerイメージをビルドする必要があります。Docker Compose環境ではローカルでイメージをビルドしてそのまま使用しましたが、ECS環境ではビルドしたイメージをECRにプッシュし、Fargateがそこからイメージを取得して実行します。

Visual Studio Codeのターミナルからapiフォルダに移動し、Dockerイメージをビルドします。

cd api
docker build --platform linux/amd64 -t note-app-api:v1 .

--platform linux/amd64を指定することで、Fargateで動作するx86_64アーキテクチャのイメージをビルドします。

💡 ポイント
サンプルリポジトリのmain.pyには、起動時にデータベースのテーブルを自動作成するlifespan関数が含まれています。Docker Compose環境ではinit.sqlでテーブルを作成していましたが、RDSではinit.sqlを自動実行できないため、この仕組みによりAPIコンテナ起動時にテーブルが自動作成されます。RDSの起動直後は接続に失敗することがあるため、最大30回まで2秒間隔でリトライする処理も入っています。

4.2 ECRリポジトリの作成

ビルドしたDockerイメージをAWS上で利用するには、イメージを保管する場所が必要です。ECR(Elastic Container Registry)はAWSが提供するDockerイメージのレジストリサービスで、Docker Hubのようにイメージをプッシュ・プルできます。

AWSマネジメントコンソールでECRを開き、サイドメニュー空「プライベートリポジトリ」の部分にある「リポジトリ」をクリックしたうえで、右上の「リポジトリを作成」をクリックします。

下記の内容を設定してください。

設定項目 設定の基準
リポジトリ名 note-app-api APIコンテナを識別するため
イメージタグのミュータビリティ Mutable デフォルトのまま。イメージタグの上書きを許可するため
暗号化設定 AES-256 デフォルトのまま。標準的な暗号化で十分なため

入力が完了したら「作成」をクリックします。リポジトリ一覧に note-app-api が表示されれば、リポジトリの作成は成功です。

4.3 ECRへのプッシュ

ECRリポジトリが作成できたら、ローカルでビルドしたDockerイメージをECRにプッシュします。ECRにプッシュすることで、ECS/Fargateがこのイメージを取得してコンテナを起動できるようになります。

ECRにログイン

aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com

ECRの認証トークンを取得し、Dockerクライアントでログインします。「Login Succeeded」と表示されれば成功です。

タグの付与

docker tag note-app-api:v1 <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/note-app-api:v1

ローカルのイメージに、ECRリポジトリのURIをタグとして付与します。Dockerはタグに含まれるURIを見てプッシュ先を判断するため、この手順が必要です。

ECRリポジトリにプッシュ

docker push <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/note-app-api:v1

タグ付けしたイメージをECRリポジトリにプッシュします。プッシュが完了すると、ECRコンソール上でイメージを確認できるようになります。

⚠️ 「AccessDeniedException」エラーが出る場合
IAMユーザにECRへの権限がありません。IAMコンソールでAmazonEC2ContainerRegistryFullAccessポリシーをアタッチしてください。

4.4 ALBとターゲットグループの作成

ECSサービスを作成する前に、トラフィックを振り分けるためのALBとターゲットグループを作成します。今回、Fargateタスクはプライベートサブネットに配置しますが、ALBはインターネットからのアクセスを受け付けるためパブリックサブネットに配置する必要があります。ALBとECSサービスで配置するサブネットが異なるため、ALBはEC2コンソールで事前に作成します。

ターゲットグループの作成

EC2コンソールの「ターゲットグループ」から「ターゲットグループの作成」をクリックします。

下記の内容を設定してください。

設定項目 設定の基準
ターゲットタイプ IPアドレス Fargateではインスタンスではなく、タスクごとに割り振られるIPアドレスでルーティングするため
ターゲットグループ名 note-app-api-tg ターゲットグループを識別するため
プロトコル HTTP HTTP通信を使用するため
ポート 8000 FastAPIがリッスンするポート
VPC CloudFormation出力値のVPCIdに対応するVPC CloudFormationで作成したVPCを選択
ヘルスチェックパス /health FastAPIのヘルスチェックエンドポイントを使用するため

「次へ」をクリックし、ターゲットの登録はスキップして「次へ」をクリックします。

確認と作成の画面が表示されるので、特に問題がなければ「ターゲットグループの作成」をクリックします。

無事にターゲットグループが作成されれば、ここまでの操作は完了です。

💡 ポイント
ターゲットの登録はスキップして構いません。ECSサービスを作成する際にALBとターゲットグループを紐づけると、ECSがタスクの起動・停止に合わせてターゲットの登録・解除を自動的に行います。そのため、ここで手動でターゲットを登録する必要はありません。

ALBの作成

EC2コンソールの「ロードバランサー」から「ロードバランサーの作成」を選択してください。

「Application Load Balancer」の「作成」をクリックします。

下記の内容を設定してください。

設定項目 設定の基準
ロードバランサー名 note-app-api-alb ALBを識別するため
スキーム インターネット向け インターネットからアクセスを受け付けるため
IPアドレスタイプ IPv4 標準的なIPv4を使用するため
VPC CloudFormation出力値のVPCIdに対応するVPC CloudFormationで作成したVPCを選択
マッピング CloudFormation出力値のPublicSubnet1IdPublicSubnet2Idに対応するサブネット パブリックサブネットを選択(プライベートサブネットを選ばないよう注意)
セキュリティグループ CloudFormation出力値のALBSecurityGroupIdに対応するSG CloudFormationで作成したALB用セキュリティグループを選択

リスナーの設定

設定項目 設定の基準
プロトコル HTTP HTTPでリクエストを受け付けるため
ポート 80 標準的なHTTPポート
デフォルトアクション note-app-api-tgに転送 先ほど作成したターゲットグループを選択

「ロードバランサーの作成」をクリックします。

ロードバランサー一覧に note-app-api-alb が表示されれば成功です。

4.5 ECSクラスターの作成

ECSクラスターは、コンテナを実行するための論理的なグループです。サービスやタスクはクラスターの中に作成するため、まずクラスターを用意する必要があります。

ECSコンソールを開き、サイドメニューから「クラスター」を選択し、右上の「クラスターの作成」をクリックします。

下記の内容を設定してください。

設定項目 設定の基準
クラスター名 note-app-cluster クラスターを識別するため
インフラストラクチャ Fargateのみ サーバレスで運用するため

無事にnote-app-clusterが作成されればここまでの操作は完了です。なお、作成までには数分かかることがあります。

4.6 タスク定義の作成

タスク定義は、コンテナの設計図にあたるものです。使用するDockerイメージ、割り当てるCPU・メモリ、環境変数、ポート設定などを定義します。Docker Composeの docker-compose.yml に相当する役割です。

「タスク定義」→「新しいタスク定義の作成」をクリックします。

下記の内容を設定してください。

設定項目 設定の基準
タスク定義ファミリー note-app-api タスク定義を識別するため
起動タイプ AWS Fargate サーバレスで実行するため
オペレーティングシステム Linux/X86_64 一般的なLinux環境を使用するため
タスクサイズ - CPU 1 vCPU 最小限のリソースで十分なため
タスクサイズ - メモリ 3 GB 最小限のリソースで十分なため
タスク実行ロール 新しいロールの作成 ECRからイメージを取得するために必要

💡 ポイント
タスク実行ロールは、ECSがコンテナを起動する際に使用するIAMロールです。「新しいロールの作成」を選択すると、ecsTaskExecutionRole という名前のロールが自動的に作成され、ECRからのイメージ取得やCloudWatch Logsへのログ出力に必要な権限が付与されます。このロールは一度作成されると、次回以降のタスク定義では「既存のロールを使用」から ecsTaskExecutionRole を選択できるようになります。
💡 ポイント
同じ画面にあるタスクロールは未選択のままで構いません。タスクロールは、コンテナ内のアプリケーションからS3やDynamoDBなどのAWSサービスにアクセスする場合に必要となるロールです。今回のアプリケーションはRDSへの接続にIAMロールを使用しないため(環境変数でユーザ名・パスワードを直接指定するため)、タスクロールは不要です。

コンテナの設定

設定項目 設定の基準
名前 api コンテナを識別するため
イメージURI <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/note-app-api:v1 ECRにプッシュしたイメージ
コンテナポート 8000 FastAPIがリッスンするポート

環境変数

Docker Compose環境では、docker-compose.ymlenvironment セクションでデータベースの接続先を指定していました。ECS環境でも同じように、タスク定義の環境変数でAPIコンテナにRDSの接続情報を渡します。アプリケーションのコード内で os.environ などを使ってこれらの値を読み取り、RDSに接続します。

キー 設定の基準
DB_HOST CloudFormation出力値のRDSEndpoint RDSに接続するため
DB_USER admin RDSのマスターユーザ名
DB_PASSWORD スタック作成時に指定したDBMasterPassword RDSのマスターパスワード
DB_NAME notedb 使用するデータベース名

💡 ポイント
実務では、パスワードなどの機密情報は環境変数に直接記載せず、AWS Secrets Managerを使用することを推奨します。このハンズオンでは簡略化のため、環境変数に直接設定しています。

すべての入力が完了したら、「作成」をクリックします。

無事にタスクが作成されれば、ここまでの操作は完了です。

4.7 ECSサービスの作成

続いて、ECSサービスを作成します。ECSサービスは、タスク定義に基づいてコンテナを起動・維持する仕組みです。指定した数のタスクを常時稼働させ、タスクが停止した場合は自動的に再起動します。

ECSコンソールでクラスターを選択し、「サービス」タブから「作成」をクリックします。

サービスの詳細

設定項目 設定の基準
タスク定義ファミリー note-app-api 先ほど作成したタスク定義を選択
タスク定義のリビジョン 空欄のまま 最新リビジョンが自動的に使用されるため
サービス名 note-app-api-service サービスを識別するため

環境

設定項目 設定の基準
既存のクラスター note-app-cluster 先ほど作成したクラスターが選択されていることを確認
コンピューティングオプション キャパシティプロバイダー戦略 デフォルトのまま
キャパシティプロバイダー FARGATE Fargateで実行するため
プラットフォームバージョン LATEST 最新のFargateプラットフォームを使用するため

デプロイ設定

設定項目 設定の基準
スケジューリング戦略 レプリカ デフォルトのまま。クラスター全体で必要な数のタスクを配置するため
必要なタスク 1 まずは1つのタスクで起動する
アベイラビリティゾーンのリバランスを有効にする チェックあり デフォルトのまま。タスクをAZ間で均等に配置するため
ヘルスチェックの猶予期間 0 デフォルトのまま

ネットワーキング

設定項目 設定の基準
VPC note-app-vpc CloudFormationで作成したVPCを選択
サブネット note-app-private-subnet-1, note-app-private-subnet-2 CloudFormation出力値のPrivateSubnet1IdPrivateSubnet2Idに対応する2つのプライベートサブネットを選択
セキュリティグループ note-app-api-sg CloudFormation出力値のAPISecurityGroupIdに対応するSG
パブリックIP オフ プライベートサブネットに配置するためパブリックIPは不要

ロードバランシング

「ロードバランシングを使用」にチェックを入れると、ALBの設定項目が表示されます。下記の内容を設定してください。

設定項目 設定の基準
ロードバランサーの種類 Application Load Balancer HTTP/HTTPSのルーティングを行うため
コンテナ api 8000:8000 タスク定義で設定したコンテナとポートを選択
Application Load Balancer 既存のロードバランサーを使用 先ほど作成したALBを使用するため
ロードバランサー note-app-api-alb 先ほど作成したALBを選択
リスナー 既存のリスナーを使用 先ほど作成したリスナーを使用するため
リスナーのポート 80:HTTP 先ほど作成したリスナーを選択
ターゲットグループ 既存のターゲットグループを使用 先ほど作成したターゲットグループを使用するため
ターゲットグループ名 note-app-api-tg 先ほど作成したターゲットグループを選択

サービスの自動スケーリング - オプション

アクセスが集中してCPU使用率が高くなった際に、タスク数を自動的に増やして負荷を分散させるための設定を行います。ここでは、CPU使用率が70%を超えたらタスクを追加し、負荷が下がったらタスクを減らすルールを設定します。

「サービスの自動スケーリングを使用」にチェックを入れると、スケーリングの設定項目が表示されます。下記の内容を設定してください。

設定項目 設定の基準
タスクの最小数 1 最低1つは常時起動するため
タスクの最大数 3 負荷に応じて最大3つまでスケールするため
スケーリングポリシータイプ ターゲットの追跡 メトリクスの目標値に基づいてタスク数を自動調整するため
ポリシー名 api-cpu-scaling スケーリングポリシーを識別するため
ECSサービスメトリクス ECSServiceAverageCPUUtilization CPU使用率を監視するため
ターゲット値 70 CPU使用率が70%を超えたらスケールアウトするため
スケールアウトクールダウン期間 300 デフォルトのまま。スケールアウト後、次の判定まで300秒待機する
スケールインクールダウン期間 300 デフォルトのまま。スケールイン後、次の判定まで300秒待機する
スケールインをオフにする チェックなし デフォルトのまま。負荷が下がったらタスクを減らすため

すべての設定が完了したら「作成」をクリックします。

サービスが作成中になります。完了まで数分かかるため、しばらく待ちましょう。

⚠️ タスクがPENDINGのままになる場合
(1)セキュリティグループでアウトバウンド通信(すべてのトラフィック)が許可されているか確認してください。
(2)サブネットがプライベートサブネットになっているか確認してください。パブリックサブネットを選択している場合は、プライベートサブネットに変更してください。
(3)タスク定義の環境変数(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME)が正しいか確認してください。

4.8 動作確認

サービスのステータスが「アクティブ」にり、デプロイとタスクが「1/1件の実行中のタスク」となったら、サービスの作成と、タスクの起動が完了です。

ALB経由でアクセスして動作を確認します。

EC2コンソールの「ロードバランサー」から note-app-api-alb を選択し、「DNS名」をコピーします。ブラウザまたはcurlで以下のようにアクセスしてください。

curl http://<ALBのDNS名>/health

以下のレスポンスが返れば、APIは正常に動作しています。

{"status":"healthy"}

ノート一覧も確認します。

curl http://<ALBのDNS名>/notes

空の配列 [] が返れば、RDSとの接続も成功しています。

⚠️ 「502 Bad Gateway」エラーが出る場合
(1)ターゲットグループでターゲットが「healthy」になっているか確認してください。ターゲットが「unhealthy」の場合は、ヘルスチェックパスが /health に設定されているか確認してください。
(2)APIタスク用セキュリティグループで、ALBのセキュリティグループからのポート8000へのインバウンドが許可されているか確認してください。

4.9 CloudWatch Logsでログを確認

ECSで動作するコンテナのログは、ローカル環境のように docker logs コマンドでは確認できません。代わりに、CloudWatch Logsでログを確認します。

ECSコンソールでクラスターを選択し、note-app-api-service をクリックします。「ログ」タブを開くと、コンテナの標準出力がCloudWatch Logsに記録されているのを確認できます。

ログにはFastAPIの起動メッセージや、APIへのリクエストのアクセスログが表示されています。先ほど curl でアクセスした /health/notes へのリクエストが記録されていることを確認してください。

ログを確認すると、自分がアクセスした覚えのない /health へのリクエストが一定間隔で記録されていることに気づくかもしれません。これは、ALBが自動的に行っているヘルスチェックのリクエストです。ALBはターゲットグループに設定されたヘルスチェックパス(今回は /health)に対して定期的にリクエストを送り、タスクが正常に動作しているかを監視しています。タスクから正常なレスポンスが返らない場合、ALBはそのタスクへのトラフィック転送を自動的に停止します。

💡 ポイント
CloudWatch Logsは、ECS上のコンテナのトラブルシューティングに欠かせないツールです。タスクが起動に失敗した場合や、アプリケーションでエラーが発生した場合は、まずCloudWatch Logsを確認してください。実務では、エラーログを検知してSlackやメールに通知する仕組みを構築するのが一般的です。

4.10 オートスケーリングの動作確認

ECSサービスの作成時にオートスケーリングを設定しましたが、ここではタスク数を手動で変更して、スケールアウト(タスクの増加)の動作を実際に確認します。

ECSコンソールでサービスを選択し、「サービスを更新」をクリックします。

「必要なタスク」の値を 1 から 2 に変更して「更新」をクリックしてください。

しばらくすると、「タスク」タブに2つのタスクが表示されます。両方のタスクのステータスが「実行中」になれば、スケールアウトが完了です。

EC2コンソールでターゲットグループ note-app-api-tg を開き、「ターゲット」タブを確認してください。2つのターゲットが「healthy」として登録されていることがわかります。ALBは、これら2つのタスクにリクエストを自動的に分散します。

💡 ポイント
今回はタスク数を手動で2に変更しましたが、ECSサービスの作成時に設定したオートスケーリングにより、CPU使用率が70%を超えた場合は自動的にタスクが追加されます。逆に負荷が下がればタスクは自動的に削減されます。
また、「必要なタスク」を 0 に変更するとすべてのタスクが停止し、ALBにアクセスしても「503 Service Unavailable」が返るようになります。メンテナンス時にタスクを一時的に停止させたい場合などに利用できます。

5. プレゼンテーション層の構築

NginxのフロントエンドをECS/Fargateにデプロイします。手順はアプリケーション層と同様ですので、重複する説明は省略します。

5.1 ソースコードの修正

Docker Compose環境では、フロントエンドとAPIは同じマシン上で動作していたため、script.js のAPI接続先は http://localhost:8000 でした。しかし、ECS環境ではフロントエンドとAPIはそれぞれ別のFargateタスクとして独立して動作しており、localhost ではAPIにアクセスできません。そこで、API接続先をAPI用ALBのDNS名に変更することで、フロントエンドからALB経由でAPIにリクエストを送る構成にします。

Visual Studio Codeのエクスプローラーから front/script.js を開いてください。ファイルの1行目に以下のような記述があります。

const API_URL = 'http://localhost:8000';

この http://localhost:8000 の部分を、先ほど作成したAPI用ALBのDNS名に変更します。ALBのDNS名は、EC2コンソールの「ロードバランサー」から note-app-api-alb を選択して確認できます。

const API_URL = 'http://<API用ALBのDNS名>';

変更したら、ファイルを保存してください。

💡 ポイント
ローカル環境ではhttp://localhost:8000でAPIに接続していましたが、ECS環境ではフロントエンドとAPIが別々のタスクで動作するため、API用ALBのDNS名を指定する必要があります。

5.2 イメージのビルドとプッシュ

API層と同じ手順で、フロントエンドのDockerイメージをビルドし、ECRにプッシュします。

イメージのビルド

Visual Studio Codeのターミナルで front フォルダに移動します。API層のビルド時に api フォルダにいる場合は、以下のコマンドで移動してください。

cd ../front

現在のディレクトリが front フォルダであることを確認します。

Macの場合:

pwd

Windowsの場合:

cd

どちらの場合も、表示されたパスの末尾が front になっていれば正しいディレクトリにいます。以下のコマンドでイメージをビルドします。

docker build --platform linux/amd64 -t note-app-front:v1 .

ECRリポジトリの作成

ECRコンソールでフロント用のリポジトリを作成します。API層と同じ手順で「リポジトリを作成」をクリックし、下記の内容を設定してください。

設定項目 設定の基準
リポジトリ名 note-app-front フロントコンテナを識別するため
イメージタグのミュータビリティ Mutable デフォルトのまま
暗号化設定 AES-256 デフォルトのまま

入力が完了したら「作成」をクリックします。リポジトリ一覧に note-app-front が表示されれば成功です。

タグの付与

docker tag note-app-front:v1 <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/note-app-front:v1

ローカルのイメージに、ECRリポジトリのURIをタグとして付与します。

ECRリポジトリにプッシュ

docker push <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/note-app-front:v1

プッシュが完了したら、ECRコンソールで note-app-front リポジトリにイメージが登録されていることを確認してください。

5.3 ALBとターゲットグループの作成

API層と同様に、フロントエンド用のALBとターゲットグループをEC2コンソールで作成します。

ターゲットグループの作成

EC2コンソールの「ターゲットグループ」から「ターゲットグループの作成」をクリックします。下記の内容を設定してください。

設定項目 設定の基準
ターゲットタイプ IPアドレス Fargateではインスタンスではなく、タスクごとに割り振られるIPアドレスでルーティングするため
ターゲットグループ名 note-app-front-tg ターゲットグループを識別するため
プロトコル HTTP HTTP通信を使用するため
ポート 80 Nginxがリッスンするポート
VPC CloudFormation出力値のVPCIdに対応するVPC CloudFormationで作成したVPCを選択
ヘルスチェックパス / ルートパスで応答確認

「次へ」をクリックし、ターゲットの登録はスキップして「次へ」をクリックします。

「ターゲットグループの作成」をクリックします。

無事にターゲットグループが作成されれば、ここまでの操作は成功です。

ALBの作成

EC2コンソールの「ロードバランサー」から「ロードバランサーの作成」→「Application Load Balancer」の「作成」をクリックします。

下記の内容を設定してください。

設定項目 設定の基準
ロードバランサー名 note-app-front-alb ALBを識別するため
スキーム インターネット向け インターネットからアクセスを受け付けるため
IPアドレスタイプ IPv4 標準的なIPv4を使用するため
VPC CloudFormation出力値のVPCIdに対応するVPC CloudFormationで作成したVPCを選択
マッピング CloudFormation出力値のPublicSubnet1IdPublicSubnet2Idに対応するサブネット パブリックサブネットを選択
セキュリティグループ CloudFormation出力値のALBSecurityGroupIdに対応するSG CloudFormationで作成したALB用セキュリティグループを選択

リスナーの設定

設定項目 設定の基準
プロトコル HTTP HTTPでリクエストを受け付けるため
ポート 80 標準的なHTTPポート
デフォルトアクション note-app-front-tgに転送 先ほど作成したターゲットグループを選択

「ロードバランサーの作成」をクリックします。

ロードバランサー一覧に note-app-front-alb が表示されれば成功です。

💡 ポイント
本来であれば、APIとフロントエンドは1つのALBにまとめ、リスナールールで振り分けるのが一般的です。たとえば、独自ドメインを取得して api.example.com はAPIへ、app.example.com はフロントエンドへ振り分けるホストベースルーティングが代表的な方法です。
ただし、この方法には独自ドメインの取得が必要になります。今回のハンズオンではドメインの取得は行わないため、APIとフロントエンドでそれぞれ別のALBを作成しています。ALBが2つになることでコストは増えますが、ハンズオンの手順をシンプルに保つことを優先しています。

5.4 タスク定義の作成

API層と同様に、フロントエンド用のタスク定義を作成します。「タスク定義」→「新しいタスク定義の作成」をクリックし、下記の内容を設定してください。

設定項目 設定の基準
タスク定義ファミリー note-app-front タスク定義を識別するため
起動タイプ AWS Fargate サーバレスで実行するため
タスクサイズ - CPU 1 vCPU 最小限のリソース
タスクサイズ - メモリ 3 GB 最小限のリソース

コンテナの設定

設定項目 設定の基準
名前 front コンテナを識別するため
イメージURI <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/note-app-front:v1 ECRにプッシュしたイメージ
コンテナポート 80 Nginxがリッスンするポート

すべて入力が終わったら「作成」をクリックします。

無事にタスク定義が作成されれば、ここまでの操作は完了です。

5.5 ECSサービスの作成

API層と同様の手順で、フロントエンド用のECSサービスを作成します。クラスターは既存のnote-app-clusterを使用します。「サービス」タブから「作成」をクリックし、下記の内容を設定してください。

サービスの詳細

設定項目 設定の基準
タスク定義ファミリー note-app-front 作成したタスク定義を選択
タスク定義のリビジョン 空欄のまま 最新リビジョンが自動的に使用されるため
サービス名 note-app-front-service サービスを識別するため

環境

設定項目 設定の基準
既存のクラスター note-app-cluster API層と同じクラスターが選択されていることを確認
コンピューティングオプション キャパシティプロバイダー戦略 デフォルトのまま
キャパシティプロバイダー FARGATE Fargateで実行するため
プラットフォームバージョン LATEST 最新のFargateプラットフォームを使用するため

デプロイ設定

設定項目 設定の基準
スケジューリング戦略 レプリカ デフォルトのまま
必要なタスク 1 最小構成
アベイラビリティゾーンのリバランスを有効にする チェックあり デフォルトのまま
ヘルスチェックの猶予期間 0 デフォルトのまま

ネットワーキング

設定項目 設定の基準
VPC CloudFormation出力値のVPCIdに対応するVPC CloudFormationで作成したVPCを選択
サブネット CloudFormation出力値のPrivateSubnet1IdPrivateSubnet2Idに対応するサブネット 2つのプライベートサブネットを選択
セキュリティグループ CloudFormation出力値のFrontSecurityGroupIdに対応するSG フロントタスク用SG
パブリックIP オフ プライベートサブネットに配置するためパブリックIPは不要

ロードバランシング

「ロードバランシングを使用」にチェックを入れ、下記の内容を設定してください。

設定項目 設定の基準
ロードバランサーの種類 Application Load Balancer HTTP/HTTPSのルーティングを行うため
コンテナ front 80:80 タスク定義で設定したコンテナとポートを選択
Application Load Balancer 既存のロードバランサーを使用 先ほど作成したALBを使用するため
ロードバランサー note-app-front-alb 先ほど作成したALBを選択
リスナー 既存のリスナーを使用 先ほど作成したリスナーを使用するため
リスナーのポート 80:HTTP 先ほど作成したリスナーを選択
ターゲットグループ 既存のターゲットグループを使用 先ほど作成したターゲットグループを使用するため
ターゲットグループ名 note-app-front-tg 先ほど作成したターゲットグループを選択

サービスの自動スケーリング - オプション

API層と同様に設定します。

設定項目 設定の基準
タスクの最小数 1 最低1つは常時起動するため
タスクの最大数 4 負荷に応じて最大4つまでスケールするため
スケーリングポリシータイプ ターゲットの追跡 メトリクスの目標値に基づいてタスク数を自動調整するため
ポリシー名 front-cpu-scaling スケーリングポリシーを識別するため
ECSサービスメトリクス ECSServiceAverageCPUUtilization CPU使用率を監視するため
ターゲット値 70 CPU使用率が70%を超えたらスケールアウトするため

すべての設定が完了したら「作成」をクリックします。

note-app-front-serviceのステータスが「アクティブ」となり、デプロイとタスクが「1/1 件の実行中のタスク」となれば、ここまでは成功です。

5.6 動作確認

フロント用ALBのDNS名を確認し、ブラウザで開きます。

http://<フロント用ALBのDNS名>

無事にノートアプリが起動することを確認します。

APIとの接続もできているかを確認するため、タイトルと内容に任意の値を入力し、「追加」をクリックします。

入力した項目がノート一覧に表示されれば、APIとの連携もできているため、ここまでは成功です。

6. 不要リソースの削除

ハンズオンが終わったら、課金を避けるため、作成したリソースを削除します。以下の順序で削除してください。

6.1 ECSサービスの削除

ECSコンソールでクラスターを選択し、2つのサービス(api、front)を削除します。「強制削除」オプションを使用すると、タスクも同時に停止されます。

6.2 ECSクラスターの削除

サービスが削除されたら、クラスターを削除します。

6.3 タスク定義の登録解除

2つのタスク定義(note-app-api、note-app-front)を登録解除します。

6.4 ALBの削除

EC2コンソールで2つのALB(api、front)を削除します。

6.5 ターゲットグループの削除

2つのターゲットグループを削除します。

6.6 ECRリポジトリの削除

ECRコンソールで2つのリポジトリ(note-app-api、note-app-front)を削除します。

6.7 CloudFormationスタックの削除

CloudFormationコンソールでecs-handsonスタックを選択し、「削除」をクリックします。VPC、セキュリティグループ、RDSなど、CloudFormationで作成したすべてのリソースが自動的に削除されます。

💡 ポイント
スタックの削除には10分程度かかります。ステータスがDELETE_COMPLETEになるまで待ってください。RDSの削除に時間がかかるため、削除中にエラーが表示された場合は数分待ってから再度削除を実行してください。

7. まとめ

このハンズオンでは、ノートアプリケーションをECS/Fargateにデプロイする方法を体験しました。

  • ECRにAPIとフロントエンドのDockerイメージをプッシュした
  • ALBとターゲットグループを作成し、パブリックサブネットに配置してインターネットからのアクセスを受け付けた
  • ECSクラスターを作成し、タスク定義に基づいて2つのサービス(API、フロント)をプライベートサブネットにデプロイした
  • CloudWatch Logsでコンテナのログを確認し、ALBのヘルスチェックログについても学んだ
  • タスク数を手動で変更して、スケールアウトの動作を確認した

8. 次のステップ

これでコンテナ講座は最後の講座まで完了です。学んだ内容を実際の成果物に落とし込みたい方は、章末尾の応用課題に挑戦してみてください。タスク管理APIをコンテナ化し、Docker Compose でローカル起動した上で、ECS/Fargate にデプロイする課題です。

応用課題の課題内容は誰でも閲覧できます。メンターによる成果物レビューや実装の壁打ちはポートフォリオプランでご提供しています。