はじめに
概要
AWSとGitHub Actionsを使用した本番環境の構築・アプリケーションのデプロイ方法について取り上げます。
前提条件
- Java/Spring Boot環境構築済み
- AWSアカウント作成済み
- GitHubアカウント作成済み
リポジトリ
動作環境
- Windows 11 Home(24H2)
- Java 21
- Maven 3.9.9
- Spring Boot 3.4.5
本手順
前編では、EC2でWebサーバを構築し、手動でアプリケーションをデプロイするところまで実施します。
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 EC2</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-ec2」とします。
IPv4 CIDRブロックはデフォルトで上記の設計通りになっていると思います。


AZの数は2つ、パブリックサブネット/プライベートサブネットの数もそれぞれ2つとします。
VPC同様、各サブネットのCIDRブロックもデフォルトで設計通りになっています。
その他の設定はデフォルトのままで、「VPCを作成」とします。
3. セキュリティグループの作成
1) 踏み台サーバ用
はじめに、踏み台サーバにアタッチするセキュリティグループを作成します。

名前は「qiita-spring-ec2-sg-bastion」とします。
踏み台サーバはSSH(ポート:22)による通信を受け付ける必要があるため、以下の通りインバウンドルールを追加します。
- タイプ:SSH、ソース:0.0.0.0/0
以上で「セキュリティグループを作成」とします。
2) ロードバランサー用
続いて、ロードバランサーにアタッチするセキュリティグループを作成します。

名前は「qiita-spring-ec2-sg-elb」とします。
ロードバランサーはHTTP(ポート:80)による通信を受け付ける必要があるため、以下の通りインバウンドルールを追加します。
- タイプ:HTTP、ソース:0.0.0.0/0
以上で「セキュリティグループを作成」とします。
3) Webサーバ用
最後に、Webサーバにアタッチするセキュリティグループを作成します。

名前は「qiita-spring-ec2-sg-web」とします。
Webサーバは、踏み台サーバからSSH(ポート:22)による通信、およびロードバランサーからHTTP(ポート:8080)による通信を受け付ける必要があるため、以下の通りインバウンドルールを追加します。
- タイプ:SSH、ソース:qiita-spring-ec2-sg-bastion
- タイプ:カスタムTCP、ポート範囲:8080、ソース:qiita-spring-ec2-sg-elb
以上で「セキュリティグループを作成」とします。
4. 踏み台サーバの構築(EC2)
1) キーペアの作成
SSH接続で使用するキーペアを作成します。

名前は「qiita-spring-ec2-key」とします。
キーペアのタイプは「RSA」、プライベートファイルキーは「.pem」形式を選択し、「キーペアを作成」とします。
2) EC2インスタンスの作成

名前は「qiita-spring-ec2-bastion」とします。
OSは「Ubuntu」、AMIは「Ubuntu Server 24.04 LTS (HVM), SSD Volume Type」を選択します。


キーペアは「qiita-spring-ec2-key」を選択します。
VPCは「qiita-spring-ec2-vpc」、サブネットは public subnet 1 (1a) とします。
外部からのアクセスを受け付ける必要があるため、パブリックIPの自動割り当てを有効にします。
セキュリティグループは「qiita-spring-ec2-sg-bastion」を選択します。
その他の設定はデフォルトのままで、「インスタンスを起動」とします。
5. Webサーバ構築用AMIの作成
続いてWebサーバを構築していきます。
今回構築する各Webサーバはプライベートサブネットに配置し、特にNATゲートウェイなどは設けないので、Webサーバからインターネットには接続できない想定です。
よって、単純に構築するとSpring Bootアプリケーションを動かすのに必要なJDKをインストールできないため、以下手順を踏むことにします。
Webサーバが継続的にインターネットへ接続する場合はNATゲートウェイを設ける必要があると思うのですが、今回は初回のJDKインストール時のみ必要なので上記方法で実施します。(NATゲートウェイを設けないことでコストを抑えます)
1) EC2インスタンスの作成

名前は「qiita-spring-ec2-ami-builder」とします。
OSは「Ubuntu」、AMIは「Ubuntu Server 24.04 LTS (HVM), SSD Volume Type」を選択します。


以下は先ほどの踏み台サーバの設定と同一です。
キーペアは「qiita-spring-ec2-key」を選択します。
VPCは「qiita-spring-ec2-vpc」、サブネットは public subnet 1 (1a) とします。
こちらも外部からのアクセスを受け付ける必要があるため、パブリックIPの自動割り当てを有効にします。
セキュリティグループは「qiita-spring-ec2-sg-bastion」を選択します。
その他の設定はデフォルトのままで、「インスタンスを起動」とします。
2) JDKのインストール
キーペア作成時にローカルマシンにダウンロードされたqiita-spring-ec2-key.pemファイルを~/.sshフォルダに配置します。
以下コマンドでAMI作成用サーバに接続します。
ssh -i ~/.ssh/qiita-spring-ec2-key.pem ubuntu@<AMI作成用サーバのパブリックIPアドレス>
接続後、JDKをインストールします。
今回はJDKのディストリビューションとして、「Amazon Corretto 21」を使用します。
# AMI作成用サーバ
# 依存パッケージのインストール
sudo apt update
sudo apt install -y wget software-properties-common
# 署名キーの追加
wget -O- https://apt.corretto.aws/corretto.key | sudo apt-key add -
# リポジトリの追加
sudo add-apt-repository 'deb https://apt.corretto.aws stable main'
sudo apt update
# Amazon Corretto 21のインストール
sudo apt install -y java-21-amazon-corretto-jdk
バージョン確認を行い、正常にインストールされていることを確認します。
# AMI作成用サーバ
java --version
> openjdk 21.0.7 2025-04-15 LTS
> OpenJDK Runtime Environment Corretto-21.0.7.6.1 (build 21.0.7+6-LTS)
> OpenJDK 64-Bit Server VM Corretto-21.0.7.6.1 (build 21.0.7+6-LTS, mixed mode, sharing)
3) AMIの作成

名前は「qiita-spring-ec2-ami-web」とします。

その他の設定はデフォルトのままで、「イメージを作成」とします。
AMIが作成され、ステータスが「利用可能」となっていることを確認します。(5~10分ほどかかります)
ここまで完了すれば、AMI作成用サーバは停止/削除してしまって問題ありません。
6. Webサーバの構築(EC2)
ここではひとまず「Webサーバ1」のみ構築します。

作成したAMIを選択し、「AMIからインスタンスを起動」とします。

名前は「qiita-spring-ec2-web1」とします。
OSおよびAMIは「qiita-spring-ec2-ami-web」が選択されています。


キーペアは「qiita-spring-ec2-key」を選択します。
VPCは「qiita-spring-ec2-vpc」、サブネットは private subnet 1 (1a) とします。
外部からのアクセスは受け付けないため、パブリックIPの自動割り当てを無効にします。
セキュリティグループは「qiita-spring-ec2-sg-web」を選択します。
その他の設定はデフォルトのままで、「インスタンスを起動」とします。
7. アプリケーションのデプロイ
1) SSH接続の設定
~/.sshフォルダに「config」という名前で、以下の通りファイルを作成します。
Host bastion
HostName <踏み台サーバのパブリックIPアドレス>
User ubuntu
IdentityFile ~/.ssh/qiita-spring-ec2-key.pem
Host web1
HostName <Webサーバ1のプライベートIPアドレス>
User ubuntu
IdentityFile ~/.ssh/qiita-spring-ec2-key.pem
ProxyCommand ssh -W %h:%p bastion
| 項目 | 内容 |
|---|---|
| Host | サーバに割り当てるエイリアス |
| HostName | IPアドレス/ホスト名(一つ前のマシンから見た情報なので、WebサーバはプライベートIPアドレスでOK) |
| User | ログインに使用するユーザ名(EC2インスタンス(OS:Ubuntu)のデフォルトユーザは「ubuntu」) |
| IdentityFile | 秘密鍵のパス(HostNameと異なり、ローカルマシンにおける情報を記述) |
| ProxyCommand | 踏み台サーバのエイリアスを記述 |
2) デプロイ用ディレクトリの作成
以下コマンドでWebサーバ1にSSH接続します。
ssh web1
初回接続時は、接続して問題ないか、踏み台サーバとWebサーバ1の分で計2回聞かれますがどちらもyesとします。
無事接続できたら、以下の通りデプロイ用ディレクトリを作成します。
# Webサーバ1
mkdir -p ~/app
3) 成果物のデプロイ
ローカルマシンに戻り、アプリケーションをビルドします。
./mvnw clean package
ビルド成果物であるJARファイルを、Webサーバ1のデプロイ用ディレクトリに転送します。
scp target/demo-0.0.1-SNAPSHOT.jar web1:~/app/app.jar
4) systemdサービスの設定
再度Webサーバ1に接続し、/etc/systemd/systemフォルダに「qiita-spring-ec2-app.service」という名前で、以下の通りファイルを作成します。
※同フォルダでのファイル作成は管理者権限が必要です。
[Unit]
Description=Qiita Spring EC2 Application
After=syslog.target network.target
[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/app
ExecStart=java -jar app.jar
SuccessExitStatus=143
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target
サービスを有効化・起動します。
# Webサーバ1
sudo systemctl enable qiita-spring-ec2-app
sudo systemctl start qiita-spring-ec2-app
正常に起動していることを確認します。
# Webサーバ1
sudo systemctl status qiita-spring-ec2-app
> Loaded: loaded (/etc/systemd/system/qiita-spring-ec2-app.service; enabled; preset: enabled)
> Active: active (running) since …
8. Webサーバのコピー
先ほどと同じ要領で、Webサーバ1からAMIを作成し、作成したAMIからWebサーバ2を構築します。



名前は「qiita-spring-ec2-ami-web-deployed」とし、その他の設定はデフォルトのままで、「イメージを作成」とします。

作成したAMIを選択し、「AMIからインスタンスを起動」とします。

名前は「qiita-spring-ec2-web2」とします。
OSおよびAMIは「qiita-spring-ec2-ami-web-deployed」が選択されています。


キーペアは「qiita-spring-ec2-key」を選択します。
VPCは「qiita-spring-ec2-vpc」、サブネットは private subnet 2 (1c) とします。
外部からのアクセスは受け付けないため、パブリックIPの自動割り当てを無効にします。
セキュリティグループは「qiita-spring-ec2-sg-web」を選択します。
その他の設定はデフォルトのままで、「インスタンスを起動」とします。
9. ロードバランサーの作成(ELB)

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

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


VPCは「qiita-spring-ec2-vpc」、サブネットは public subnet 1 (1a) 、 public subnet 2 (1c) を選択します。
セキュリティグループは「qiita-spring-ec2-sg-elb」を選択します。
続いて、「ターゲットグループの作成」リンクから、別タブでターゲットグループを作成します。

ロードバランサーのルーティング先はWebサーバ(EC2インスタンス)なので、ターゲットタイプは「インスタンス」を選択します。


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

Webサーバ1とWebサーバ2を選択し、「保留中として以下を含める」を選択します。

ターゲットに該当のサーバが追加されるので、「ターゲットグループの作成」とします。
ロードバランサー作成のタブに戻ります。

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


ロードバランサーのステータスが「アクティブ」、ターゲットグループに含まれる各ターゲットのヘルスステータスが「Healthy」となっていることを確認します。(5分ほどかかります)
10. 動作確認
まずはアプリケーションにアクセスできることを確認します。
以下URLにアクセスします。
http://<ロードバランサーのDNS名>

また、何度かリロードするとホスト名が変わることから、各Webサーバにリクエストが分散していることが分かります。
続いて、片方のWebサーバを停止しても問題なくアクセスできるか確認します。

ターゲットグループを確認すると、意図した通りヘルスステータスが「Unused」に変わっていることが分かります。

再度リロードを繰り返してみても、引き続き問題なくアクセスできています!
また、アクセスがWebサーバ2に集中していることも分かります。
後片付け
本手順において、AWSサービスに対する必要以上の課金を防ぐ必要がある場合、最低限行う対応は以下の通りです。
- ロードバランサーの削除
- EC2インスタンスの停止
おわりに
以上で、AWSを使用した本番環境の構築、および手動でのデプロイが完了しました。
WebサーバはEC2で構築し、プライベートサブネットに配置することで外部から隠蔽しています。
また、マルチAZ構成でサーバを冗長化し、ロードバランサーによりトラフィックを分散させることを実現しました。
本記事では、アプリケーションのビルドや、Webサーバへの成果物の転送、systemdサービスの更新を手動で実施しました。
後編では、GitHub Actionsでパイプラインを構築することでこれらを自動化していきます。






