はじめに
概要
AWSとGitHub Actionsを使用した本番環境の構築・コンテナ化アプリのデプロイ方法について取り上げます。
前提条件
- 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
続いて、以下の通り実装します。
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;
}
}
<!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
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コンソールにログインして、実際に作成していきます。
リージョンは東京を使用します。
作成するリソースは「VPCなど」を選択することで、VPC・サブネット・ルートテーブル・インターネットゲートウェイなどをまとめて作成します。
名前は「qiita-spring-ecs」とします。
IPv4 CIDRブロックはデフォルトで上記の設計通りになっていると思います。
AZの数は2つ、パブリックサブネット/プライベートサブネットの数もそれぞれ2つとします。
VPC同様、各サブネットのCIDRブロックもデフォルトで設計通りになっています。
その他の設定はデフォルトのままで、「VPCを作成」とします。
3. セキュリティグループの作成
1) ロードバランサー用
はじめに、ロードバランサーにアタッチするセキュリティグループを作成します。
名前は「qiita-spring-ecs-sg-elb」とします。
ロードバランサーはHTTP(ポート:80)による通信を受け付ける必要があるため、以下の通りインバウンドルールを追加します。
- タイプ:HTTP、ソース:0.0.0.0/0
以上で「セキュリティグループを作成」とします。
2) ECSサービス用
続いて、ECSサービスにアタッチするセキュリティグループを作成します。
名前は「qiita-spring-ecs-sg-service」とします。
ECSサービスはロードバランサーからHTTP(ポート:8080)による通信を受け付ける必要があるため、以下の通りインバウンドルールを追加します。
- タイプ:カスタムTCP、ポート範囲:8080、ソース:qiita-spring-ecs-sg-elb
以上で「セキュリティグループを作成」とします。
3) VPCエンドポイント用
最後に、VPCエンドポイントにアタッチするセキュリティグループを作成します。
名前は「qiita-spring-ecs-sg-vpce」とします。
今回作成するVPCエンドポイントは、ECSサービスからHTTPS(ポート:443)による通信を受け付ける必要があるため、以下の通りインバウンドルールを追加します。
- タイプ:HTTPS、ソース:qiita-spring-ecs-sg-service
以上で「セキュリティグループを作成」とします。
4. プライベートリポジトリの作成(ECR)
Dockerレジストリ(イメージの保存先)にECRを使用します。
プライベートリポジトリを作成し、名前は「qiita-spring-ecs-app」とします。
ミュータビリティは「Mutable」、暗号化設定は「AES-256」を選択します。
その他の設定はデフォルトのままで、「作成」とします。
5. Dockerイメージの作成
1) 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
はじめに、アプリケーションをビルドしておきます。

ここで、先ほど作成したリポジトリを選択し、「プッシュコマンドを表示」とすると、必要なコマンドが表示されるのでこちらを参考にしていきます。
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へプッシュします。
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つを作成します。
名前は「qiita-spring-ecs-vpce-ecr-dkr」とし、タイプは「AWSのサービス」を選択します。
サービスは「com.amazonaws.ap-northeast-1.ecr.dkr」を選択します。
VPCは「qiita-spring-ecs-vpc」、サブネットは private subnet 1 (1a) 、 private subnet 2 (1c) を選択します。
セキュリティグループは「qiita-spring-ecs-sg-vpce」を選択します。
同様に他2つのエンドポイントも作成します。
7. ロードバランサーの作成(ELB)
ロードバランサータイプは「Application Load Balancer」として、ロードバランサーを作成します。
名前は「qiita-spring-ecs-elb」とします。
外部からアクセスできるようにするため、スキームは「インターネット向け」を選択します。
VPCは「qiita-spring-ecs-vpc」、サブネットは public subnet 1 (1a) 、 public subnet 2 (1c) を選択します。
セキュリティグループは「qiita-spring-ecs-sg-elb」を選択します。
続いて、「ターゲットグループの作成」リンクから、別タブでターゲットグループを作成します。
ロードバランサーは、ECSタスクのIPアドレスに対してルーティングを行うので、ターゲットタイプは「IPアドレス」を選択します。
名前は「qiita-spring-ecs-tg」とします。
プロトコル:ポートについては、ターゲットグループがロードバランサーから受け付けるリクエストの情報を設定するので、「HTTP」「8080」とします。
(Spring Bootアプリケーションはデフォルトポート8080で動かす想定)
VPCは「qiita-spring-ecs-vpc」を選択し、その他の設定はデフォルトのままで、「次へ」とします。
ターゲットは何も登録せず、「ターゲットグループの作成」とします。
ECSタスク起動時に、ECSがタスクに対してIPアドレスの割り当て・ターゲットグループへの追加を自動で行ってくれるので、ここでは何も登録しなくて大丈夫です。
ロードバランサー作成のタブに戻ります。
リスナーとルーティングについて、こちらはロードバランサーがクライアントから受け付けるリクエストの情報を設定するので、「HTTP」「80」とし、デフォルトアクションに先ほど作成した「qiita-spring-ecs-tg」を設定します。
その他の設定はデフォルトのままで、「ロードバランサーの作成」とします。
ロードバランサーのステータスが「アクティブ」となっていることを確認します。
8. ECSの構築
- クラスター:コンテナを動かすための論理的なグループ
- サービス:タスクの起動タイプや、実行するタスクとその数などを定義。ロードバランサーと連携して各タスクへリクエストを分散
- タスク定義:タスクを構成するコンテナ群や、イメージの参照先URIなどを定義
- タスク:タスク定義に基づき、サービスにより起動されるコンテナ群
以下ではこれらを作成していきます。
1) クラスターの作成
名前は「qiita-spring-ecs-cluster」とします。
インフラストラクチャでは「AWS Fargate (サーバーレス)」を選択します。
その他の設定はデフォルトのままで、「作成」とします。
2) IAMロールの作成
続いて、この後作成するタスク定義にアタッチするIAMロールを作成します。
ECRからのイメージ取得や、ログ出力などを行うのに必要となります。
サービスまたはユースケースは「Elastic Container Service」、ユースケースは「Elastic Container Service Task」を選択し、「次へ」とします。
許可ポリシーでは「AmazonECSTaskExecutionRolePolicy」を選択し、「次へ」とします。
名前は「ECSTaskExecutionRole」とし、その他の設定はデフォルトのままで「ロールを作成」とします。
3) タスク定義の作成
名前は「qiita-spring-ecs-task」とします。
起動タイプは「AWS Fargate」を選択し、タスク実行ロールは「ECSTaskExecutionRole」を選択します。
今回立ち上げるコンテナは以下の1つのみです。
名前は「qiita-spring-ecs-app」とし、イメージURIはリポジトリURI:1.0.0とします。
アプリケーションは8080ポートで動かすので、コンテナポートを「8080」、プロトコルを「TCP」、アプリケーションプロトコルは「HTTP」を選択します。
その他の設定はデフォルトのままで「作成」とします。
4) サービスの作成
先ほど作成したクラスターを選択し、サービスを作成とします。
タスク定義ファミリーは「qiita-spring-ecs-task」を選択し、リビジョンは自動で入力されます。
サービス名は「qiita-spring-ecs-service」とします。
必要なタスクは2、リバランスを有効にすることで、各プライベートサブネットにタスクを起動するようにします。
VPCは「qiita-spring-ecs-vpc」、サブネットは private subnet 1 (1a) 、 private subnet 2 (1c) を選択します。
セキュリティグループは「qiita-spring-ecs-sg-service」を選択し、パブリックIPはオフにします。
ロードバランシングを使用とし、既存のロードバランサーから「qiita-spring-ecs-elb」を選択します。
また既存のターゲットグループから「qiita-spring-ecs-tg」を選択します。
その他の設定はデフォルトのままで、「作成」とします。
5分ほど待ち、作成されたサービスを確認すると、ステータスはアクティブとなり、2件のタスクが実行中であることが分かります。
9. 動作確認
まずはアプリケーションにアクセスできることを確認します。
以下URLにアクセスします。
http://<ロードバランサーのDNS名>
また、何度かリロードするとホスト名が変わることから、各コンテナにリクエストが分散していることが分かります。
続いて、片方のコンテナを停止しても問題なくアクセスできるか確認します。
すると、先ほどのタスクは停止済みとなり、自動で新たなタスクが実行されました。
再度ブラウザからアクセスすると、新たなタスクへのアクセス含め、正常に行われていることが分かります。
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タグを付けてプッシュします。
2) タスク定義の更新
作成したタスク定義を選択し、「新しいリビジョンの作成」とします。
コンテナのイメージURIのタグを2.0.0に変更します。
その他の設定は変更せず、「作成」とします。
リビジョンが自動でインクリメントされ、新たなタスク定義が作成されました。
3) サービスの更新
作成したサービスを選択し、「サービスを更新」とします。
タスク定義のリビジョンから、先ほど作成したタスク定義のリビジョンを選択します。
その他の設定は変更せず、「更新」とします。
はじめに、新たに作成したリビジョン(7)のタスクが実行され、その後、古いリビジョン(6)のタスクが停止されます。
ブラウザからアクセスすると、更新後の内容となっていることが分かります。
後片付け
本手順において、AWSサービスに対する必要以上の課金を防ぐ必要がある場合、最低限行う対応は以下の通りです。
- ロードバランサーの削除
- ECSサービスの削除
- ECSタスク定義の削除
- ECRにプッシュしたイメージの削除
- VPCエンドポイント(ecr.dkr, ecr.api, logs)の削除
おわりに
以上で、AWSを使用した本番環境の構築、および手動でのコンテナ化アプリのデプロイが完了しました。
アプリケーションのイメージはECRで管理し、ECSサービスとして実行しています。
また、マルチAZ構成でECSタスクを複数立ち上げ、ロードバランサーによりトラフィックを分散させています。
本記事では、イメージのビルドやECRへのプッシュ、タスク定義・サービスの更新を手動で実施しました。
後編では、GitHub Actionsでパイプラインを構築することでこれらを自動化していきます。