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

【EC2 + GitHub Actions】Spring Bootアプリの本番環境を構築 [後編] - CI/CDパイプライン構築

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

本手順

前編では、Webサーバやロードバランサーの構築まで完了しました。

後編では、アプリケーションのビルドや、Webサーバへの成果物の転送、systemdサービスの更新を、GitHub Actionsを使用して自動化する方法をまとめていきます。

1. 基本

ブランチ戦略はGitHub Flowを採用する想定で進めます。

GitHub Flowの「mainブランチは常にデプロイ可能な状態を維持する」という考え方に基づき、ワークフローは以下の通り作成します。
image.png

後述するブランチ保護ルールの設定と組み合わせることで、ビルド・テストが正常に動くことが担保されたコードのみがmainブランチにマージされる構成です。

上記構成に基づき、アプリケーションルート/.github/workflowsフォルダに「main.yml」という名前で、以下の通りワークフローファイルを作成します。

main.yml
name: Qiita Spring EC2 Application CI/CD
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

  test:
    needs: build
    runs-on: ubuntu-latest

  deploy:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    needs: test
    runs-on: ubuntu-latest

2. ブランチ保護ルールの設定

今回設定するルールは以下の2つです。

ルール 内容
Require a pull request before merging PRを介さない直接のプッシュを禁止
Require status checks to pass PR時、指定したワークフローのジョブが成功することをチェック

実際に設定していきます。

image.png
GitHubで対象のリポジトリを開き、設定から「New branch ruleset」を選択します。

image.png
名前は「main rule」とし、ステータスを「Active」にします。

image.png
ターゲットは「default branch(=main)」を選択します。

ルールでは、デフォルトで選択されている「Restrict deletions(ブランチ削除禁止)」と「Block force pushes(強制プッシュ禁止)」はそのままとします。

image.png
Require a pull request before merging」を選択します。
「Required approvals(チームメンバーから必要な承認の数)」は0のままとし、その他の追加設定もデフォルトのままとします。

image.png
Require status checks to pass」を選択し、checksに「build」「test」を登録します。
これにより、PR時にbuild・testの両ジョブが成功しない限りマージできないようにします。
加えて、「Require branches to be up to date before merging」を選択することで、最新のコードベース、すなわちPRブランチがターゲットブランチの最新の変更まで全て取り込んだ上で上記確認を行うようにします。

以上で、「Create」とします。

3. buildジョブの作成

ジョブを作成していきます。

始めに、以下の構成でbuildジョブを作成します。
①ソースコードのチェックアウト
②Java環境構築
③アプリケーションのビルド
④ビルド成果物のアップロード

①ソースコードのチェックアウト

uses: actions/checkout@v4

GitHub公式のアクションを使用します。

②Java環境構築

uses: actions/setup-java@v4
with:
  java-version: '21'
  distribution: 'corretto'
  cache: maven

GitHub公式のアクションを使用します。
パラメータに、Javaバージョンは21、ディストリビューションは本番環境のWebサーバと同様「Amazon Corretto」を指定します。
また、Mavenのローカルリポジトリをキャッシュすることで、2回目以降の実行時間を短縮するようにします。

③アプリケーションのビルド

run: mvn -B clean package -DskipTests

Mavenコマンドをバッチモードで使用し、パッケージングを行います。
テストは後述するtestジョブが担う想定なので、ここではスキップします。

④ビルド成果物のアップロード

uses: actions/upload-artifact@v4
with:
  name: app-jar
  path: target/demo-0.0.1-SNAPSHOT.jar

GitHub公式のアクションを使用します。
ビルド成果物を、「app-jar」という名前でGitHub Actionsのアーティファクトに保存します。

以上で完成したワークフローファイルは以下の通りです。

main.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'corretto'
          cache: maven
      - name: Build with Maven
        run: mvn -B clean package -DskipTests
      - name: Upload JAR Artifacts
        uses: actions/upload-artifact@v4
        with:
          name: app-jar
          path: target/demo-0.0.1-SNAPSHOT.jar

4. testジョブの作成

続いて、以下の構成でtestジョブを作成します。
①ソースコードのチェックアウト
②Java環境構築
③アプリケーションのテスト
④テスト結果のアップロード

①ソースコードのチェックアウト
②Java環境構築
buildジョブ同様、GitHub公式のアクションを使用します。

③アプリケーションのテスト

run: mvn -B test

Mavenコマンドをバッチモードで使用し、テストを行います。

④テスト結果のアップロード

if: always()
uses: actions/upload-artifact@v4
with:
  name: test-results
  path: target/surefire-reports/

他のステップ(主にアプリケーションのテスト)が失敗したとしても、テスト結果を「test-results」という名前でGitHub Actionsのアーティファクトに保存します。

以上で完成したワークフローファイルは以下の通りです。

main.yml
jobs:
  # build:…

  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'corretto'
          cache: maven
      - name: Test with Maven
        run: mvn -B test
      - name: Upload Test Results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: target/surefire-reports/

5. deployジョブの作成

1) GitHub Secretsの登録

deployジョブではビルド成果物をWebサーバに転送するにあたり、接続情報が必要になります。
この時、接続情報をワークフローに直接持たせるのはセキュリティ上危険なので、GitHub Secretsに登録してこちらを参照するようにします。

image.png
対象のリポジトリを開き、設定から、Secrets and variablesのActionsを選択します。

Secretsタブから、「New repository secret」を選択します。

image.png
下表の項目を全て登録します。

キー
SPRING_APP_SSH_KEY qiita-spring-ec2-key.pemファイルの中身
SPRING_APP_BASTION_HOST 踏み台サーバのパブリックIPアドレス
SPRING_APP_BASTION_USER ubuntu
SPRING_APP_WEB1_HOST Webサーバ1のプライベートIPアドレス
SPRING_APP_WEB1_USER ubuntu
SPRING_APP_WEB2_HOST Webサーバ2のプライベートIPアドレス
SPRING_APP_WEB2_USER ubuntu

2) ワークフローの作成

最後に、以下の構成でdeployジョブを作成します。
①ソースコードのチェックアウト
②ビルド成果物のダウンロード
③SSH接続の設定
④ビルド成果物の転送
⑤アプリケーションの再起動

①ソースコードのチェックアウト
build・testジョブ同様、GitHub公式のアクションを使用します。

②ビルド成果物のダウンロード

uses: actions/download-artifact@v4
with:
  name: app-jar
  path: target/

GitHub公式のアクションを使用し、buildジョブでアップロードした成果物をダウンロードします。

③SSH接続の設定
・秘密鍵の登録

uses: webfactory/ssh-agent@v0.8.0
with:
  ssh-private-key: ${{ secrets.SPRING_APP_SSH_KEY }}

先ほどGitHub Secretsに登録した情報は、上記の通り「secrets.キー名」で取得できます。
ここでは既存のアクションを使用し、秘密鍵の内容をファイルに書き込むことなくSSHエージェントへ登録しています。
これにより、以降のステップで、例えばsshコマンドにおける-iオプションの指定などが不要になります。

・SSH設定ファイルの作成

run: |
  mkdir -p ~/.ssh
  cat > ~/.ssh/config << 'EOL'
    Host bastion
        HostName ${{ secrets.SPRING_APP_BASTION_HOST }}
        User ${{ secrets.SPRING_APP_BASTION_USER }}
    
    Host web1
        HostName ${{ secrets.SPRING_APP_WEB1_HOST }}
        User ${{ secrets.SPRING_APP_WEB1_USER }}
        ProxyCommand ssh -W %h:%p bastion
    
    Host web2
        HostName ${{ secrets.SPRING_APP_WEB2_HOST }}
        User ${{ secrets.SPRING_APP_WEB2_USER }}
        ProxyCommand ssh -W %h:%p bastion
  EOL
  chmod 600 ~/.ssh/config

続いてSSH接続の設定ファイルを作成します。
各Webサーバのホスト名やユーザ名はGitHub Secretsから取得しています。
先ほど秘密鍵の登録は完了しているので、「IdentityFile xxx.pem」のような指定は省略しています。

・SSH接続先の情報取得

run: |
  ssh-keyscan ${{ secrets.SPRING_APP_BASTION_HOST }} >> ~/.ssh/known_hosts
  ssh bastion "ssh-keyscan -H ${{ secrets.SPRING_APP_WEB1_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null
  ssh bastion "ssh-keyscan -H ${{ secrets.SPRING_APP_WEB2_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null

各Webサーバの公開鍵を事前にknown_hostsファイルに登録しておくことで、初回接続時の確認が発生しないようにします。
また、各Webサーバの公開鍵の情報は-Hオプションでハッシュ化、およびエラー発生時のメッセージをログに残らないようにすることで安全性を高くしています。

④ビルド成果物の転送

run: |
  scp target/demo-0.0.1-SNAPSHOT.jar web1:~/app/app.jar
  scp target/demo-0.0.1-SNAPSHOT.jar web2:~/app/app.jar

scpコマンドを使用してダウンロードした成果物を各Webサーバに転送します。

⑤アプリケーションの再起動

run: |
  ssh web1 << 'ENDSSH1'
    sudo systemctl restart qiita-spring-ec2-app
    sudo systemctl status qiita-spring-ec2-app --no-pager
  ENDSSH1
  ssh web2 << 'ENDSSH2'
    sudo systemctl restart qiita-spring-ec2-app
    sudo systemctl status qiita-spring-ec2-app --no-pager
  ENDSSH2

各Webサーバに接続してアプリケーションの再起動を行います。

以上で完成したワークフローファイルは以下の通りです。

main.yml
jobs:
  # build:…

  # test:…

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Download JAR Artifacts
        uses: actions/download-artifact@v4
        with:
          name: app-jar
          path: target/
      - name: Setup SSH
        uses: webfactory/ssh-agent@v0.8.0
        with:
          ssh-private-key: ${{ secrets.SPRING_APP_SSH_KEY }}
      - name: Create SSH config file
        run: |
          mkdir -p ~/.ssh
          cat > ~/.ssh/config << 'EOL'
            Host bastion
                HostName ${{ secrets.SPRING_APP_BASTION_HOST }}
                User ${{ secrets.SPRING_APP_BASTION_USER }}
            
            Host web1
                HostName ${{ secrets.SPRING_APP_WEB1_HOST }}
                User ${{ secrets.SPRING_APP_WEB1_USER }}
                ProxyCommand ssh -W %h:%p bastion
            
            Host web2
                HostName ${{ secrets.SPRING_APP_WEB2_HOST }}
                User ${{ secrets.SPRING_APP_WEB2_USER }}
                ProxyCommand ssh -W %h:%p bastion
          EOL
          chmod 600 ~/.ssh/config
      - name: Setup known_hosts
        run: |
          ssh-keyscan ${{ secrets.SPRING_APP_BASTION_HOST }} >> ~/.ssh/known_hosts
          ssh bastion "ssh-keyscan -H ${{ secrets.SPRING_APP_WEB1_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null
          ssh bastion "ssh-keyscan -H ${{ secrets.SPRING_APP_WEB2_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null
      - name: Deploy to EC2
        run: |
          scp target/demo-0.0.1-SNAPSHOT.jar web1:~/app/app.jar
          scp target/demo-0.0.1-SNAPSHOT.jar web2:~/app/app.jar
      - name: Start Application
        run: |
          ssh web1 << 'ENDSSH1'
            sudo systemctl restart qiita-spring-ec2-app
            sudo systemctl status qiita-spring-ec2-app --no-pager
          ENDSSH1
          ssh web2 << 'ENDSSH2'
            sudo systemctl restart qiita-spring-ec2-app
            sudo systemctl status qiita-spring-ec2-app --no-pager
          ENDSSH2

6. 動作確認

PRを投げる

以上の内容でPRを投げてみます。

image.png
Actionsタブから該当のワークフローを確認すると、build・testジョブが成功していることが分かります。
deployジョブは、まだPRを投げただけなので実行されていません。
また、アーティファクトにはビルド成果物とテスト結果が格納されており、ダウンロードして確認することができます。

image.png
PRを確認すると、ブランチ保護ルールとしてRequire status checks to passに設定した、build・testジョブには「Required」と表示されています。

このPRをマージすると、今度はmainブランチへのプッシュがトリガーとなり再度ワークフローが動きます。

image.png
この通り、deployジョブまで成功しています。

アプリケーションに変更を加える

image.png
「CI/CD実装済み」という文言を追加しました。

失敗例

本アプリケーションではテストコードは実装していなかったのですが、簡易的な失敗するテストコードを書き、上記変更がmainブランチに取り込まれないことを確認します。

HelloControllerTest.java
package com.example.demo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@WebMvcTest(HelloController.class)
public class HelloControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    public void indexShouldReturnIncorrectViewName() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get(""))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.view().name("wrong"));
    }
}

ルートパスにアクセスして取得するビュー名が、「wrong」であることを期待するテストを実装しました。(正しくは「hello」)

以上でPRを投げます。

image.png
ワークフローを確認すると、意図した通りtestジョブが失敗しています。

com.example.demo.HelloControllerTest.txt
-------------------------------------------------------------------------------
Test set: com.example.demo.HelloControllerTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 2.362 s <<< FAILURE! -- in com.example.demo.HelloControllerTest
com.example.demo.HelloControllerTest.indexShouldReturnIncorrectViewName -- Time elapsed: 0.962 s <<< FAILURE!
java.lang.AssertionError: View name expected:<wrong> but was:<hello>
…

アーティファクトに保存されているテスト結果を確認すると、「wrong」で期待したが、実際は「hello」だった旨のメッセージが格納されています。

image.png
また、PRを確認すると、上記失敗に伴いマージができないようになっていることが分かります。

成功例

HelloControllerTest.java
@Test
public void indexShouldReturnCorrectViewName() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.get(""))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.view().name("hello"));
}

該当のテストメソッドを修正しコミットします。

image.png
今度はtestジョブも成功しています。

image.png
また、PRもマージできる状態になっていることが分かります。

このままマージします。

image.png
ワークフローを確認すると、deployジョブまで成功しています。

また、ブラウザからアプリケーションにアクセスすると、
image.png
image.png
無事に変更後のコードが適用されていることが確認できました!

前編記事との間でEC2インスタンスを再起動しているので、ホスト名が変わっています

おわりに

以上で、GitHub Actionsを使用したCI/CDパイプラインの構築が完了しました。

アプリケーションのビルドやテスト、前編で構築したWebサーバへの接続や成果物の転送を自動化しています。

テストコードをちゃんと実装していけば、今回作成したtestジョブにより自動でテストが実行される環境が整っています。
また、静的解析ツールなどを組み込む際には、Mavenプラグインを追加してライフサイクルへ組み込み、またはワークフローから直接実行するだけで、簡単に自動化することができるようになりました。

最後に、他にも基礎的なAWSインフラ構築関連の記事を書いているので、よければご参照ください。

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