AWS CodeDeployでSpring Bootアプリケーションのデプロイ
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に関する詳細は公式ドキュメントを参照されると詳細が記載されています。
作成物
今回作成/修正/構築するものとしては以下となります。
作成物 | 説明 |
---|---|
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も修正します。 |
ソースコード
今回作成したソースコードは以下から参照できます。
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をインストールします。
今回は以下を実行しています。
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を付与する必要があります。
(ただ、どうしてこのロールが必要なのかわかっていないです。。。デプロイの際にEC2からCodeDeployに対してアクセスする必要があるのか?うーん、腹落ちしていないです。。)ロールを作成して、EC2インスタンスにアタッチしましょう。
ロールを作成
ボタンをクリックして、作成画面を開きます。
EC2にアタッチするためのロールなので、ユースケースとしてEC2
を選択します。
許可ポリシーはAmazonEC2RoleforAWSCodeDeploy
を選択します。
ロール名に任意の名称を入力して、ロールを作成します。
次に作成したロールをEC2インスタンスにアタッチします。
EC2の画面から該当のインスタンスを選択して、アクション
->セキュリティ
->IAMロールを変更
をクリックします。
IAMロール変更画面で作成ほど作成したロールを選択して、更新ボタンをクリックします。
CodeDeploy用ロールの作成
後ほどデプロイグループにロールを指定する必要があるので、公式ドキュメントにしたがって作成しましょう。
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
は以下の通りです。
詳細な仕様については公式ドキュメントを参照ください。
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
フェーズとartifacts
にappspec.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
をフック(処理を置き換える)するようにしています。
※今回は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_codedeploy
をmaster
にマージしましょう。
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
は結構参照しました。
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
記事一覧
第二回 IntelliJ IDEAを使って、Spring BootプロジェクトをGitHubにPush
第三回 Dockerコンテナ上でSpring Bootアプリケーションのビルド
第四回 AWS CodeBuildでSpring Bootアプリケーションをビルド
Discussion