0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【ECS + GitHub Actions】コンテナ化Spring Bootアプリの本番環境を構築 [前編] - アプリ公開まで

Posted at

はじめに

概要

AWSとGitHub Actionsを使用した本番環境の構築・コンテナ化アプリのデプロイ方法について取り上げます。

image.png

前提条件

  • Java/Spring Boot環境構築済み
  • Docker環境構築済み
  • AWSアカウント作成済み
  • GitHubアカウント作成済み

リポジトリ

動作環境

  • Windows 11 Home(24H2)
  • Java 21
  • Maven 3.9.9
  • Spring Boot 3.4.5
  • Docker 27.3.1
  • Docker Desktop 4.36.0

本手順

前編では、ECRへアプリケーションのイメージをプッシュし、ECSサービスを立ち上げるところまで実施します。

1. サンプルアプリケーションの作成

Spring InitializrからMavenプロジェクトを作成し、以下の依存関係を追加します。

  • Spring Boot DevTools
  • Spring Web
  • Thymeleaf

続いて、以下の通り実装します。

HelloController.java
package com.example.demo;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class HelloController {
    @GetMapping({ "", "/" })
    public ModelAndView index(ModelAndView mav) {
        // 現在時刻を取得
        LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Tokyo"));
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String currentTime = now.format(formatter);

        // マシンのホスト名を取得
        String hostName;
        try {
            hostName = InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
            hostName = "Unknown";
        }

        mav.addObject("currentTime", currentTime);
        mav.addObject("hostName", hostName);
        mav.setViewName("hello");
        return mav;
    }
}
hello.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Hello</title>
  </head>
  <body>
    <h1>Hello Spring Boot on Amazon ECS</h1>
    <p>現在時刻:<span th:text="${currentTime}"></span></p>
    <p>ホスト名:<span th:text="${hostName}"></span></p>
  </body>
</html>

メッセージと現在時刻、動作しているマシンのホスト名を画面に表示する単純な作りです。

試しにアプリケーションを実行します。

./mvnw spring-boot:run

image.png
正常に動いていることが分かります。

2. ネットワークの構築(VPC)

2つのAZにまたがるネットワークを構築します。
パブリックサブネット/プライベートサブネットは2つずつ用意します。

CIDR設計は以下の通りです。

ネットワーク IPv4アドレス CIDRブロック
VPC 10.0.0.0/16 00001010.00000000.00000000.00000000
public subnet 1 (1a) 10.0.0.0/20 00001010.00000000.00000000.00000000
public subnet 2 (1c) 10.0.16.0/20 00001010.00000000.00010000.00000000
private subnet 1 (1a) 10.0.128.0/20 00001010.00000000.10000000.00000000
private subnet 2 (1c) 10.0.144.0/20 00001010.00000000.10010000.00000000

AWSコンソールにログインして、実際に作成していきます。
リージョンは東京を使用します。

image.png
作成するリソースは「VPCなど」を選択することで、VPC・サブネット・ルートテーブル・インターネットゲートウェイなどをまとめて作成します。
名前は「qiita-spring-ecs」とします。
IPv4 CIDRブロックはデフォルトで上記の設計通りになっていると思います。

image.png
image.png
AZの数は2つ、パブリックサブネット/プライベートサブネットの数もそれぞれ2つとします。
VPC同様、各サブネットのCIDRブロックもデフォルトで設計通りになっています。

その他の設定はデフォルトのままで、「VPCを作成」とします。

3. セキュリティグループの作成

1) ロードバランサー用

はじめに、ロードバランサーにアタッチするセキュリティグループを作成します。

image.png
名前は「qiita-spring-ecs-sg-elb」とします。
ロードバランサーはHTTP(ポート:80)による通信を受け付ける必要があるため、以下の通りインバウンドルールを追加します。

  • タイプ:HTTP、ソース:0.0.0.0/0

以上で「セキュリティグループを作成」とします。

2) ECSサービス用

続いて、ECSサービスにアタッチするセキュリティグループを作成します。

image.png
名前は「qiita-spring-ecs-sg-service」とします。
ECSサービスはロードバランサーからHTTP(ポート:8080)による通信を受け付ける必要があるため、以下の通りインバウンドルールを追加します。

  • タイプ:カスタムTCP、ポート範囲:8080、ソース:qiita-spring-ecs-sg-elb

以上で「セキュリティグループを作成」とします。

3) VPCエンドポイント用

最後に、VPCエンドポイントにアタッチするセキュリティグループを作成します。

image.png
名前は「qiita-spring-ecs-sg-vpce」とします。
今回作成するVPCエンドポイントは、ECSサービスからHTTPS(ポート:443)による通信を受け付ける必要があるため、以下の通りインバウンドルールを追加します。

  • タイプ:HTTPS、ソース:qiita-spring-ecs-sg-service

以上で「セキュリティグループを作成」とします。

4. プライベートリポジトリの作成(ECR)

Dockerレジストリ(イメージの保存先)にECRを使用します。

image.png
プライベートリポジトリを作成し、名前は「qiita-spring-ecs-app」とします。
ミュータビリティは「Mutable」、暗号化設定は「AES-256」を選択します。
その他の設定はデフォルトのままで、「作成」とします。

5. Dockerイメージの作成

1) Dockerfileの作成

アプリケーションのルートフォルダに、以下の通りDockerfileを作成します。

Dockerfile
FROM amazoncorretto:21
WORKDIR /app
COPY target/demo-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

ベースイメージは「Amazon Corretto 21」を使用します。

2) イメージのビルド・プッシュ

./mvnw clean package

はじめに、アプリケーションをビルドしておきます。

image.png

ここで、先ほど作成したリポジトリを選択し、「プッシュコマンドを表示」とすると、必要なコマンドが表示されるのでこちらを参考にしていきます。

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

Dockerイメージを何かしらのレジストリにプッシュする時は、そのレジストリの認証が必要になります。
そこで、まずはこちらのコマンドでECRに対して認証を行います。

docker build -t qiita-spring-ecs-app .

続いて、先ほど作成したDockerfileをもとにDockerイメージをビルドします。
イメージ名は、自動的にリポジトリ名と同じく「qiita-spring-ecs-app」となっています。

docker tag qiita-spring-ecs-app:latest <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/qiita-spring-ecs-app:1.0.0
docker push <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/qiita-spring-ecs-app:1.0.0

最後に、作成したイメージに1.0.0のタグを付け、ECRへプッシュします。

image.png

6. VPCエンドポイントの作成

ECSサービスはプライベートサブネットに配置しますが、VPC外部のAWSサービスへアクセスする必要があるため、エンドポイントを作成します。

作成するエンドポイントは以下の4つです。

エンドポイント 内容
com.amazonaws.ap-northeast-1.ecr.dkr Dockerイメージのプル
com.amazonaws.ap-northeast-1.ecr.api Dockerレジストリへの認証
com.amazonaws.ap-northeast-1.s3(Gateway) Dockerイメージの保存
com.amazonaws.ap-northeast-1.logs CloudWatch Logsへのログ書き込み

3つ目のs3は、初めの「ネットワークの構築」でVPCと併せて作成しているので、残り3つを作成します。

image.png
名前は「qiita-spring-ecs-vpce-ecr-dkr」とし、タイプは「AWSのサービス」を選択します。

image.png
サービスは「com.amazonaws.ap-northeast-1.ecr.dkr」を選択します。

image.png
VPCは「qiita-spring-ecs-vpc」、サブネットは private subnet 1 (1a)private subnet 2 (1c) を選択します。

image.png
セキュリティグループは「qiita-spring-ecs-sg-vpce」を選択します。

同様に他2つのエンドポイントも作成します。

7. ロードバランサーの作成(ELB)

image.png
ロードバランサータイプは「Application Load Balancer」として、ロードバランサーを作成します。

image.png
名前は「qiita-spring-ecs-elb」とします。
外部からアクセスできるようにするため、スキームは「インターネット向け」を選択します。

image.png
image.png
VPCは「qiita-spring-ecs-vpc」、サブネットは public subnet 1 (1a)public subnet 2 (1c) を選択します。
セキュリティグループは「qiita-spring-ecs-sg-elb」を選択します。

続いて、「ターゲットグループの作成」リンクから、別タブでターゲットグループを作成します。

image.png
ロードバランサーは、ECSタスクのIPアドレスに対してルーティングを行うので、ターゲットタイプは「IPアドレス」を選択します。

image.png
image.png
名前は「qiita-spring-ecs-tg」とします。
プロトコル:ポートについては、ターゲットグループがロードバランサーから受け付けるリクエストの情報を設定するので、「HTTP」「8080」とします。
(Spring Bootアプリケーションはデフォルトポート8080で動かす想定)
VPCは「qiita-spring-ecs-vpc」を選択し、その他の設定はデフォルトのままで、「次へ」とします。

image.png
ターゲットは何も登録せず、「ターゲットグループの作成」とします。

ECSタスク起動時に、ECSがタスクに対してIPアドレスの割り当て・ターゲットグループへの追加を自動で行ってくれるので、ここでは何も登録しなくて大丈夫です。

ロードバランサー作成のタブに戻ります。

image.png
リスナーとルーティングについて、こちらはロードバランサーがクライアントから受け付けるリクエストの情報を設定するので、「HTTP」「80」とし、デフォルトアクションに先ほど作成した「qiita-spring-ecs-tg」を設定します。

その他の設定はデフォルトのままで、「ロードバランサーの作成」とします。

image.png
ロードバランサーのステータスが「アクティブ」となっていることを確認します。

8. ECSの構築

ECSは主に以下の要素で構成されています。
image.png

  • クラスター:コンテナを動かすための論理的なグループ
  • サービス:タスクの起動タイプや、実行するタスクとその数などを定義。ロードバランサーと連携して各タスクへリクエストを分散
  • タスク定義:タスクを構成するコンテナ群や、イメージの参照先URIなどを定義
  • タスク:タスク定義に基づき、サービスにより起動されるコンテナ群

以下ではこれらを作成していきます。

1) クラスターの作成

image.png
名前は「qiita-spring-ecs-cluster」とします。
インフラストラクチャでは「AWS Fargate (サーバーレス)」を選択します。

その他の設定はデフォルトのままで、「作成」とします。

2) IAMロールの作成

続いて、この後作成するタスク定義にアタッチするIAMロールを作成します。
ECRからのイメージ取得や、ログ出力などを行うのに必要となります。

image.png
エンティティタイプは「AWSのサービス」を選択します。

image.png
サービスまたはユースケースは「Elastic Container Service」、ユースケースは「Elastic Container Service Task」を選択し、「次へ」とします。

image.png
許可ポリシーでは「AmazonECSTaskExecutionRolePolicy」を選択し、「次へ」とします。

image.png
名前は「ECSTaskExecutionRole」とし、その他の設定はデフォルトのままで「ロールを作成」とします。

3) タスク定義の作成

image.png
image.png
名前は「qiita-spring-ecs-task」とします。
起動タイプは「AWS Fargate」を選択し、タスク実行ロールは「ECSTaskExecutionRole」を選択します。

image.png
今回立ち上げるコンテナは以下の1つのみです。
名前は「qiita-spring-ecs-app」とし、イメージURIはリポジトリURI:1.0.0とします。
アプリケーションは8080ポートで動かすので、コンテナポートを「8080」、プロトコルを「TCP」、アプリケーションプロトコルは「HTTP」を選択します。

その他の設定はデフォルトのままで「作成」とします。

4) サービスの作成

先ほど作成したクラスターを選択し、サービスを作成とします。

image.png
タスク定義ファミリーは「qiita-spring-ecs-task」を選択し、リビジョンは自動で入力されます。
サービス名は「qiita-spring-ecs-service」とします。

image.png
コンピューティングオプションは「起動タイプ」を選択します。

image.png
必要なタスクは2、リバランスを有効にすることで、各プライベートサブネットにタスクを起動するようにします。

image.png
VPCは「qiita-spring-ecs-vpc」、サブネットは private subnet 1 (1a)private subnet 2 (1c) を選択します。
セキュリティグループは「qiita-spring-ecs-sg-service」を選択し、パブリックIPはオフにします。

image.png
image.png
ロードバランシングを使用とし、既存のロードバランサーから「qiita-spring-ecs-elb」を選択します。
また既存のターゲットグループから「qiita-spring-ecs-tg」を選択します。

その他の設定はデフォルトのままで、「作成」とします。

image.png
5分ほど待ち、作成されたサービスを確認すると、ステータスはアクティブとなり、2件のタスクが実行中であることが分かります。

9. 動作確認

まずはアプリケーションにアクセスできることを確認します。

以下URLにアクセスします。
http://<ロードバランサーのDNS名>

image.png
無事動いています!

image.png
また、何度かリロードするとホスト名が変わることから、各コンテナにリクエストが分散していることが分かります。

続いて、片方のコンテナを停止しても問題なくアクセスできるか確認します。

image.png
タスクを1つ停止します。

image.png
すると、先ほどのタスクは停止済みとなり、自動で新たなタスクが実行されました。

image.png
image.png
再度ブラウザからアクセスすると、新たなタスクへのアクセス含め、正常に行われていることが分かります。

10. ECSタスクの更新

1) イメージの更新

適当にアプリケーションを更新し、5の2)と同様の方法で、Dockerイメージのビルド、およびECRへの認証を行います。

docker tag qiita-spring-ecs-app:latest <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/qiita-spring-ecs-app:2.0.0
docker push <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/qiita-spring-ecs-app:2.0.0

今回は2.0.0タグを付けてプッシュします。

image.png

2) タスク定義の更新

作成したタスク定義を選択し、「新しいリビジョンの作成」とします。

コンテナのイメージURIのタグを2.0.0に変更します。
その他の設定は変更せず、「作成」とします。

image.png
リビジョンが自動でインクリメントされ、新たなタスク定義が作成されました。

3) サービスの更新

作成したサービスを選択し、「サービスを更新」とします。

タスク定義のリビジョンから、先ほど作成したタスク定義のリビジョンを選択します。
その他の設定は変更せず、「更新」とします。

image.png
はじめに、新たに作成したリビジョン(7)のタスクが実行され、その後、古いリビジョン(6)のタスクが停止されます。

新たなタスクのデプロイ方法は、サービスに対して設定することができます。

image.png
今回はタスク数2、デプロイ方法は上記の通りデフォルトの設定なので、以下の通り動作しました。
image.png

image.png
image.png
ブラウザからアクセスすると、更新後の内容となっていることが分かります。

後片付け

本手順において、AWSサービスに対する必要以上の課金を防ぐ必要がある場合、最低限行う対応は以下の通りです。

  • ロードバランサーの削除
  • ECSサービスの削除
  • ECSタスク定義の削除
  • ECRにプッシュしたイメージの削除
  • VPCエンドポイント(ecr.dkr, ecr.api, logs)の削除

おわりに

以上で、AWSを使用した本番環境の構築、および手動でのコンテナ化アプリのデプロイが完了しました。

アプリケーションのイメージはECRで管理し、ECSサービスとして実行しています。
また、マルチAZ構成でECSタスクを複数立ち上げ、ロードバランサーによりトラフィックを分散させています。

本記事では、イメージのビルドやECRへのプッシュ、タスク定義・サービスの更新を手動で実施しました。
後編では、GitHub Actionsでパイプラインを構築することでこれらを自動化していきます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?