1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【EC2 + GitHub Actions】Spring Bootアプリの本番環境を構築 [前編] - アプリ公開まで

1
Last updated at Posted at 2025-08-09

はじめに

概要

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

image.png

前提条件

  • 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

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

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 EC2</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コンソールにログインして、実際に作成していきます。

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

{D7D594EA-B72B-4802-9C18-D7239111D0B0}.png
{F35833F2-50BE-4BF0-8E8E-47E74658861D}.png
AZの数は2つ、パブリックサブネット/プライベートサブネットの数もそれぞれ2つとします。
VPC同様、各サブネットのCIDRブロックもデフォルトで設計通りになっています。

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

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

1) 踏み台サーバ用

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

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

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

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

2) ロードバランサー用

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

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

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

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

3) Webサーバ用

最後に、Webサーバにアタッチするセキュリティグループを作成します。

image.png
名前は「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接続で使用するキーペアを作成します。

{114F78A6-E03D-49E0-ABC2-0759DF78CCE0}.png
名前は「qiita-spring-ec2-key」とします。
キーペアのタイプは「RSA」、プライベートファイルキーは「.pem」形式を選択し、「キーペアを作成」とします。

{03569254-03A5-4ADC-97BE-848B23A41699}.png
秘密鍵がダウンロードされます。

2) EC2インスタンスの作成

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

image.png
image.png
キーペアは「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をインストールできないため、以下手順を踏むことにします。

  1. 一度AMI作成用サーバをパブリックサブネットに構築
  2. 構築したサーバに接続し、JDKをインストール
  3. 上記サーバにおけるAMIを作成
  4. 作成したAMIをもとにWebサーバを構築
  5. AMI作成用サーバを削除
    image.png

Webサーバが継続的にインターネットへ接続する場合はNATゲートウェイを設ける必要があると思うのですが、今回は初回のJDKインストール時のみ必要なので上記方法で実施します。(NATゲートウェイを設けないことでコストを抑えます)

1) EC2インスタンスの作成

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

image.png
image.png
以下は先ほどの踏み台サーバの設定と同一です。
キーペアは「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の作成

image.png
AMI作成用サーバを選択し、「イメージを作成」とします。

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

image.png
その他の設定はデフォルトのままで、「イメージを作成」とします。

AMIが作成され、ステータスが「利用可能」となっていることを確認します。(5~10分ほどかかります)
ここまで完了すれば、AMI作成用サーバは停止/削除してしまって問題ありません。

6. Webサーバの構築(EC2)

ここではひとまず「Webサーバ1」のみ構築します。

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

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

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

7. アプリケーションのデプロイ

1) SSH接続の設定

~/.sshフォルダに「config」という名前で、以下の通りファイルを作成します。

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」という名前で、以下の通りファイルを作成します。
※同フォルダでのファイル作成は管理者権限が必要です。

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を構築します。

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

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

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

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

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

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

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

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

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

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

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

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

image.png
ターゲットに該当のサーバが追加されるので、「ターゲットグループの作成」とします。

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

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

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

image.png
image.png
ロードバランサーのステータスが「アクティブ」、ターゲットグループに含まれる各ターゲットのヘルスステータスが「Healthy」となっていることを確認します。(5分ほどかかります)

10. 動作確認

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

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

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

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

続いて、片方のWebサーバを停止しても問題なくアクセスできるか確認します。

image.png
Webサーバ1を停止します。

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

image.png
再度リロードを繰り返してみても、引き続き問題なくアクセスできています!
また、アクセスがWebサーバ2に集中していることも分かります。

後片付け

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

  • ロードバランサーの削除
  • EC2インスタンスの停止

おわりに

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

WebサーバはEC2で構築し、プライベートサブネットに配置することで外部から隠蔽しています。
また、マルチAZ構成でサーバを冗長化し、ロードバランサーによりトラフィックを分散させることを実現しました。

本記事では、アプリケーションのビルドや、Webサーバへの成果物の転送、systemdサービスの更新を手動で実施しました。
後編では、GitHub Actionsでパイプラインを構築することでこれらを自動化していきます。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?