💭

AWS CodeDeployでSpring Bootアプリケーションのデプロイ

2022/10/22に公開

AWS CodeDeployでSpring Bootアプリケーションのデプロイ

前回の記事でGitHubで管理しているSpring Bootアプリケーションのソースをもとにビルドして、モジュールをS3バケットにアップロードしました。
(前回記事) CodeBuildでSpring Bootアプリをビルド

今回はS3バケットにあるモジュールをEC2にデプロイします。

最終的なシステム構成

前回記事にも掲載しましたが、最終的な構成は以下の通りとなります。

今回のシステム構成

今回のシステム構成です。

EC2を起動して、アプリケーションが稼働する環境を構築して、その上にCodeDeployでjarをデプロイして、サービスを再起動します。

EC2を新規に起動(作成)して、CodeDeployを使ってEC2に対してSpring Bootアプリケーションをデプロイします。
実際にデプロイ処理を実行するのはEC2です。
CodeDeployは指示を与えるのみです。
デプロイの主体がデプロイ対象であるためPull型と呼ばれています。

今回は登場するサービスが多く複雑なため、もう少し詳細な構成を以下に示しました。

CodeDeployに関する詳細は公式ドキュメントを参照されると詳細が記載されています。
https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/welcome.html

作成物

今回作成/修正/構築するものとしては以下となります。

作成物 説明
EC2インスタンス アプリケーションを稼働させるサーバ。今回はJavaのインストールなどは手動で行います。
CodeDeployアプリケーション 複数のデプロイグループを管理する器のようなものです。設定項目はデプロイ対象がEC2/Lambda/ECSのどれかということだけです。
CodeDeployデプロイグループ デプロイ対象をタグを使って管理します。今回はEC2を1台でNameタグで対象を指定します。その他、デプロイする際に最新モジュールを置き換えるのか、Blue/Greenとするのか、などのデプロイ方法に関する設定も行います。
CodeDeployデプロイ デプロイするたびに作成するものです。デプロイするアプリケーションやデプロイ仕様が記載されているappspec.ymlの保管場所はここで指定します。CodePipelineを使用する場合は、CodePipelineがデプロイを作成してくれます。
appspec.yml デプロイの仕様を記載したYaml形式のファイル。S3から取得したモジュールの配置場所、各フェーズで実行する処理を記述します。
spring-boot-demo-restart.sh 今回はアプリケーションをSystemdサービスとして常駐させます。デプロイ時にモジュールを配置した後にサービスを再起動させますが、その際に使用するシェルスクリプト。
buildspec.yml デプロイするためにS3上のモジュールにappspec.ymlと実行させるスクリプトを含める必要があるため、buildspec.ymlも修正します。

ソースコード

今回作成したソースコードは以下から参照できます。
https://github.com/ryotsuka7/spring-boot-demo/tree/v.1.0.2

EC2インスタンスを起動と環境構築

アプリケーションを稼働させるEC2インスタンスを作成して、Javaのインストール等環境構築を行います。
CodeDeploy Agentも行います。

EC2インスタンスの起動

EC2インスタンスを起動(作成)します。
ネットワーク等さまざまな要因が関連してくるため、ここでは詳細な手順は割愛します。
以下の二つの要件を満たせていれば、特に問題はないかと思います。

  • ローカルPCからsshで接続できる
  • OSはAmazon Linux2を選択する
    今回は以下を選択しています。

OSの設定とJavaのインストール

sshでEC2インスタンスに接続して、OSの設定やJavaのインストールを行います。
ec2-userユーザーで作業するため、root権限が必要なコマンドについてはsudoしています。

# ユーザ情報の確認
id

# OS情報の確認
cat /etc/system-release
cat /etc/os-release

# モジュールの最新化
sudo yum update -y

# localeの設定
date
sudo cp /usr/share/zoneinfo/Japan /etc/localtime
date

# Javaのインストール
sudo yum install -y java-17-amazon-corretto
java -version

CodeDeploy Agentのインストール

システム構成のところで簡単に説明しましたが、CodeDeployから指示を受け、デプロイを実行するためにCodeDeploy Agentをインストールする必要があります。

以下の公式ドキュメントを参考にCodeDeploy Agentをインストールします。
https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/codedeploy-agent-operations-install-linux.html

今回は以下を実行しています。

sudo yum install -y ruby

wget https://aws-codedeploy-ap-northeast-1.s3.ap-northeast-1.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto

sudo service codedeploy-agent status

EC2用のロールの作成

公式ドキュメントにも記載されていますが、EC2にRoleを付与する必要があります。
https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/instances-ec2-configure.html
(ただ、どうしてこのロールが必要なのかわかっていないです。。。デプロイの際にEC2からCodeDeployに対してアクセスする必要があるのか?うーん、腹落ちしていないです。。)

ロールを作成して、EC2インスタンスにアタッチしましょう。

ロールを作成ボタンをクリックして、作成画面を開きます。

EC2にアタッチするためのロールなので、ユースケースとしてEC2を選択します。

許可ポリシーはAmazonEC2RoleforAWSCodeDeployを選択します。

ロール名に任意の名称を入力して、ロールを作成します。

次に作成したロールをEC2インスタンスにアタッチします。
EC2の画面から該当のインスタンスを選択して、アクション->セキュリティ->IAMロールを変更をクリックします。

IAMロール変更画面で作成ほど作成したロールを選択して、更新ボタンをクリックします。

CodeDeploy用ロールの作成

後ほどデプロイグループにロールを指定する必要があるので、公式ドキュメントにしたがって作成しましょう。
https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/getting-started-create-service-role.html

IAMロールの画面からロールを作成ボタンをクリック。

CodeDeployサービスに対してアタッチするので、CodeDeployを選択します。

デフォルトのままで次へいきます。

適当な名称を入力して、作成ボタンをクリックします。

CodeDeployアプリケーションの作成

CodeDeployアプリケーションを作成していきます。

デベロッパー用ツール画面の左メニューからデプロイ - アプリケーションを選択して、アプリケーション画面を開いて、アプリケーションの作成ボタンをクリック。

適当な名称を入力して、ここではEC2にデプロイするためコンピューティングプラットフォームEC2/オンプレミスを選択して作成ボタンをクリック。

以下の通り、アプリケーションが作成されます。

CodeDeployデプロイグループの作成

続いてデプロイグループを作成します。
先ほどのアプリケーション画面の中のデプロイグループの作成をクリックします。
すると、作成画面が開くので、各種設定情報を入力していきます。

  • 名称は適当なものを入力
  • サービスロールには先ほど作成したロールのARNを入力
  • デプロイタイプは今回は単純に入れ替えるだけとするためインプレース
  • 環境設定でECインスタンスをタグで指定。Nameタグで指定。
    あとはデフォルトのままとしています。

作成ボタンをクリックすると以下の通り、デプロイグループが作成されます。

appspec.ymlの作成と再ビルド

ブランチの作成(任意)

今回も、GitHubにPush→ビルド/デプロイの実行、を試行錯誤するために何度も行います。
masterブランチを汚さないために、前回の記事を参考にdev_codedeployブランチを作成します。

appspec.ymlの作成

EC2インスタンスが実際にデプロイを行う仕様を記載するappspec.ymlを作成します。
場所は/codedeploy/appspec.ymlとしています。
buildspec.ymlはルートに置かざるを得ないのですが、appspec.ymlはビルドの際にファイルの場所を移動できるため任意の場所に作成できます。
このあとデプロイで使用するシェルスクリプトも作成するので、codedeployフォルダを切って、そこで管理します。

appspec.ymlは以下の通りです。
詳細な仕様については公式ドキュメントを参照ください。
https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/reference-appspec-file-structure.html

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ec2-user/app/
permissions:
  - object: /home/ec2-user
    pattern: "app"
    owner: ec2-user
    group: ec2-user
    type:
      - directory
  - object: /home/ec2-user/app
    pattern: "**"
    owner: ec2-user
    group: ec2-user
    mode: 444
    type:
      - file

filesでは、S3から取得したアプリケーションファイル(zipファイル)を解凍したものを配置する場所を指定しています。

permissionsは指定しないと配置されるファイルは全て属性がrootユーザ、rootグループとなってしまうので、ec2-userを指定しています。
これは特に指定しなくとも問題はないのですが、/home/ec2-user配下に配置するのにrootがオーナーとなるのが気持ち悪いので、お作法的に指定しています。

buildspec.ymlの修正

ビルド時にS3にアップロードするファイルにappspec.ymlを含めるためにbuildspec.ymlを修正します。
post_buildフェーズとartifactsappspec.ymlに関する記述を追記しています。

  post_build:
    commands:
      - echo start post build.

      # S3にアップロードするファイルを所定のディレクトリにコピー
      - mkdir artifacts
      - cp target/spring-boot-demo-0.0.1-SNAPSHOT.jar artifacts
      - cp codedeploy/appspec.yml artifacts

      - echo finish post build.

artifacts:
  # S3にアップロードするファイルを指定
  files:
    - spring-boot-demo-0.0.1-SNAPSHOT.jar
    - appspec.yml
  # ベースディレクトリ
  base-directory: artifacts

動作確認するためにGitHubにPushします。

CodeBuildの再実行

現在S3にあがっているモジュールにはappspec.ymlは含まれていません。
デプロイの動作確認するために、再度CodeBuildでビルドを実行します。

開発用ブランチで作業していますので、ブランチを指定します。
ソースソースバージョンdev_codedeployに修正します。

ビルドを再実行します。
終了したら、S3に生成されたファイルをダウンロードして、解凍して、appspec.ymlが含まれていることを確認します。

CodeDeployデプロイの作成

デプロイする準備が整いましたので、実行してみましょう。
CodeDeployデプロイを作成するとデプロイが実行されます。

デプロイ対象のEC2は起動しておいてください。

先ほど作成したデプロイグループの画面を開いて、デプロイの作成ボタンをクリックします。

デプロイに関する設定を指定します。

  • アプリケーション(zipファイル)の取得先として、S3とGitHubが選択できますが、S3にビルドしたモジュールを用意していますので、S3を選択して、zipファイルの場所を指定します。
    なお、Pythonなどビルドが不要な言語のデプロイを行う場合は、GitHubを指定して使用するケースが多いかと思います。
  • リビジョンファイルの種類は.zipを指定
    他の設定はデフォルトとしています。
    作成ボタンをクリックします。

デプロイが作成されると、自動でデプロイが実行され、以下の通りステータスが進行中となります。

問題がなければ、ステータスが成功に変わります。

結果確認

EC2にモジュールが配置されているか確認してみましょう。
EC2にsshでログインして以下のコマンドを実行します。
/home/ec2-user/app配下にappspec.ymlやjarファイルが配置されていれば成功です。

ls -l
ls -l app

Spring Bootアプリケーションを手動で起動してみましょう。
以下の通り実行してアプリケーションが起動していることを確認します。

$ java -jar app/spring-boot-demo-0.0.1-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.4)

2022-10-19 11:23:41.863  INFO 11929 --- [           main] c.e.s.SpringBootDemoApplication          : Starting SpringBootDemoApplication v0.0.1-SNAPSHOT using Java 17.0.4.1 on ip-10-X-X-X.ap-northeast-1.compute.internal with PID 11929 (/home/ec2-user/app/spring-boot-demo-0.0.1-SNAPSHOT.jar started by ec2-user in /home/ec2-user)
2022-10-19 11:23:41.867  INFO 11929 --- [           main] c.e.s.SpringBootDemoApplication          : No active profile set, falling back to 1 default profile: "default"
2022-10-19 11:23:43.493  INFO 11929 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-10-19 11:23:43.551  INFO 11929 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-10-19 11:23:43.552  INFO 11929 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.65]
2022-10-19 11:23:43.698  INFO 11929 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-10-19 11:23:43.699  INFO 11929 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1700 ms
2022-10-19 11:23:44.359  INFO 11929 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-10-19 11:23:44.378  INFO 11929 --- [           main] c.e.s.SpringBootDemoApplication          : Started SpringBootDemoApplication in 3.208 seconds (JVM running for 4.054)

別ターミナルを開いてEC2にssh接続してAPIを実行してみましょう。
curlでGETメソッドを発行して、jsonが返却されれば、成功です。

$ curl localhost:8080/hello
{"id":100,"name":"taro"}

アプリケーションのサービス化

手動でアプリケーションを起動して動作確認しましたが、ターミナルを閉じるとアプリケーションも終了されてしまいます。
そこで、アプリケーションを常駐できるようにサービス化しましょう。

systemdによるサービス化

サービス化にはsystemdという仕組みを使います。
ここでは詳細は割愛しますが、サービスの仕様を記載して、サービスを起動します。

まずはサービスの仕様を所定のファイルに記述します。

sudo vi /usr/lib/systemd/system/spring-boot-demo.service

ファイルには以下の内容を記述します。

[Unit]
Description=spring boot demo
After=syslog.target

[Service]
User=ec2-user
ExecStart=/usr/bin/java -jar /home/ec2-user/app/spring-boot-demo-0.0.1-SNAPSHOT.jar
SuccessExitStatus=143

[Install]
WantedBy=multi-user.target

サービスの仕様をsystemdに読み込ませるためにreloadという処理を行います。
その後、サービスを起動して、状態を確認しましょう。

sudo systemctl daemon-reload
sudo systemctl start spring-boot-demo
sudo systemctl status spring-boot-demo

動作確認

今回はバックグラウンドで実行されているので、ターミナルを新しく開く必要がなく、同じターミナルでAPIを実行してみましょう。
curlでGETメソッドを発行して、jsonが返却されれば、成功です。

$ curl localhost:8080/hello
{"id":100,"name":"taro"}

再起動するスクリプトを作成

デプロイする際に配備したモジュールに入れ替えるために、サービスを再起動するように修正します。
サービスを再起動するシェルスクリプトdeploy/spring-boot-demo-restart.shファイルを作成します。

#!/bin/sh

systemctl restart spring-boot-demo

デプロイ時にサービス再起動するように設定

appspec.ymlを修正して、モジュール配備後にアプリケーションを起動するように記述します。

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ec2-user/app/
permissions:
  - object: /home/ec2-user
    pattern: "app"
    owner: ec2-user
    group: ec2-user
    type:
      - directory
  - object: /home/ec2-user/app
    pattern: "**"
    owner: ec2-user
    group: ec2-user
    mode: 444
    type:
      - file
hooks:
  ApplicationStart:
    - location: spring-boot-demo-restart.sh
      timeout: 300

hooks - ApplicationStartを追加しています。
CodeDeployでは以下の流れで各フェーズの処理が実行されます。
Installで実際のファイルの配備が行われます。
サービス再起動はその後に行いたいため、ApplicationStartをフック(処理を置き換える)するようにしています。
https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/reference-appspec-file-structure-hooks.html#appspec-hooks-server
※今回はLBは使っていないので、左図となります。

ビルド時にシェルスクリプトもzipに含めるように設定を変更

deploy/spring-boot-demo-restart.shファイルをCodeDeployが読めるようにbuildspec.ymlのartifactsに追加します。
そうすることでS3上のzipファイルにシェルスクリプトも含まれ、これも含めCodeDeployが取得するようになります。

version: 0.2

phases:
  pre_build:
    commands:
      - echo start pre build.

      # ログインユーザーの確認
      - id
      # OS情報の確認
      - cat /etc/system-release
      - cat /etc/os-release
      # 各種パッケージの最新化
      - yum update -y
      # タイムゾーンの変更
      - date
      - cp /usr/share/zoneinfo/Japan /etc/localtime
      - date
      # Javaのインストール
      - yum install -y java-17-amazon-corretto
      - java -version
      - /usr/sbin/alternatives --set java /usr/lib/jvm/java-17-amazon-corretto.aarch64/bin/java
      - java -version
      - /usr/sbin/alternatives --display java
      # JAVA_HOME環境変数の設定
      - export JAVA_HOME=/usr/lib/jvm/java-17-amazon-corretto.aarch64
      # Mavenのインストール
      - wget https://repos.fedorapeople.org/repos/dchen/apache-maven/epel-apache-maven.repo -O /etc/yum.repos.d/epel-apache-maven.repo
      - sed -i s/\$releasever/7/g /etc/yum.repos.d/epel-apache-maven.repo
      - sed -i s/\$basearch/x86_64/g /etc/yum.repos.d/epel-apache-maven.repo
      - yum install -y apache-maven
      - mvn -version

      - echo finish pre build.

  build:
    commands:
      - echo start build.

      # Spring Bootプロジェクトのビルド(jarの作成)
      - mvn package
      - ls -l target

      - echo finish build.

  post_build:
    commands:
      - echo start post build.

      # S3にアップロードするファイルを所定のディレクトリにコピー
      - mkdir artifacts
      - cp target/spring-boot-demo-0.0.1-SNAPSHOT.jar artifacts
      - cp codedeploy/appspec.yml artifacts
      - cp codedeploy/spring-boot-demo-restart.sh artifacts

      - echo finish post build.

artifacts:
  # S3にアップロードするファイルを指定
  files:
    - spring-boot-demo-0.0.1-SNAPSHOT.jar
    - appspec.yml
    - spring-boot-demo-restart.sh
  # ベースディレクトリ
  base-directory: artifacts

動作確認用にAPIを修正

手動でデプロイして、サービスを起動している状態に対して、動作確認を行うために、APIから返却する値を変更します。
Controllerの該当箇所を修正してNameで返却する値をjiroに変更します。
確認するために、EC2上のモジュールのタイムスタンプや再起動が行われたかsyslogなど確認しても良いですが、この方法が確実かと思います。

    @GetMapping
    public Sample hello(){
        Sample sample = new Sample();
        sample.setId(100);
        sample.setName("jiro");  // <- ここを修正

        return  sample;
    }

このままではテストの際に期待値とテスト結果が一致しないために、CodeBuildでエラーとなってしまいます。
テストコードの期待値も変更しましょう。

	@Test
	void contextLoads() throws  Exception{
		// JavaのObjectをJSONに変換するためのクラスを生成
		ObjectMapper objectMapper = new ObjectMapper();

		// 結果を検証するためのクラスを生成して、期待値をセット
		Sample sample = new Sample();
		sample.setId(100);
		sample.setName("jiro");  // <- ここを修正

		// 「/hello」パスのAPIを実行してレスポンスを検証
		this.mockMvc.perform(MockMvcRequestBuilders.get("/hello"))
				.andDo(MockMvcResultHandlers.print())
				.andExpect(status().isOk())
				.andExpect(content().json(objectMapper.writeValueAsString(sample)));
	}

Controller、テストクラス、appspec.yml、buildspec、spring-boot-demo-restart.shGitHubにPushしましょう。

動作確認

CodeBuildを再実行して、S3上のモジュールを最新化します。
CodeDeployを再実行して、デプロイとサービス再起動を行います。

モジュールが最新化され、サービスが再起動されていることを確認しましょう。
sshでEC2に接続して、curlでGETメソッドを発行して、返却されるjsonが変更されていればOKです。

$ curl localhost:8080/hello
{"id":100,"name":"jiro"}

ブランチのマージ

以上でCodeDeploy環境の構築は完了です。
開発用のブランチdev_codedeploymasterにマージしましょう。
CodeBuildで参照するブランチもmasterに変更します。
この辺の手順は前回の記事で説明していますので、そちらを参照ください。

まとめ

かなり長い記事となりましたが、重要な点は以下となります。

  • 構成図で示した全体の流れ
  • appspec.ymlの仕様
    • アプリケーションの配置
    • 配置前後に処理を実行するためのフック

上記を押さえておけば、全体が見渡せるかと思います。
その上で本番運用するためのBlue/Green等の詳細設計を行うことをお勧めします。

Tips: CodeDeployのデバッグ

今回はじめてCodeDeployを使用しましたが、かなりハマりました。。。
ハマりポイントとしては、

  • 設定を変更して再度実行するためにCodeBuildでのビルドからやるのですが、時間がかかって効率が悪いかったです。
    ただ、本番運用する際にビルド時間の短縮をするべきか、というと状況次第かと思います。
    効率が悪いのはCI/CD環境構築時だけであれば、そのままでも良いのではないかと思います。
    ビルド時間を短縮するに越したことはありませんが、それなりに工数がかかると思いますので。
  • ログがわかりづらい
  • CodeDeploy Agentで諸々の情報をキャッシュしているので予期せぬエラーが発生

CodeDeployのログについて

以下のドキュメントを参照して、errorなどのキーワードで検索して原因を調査しました。
/opt/codedeploy-agent/deployment-root/deployment-group-ID/deployment-ID/logs/scripts.logは結構参照しました。
https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/deployments-view-logs.html#deployments-view-logs-instance-unix

CodeDeployのキャッシュのクリア

デプロイが中途半端な状態でエラーとなった場合や手動でデプロイされたファイルを移動、削除したりするとエラーとなることがあります。
その場合は、キャッシュをクリアして、配置されたアプリケーションを削除してまっさらな状態にしていました。
その際に実行したコマンドです。

# CodeDeploy Agentを停止
sudo service codedeploy-agent stop
# CodeDeployのキャッシュや前回の作業ファイルなどを削除
sudo rm -rf /opt/codedeploy-agent/deployment-root/*
# CodeDeploy Agentを起動
sudo service codedeploy-agent start

# 配置された前回のアプリケーションを削除
sudo rm -rf /home/ec2-user/app

記事一覧

第一回 Spring Bootを使ったWebAPIの作成

第二回 IntelliJ IDEAを使って、Spring BootプロジェクトをGitHubにPush

第三回 Dockerコンテナ上でSpring Bootアプリケーションのビルド

第四回 AWS CodeBuildでSpring Bootアプリケーションをビルド

第五回 AWS CodeDeployでSpring Bootアプリケーションのデプロイ

第六回 CodePipelineでSpring BootアプリをCI/CD

Discussion