SpringBootで構築したアプリケーションがgitにpushされたら自動でデプロイされる仕組みを構築した記録です。
APサーバはEC2を使用していますが、オンプレミスのサーバでもCodeDeploy Agentをインストールして適切なNW設定を行うことにより同様のことが可能です。
構成
- Gitリポジトリ
- GitLab(なんでも良い)
- CI
- Jenkins
- Test & Build
- Maven(Gradleでもそんなに変わらない)
- APサーバ
- SpringBoot 1.3.x on EC2
- Lambda function
- Java8(node.jsとかPythonでもそんなに変わらない)
大まかな流れ
開発者
- 任意の(ここでは例としてdevelop) branchにpush
Jenkins
- 5分毎にポーリングし、develop branchが更新されていればpull
- Test & Build(Maven)
- Test & Buildが成功したらS3にリビジョンをアップロード
Lambda
- リビジョンがアップロードされるとそれをトリガーとしてLambda functionが起動
- Lambda functionがCodeDeployにデプロイリクエストを投げる
CodeDeploy
- CodeDeployから各インスタンスにSpringBootアプリがdeployされ、新しいバージョンのアプリが起動する
問題点
SpringBootはアプリのjarファイル自体がAPサーバとして実行されるので、デプロイ時にサーバ(jar)のstop/sartが必要
Tomcatにwarをデプロイする場合だとそのままwarファイルを置けばいいし、restartする場合でもサービスとして起動しているのでコマンド一発で楽ちん。
それに対してSpringBootの組み込みサーバを利用した場合、実行されているjarファイルごと置き換えるので、プロセスを探して停止、新しいjarを起動みたいにしないといけない。
これはSpringBoot1.3以降で利用できるFully Executable Jarsを作成、SpringBootアプリをserviceとして起動することで解決しました。
必要な作業
- アプリ
- SpringBootアプリをservice起動するためにmavenもしくはgradle設定でFully Executable Jarsを作成できるように設定
- CodeDeploy
- APサーバにCodeDeploy Agentをインストール
- EC2インスタンスにIAMロールを紐付ける
- CodeDeploy設定
- appspec.ymlの作成
- 各フェーズで呼び出されるshell scriptの作成
- Jenkins
- gitポーリング設定
- Test & Build
- S3にリビジョンをアップロードするshell script
- Lambda
- CodeDeploy起動用Lambda function
- 実行権限の設定
- Triggerの設定
[アプリ]SpringBootアプリをservice起動するためにmavenもしくはgradle設定でFully Executable Jarsを作成できるように設定
SpringBoot1.3以降で利用できるFully Executable Jarsを作成します。
参照: Spring BootのFully Executable Warを試す
CodeDeploy
以下を参考にCodeDeployの設定を行います。
EC2デプロイのためにCodeDeployを導入する
appspec.ymlは今回の構成だと以下のような流れになります。
- 起動中のSpringBoot serviceを停止する
- Deploy("target/apps.jar"をlinuxサーバの"/var/src/"に配置する)
- SpringBoot serviceを起動
version: 1.0.0
os: linux
files:
- source: target/apps.jar
destination: /var/src/
overwrite: yes
hooks:
ApplicationStop:
- location: scripts/appstop.sh
timeout: 300
runas: root
ApplicationStart:
- location: scripts/appstart.sh
timeout: 300
runas: root
SpringBootアプリをserviceとして起動しているので、scripts/appstop.shおよびscripts/appstart.shはそれぞれ以下の様な内容になります。
#!/bin/sh
service [:service名] stop
#!/bin/sh
service [:service名] start
[Jenkins]gitポーリング設定
詳細はググればすぐ出てくるので割愛しますが、5分間隔でgitリポジトリをポーリングします。
[Jenkins]Test & Build
MavenなりGradleでTest & Buildします。
[Jenkins]S3にリビジョンをアップロードする
aws deploy pushでjarファイルをS3にアップロードします。
ビルドの設定でシェルの実行を選択し、以下のようにします。
ここではJenkinsサーバにIAMのキーペアを持たせたくなかったので環境変数に一時的にキーペアをセットしています。
application-nameとs3-locationを指定してdeploy。
#!/bin/bash
export AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXX
export AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
export AWS_DEFAULT_REGION=ap-northeast-1
export AWS_DEFAULT_OUTPUT=json
cd ${WORKSPACE}
aws deploy push --application-name [:applicationName] --s3-location s3://[:bucketName]/[:key] --source ./
[Lambda]CodeDeploy起動用Lambda function(Java)
S3にリビジョンがアップロードされたらAPサーバにjar(zip)ファイルをデプロイするLambda functionを作成します。
以下、説明はソースコメントに記します。
import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.codedeploy.AmazonCodeDeploy;
import com.amazonaws.services.codedeploy.AmazonCodeDeployClient;
import com.amazonaws.services.codedeploy.model.BundleType;
import com.amazonaws.services.codedeploy.model.CreateDeploymentRequest;
import com.amazonaws.services.codedeploy.model.RevisionLocation;
import com.amazonaws.services.codedeploy.model.RevisionLocationType;
import com.amazonaws.services.codedeploy.model.S3Location;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.S3Event;
import com.amazonaws.services.s3.event.S3EventNotification.S3Entity;
import com.amazonaws.services.s3.event.S3EventNotification.S3ObjectEntity;
/**
* S3にリビジョンがアップロードされたらAPサーバにjar(zip)ファイルをデプロイする。<br />
* TriggerはObjectCreatedByCompleteMultipartUpload(ObjectPutではない)。
*
* @author ryosukehayashi
*
*/
public class CodeDeoloy implements RequestHandler<S3Event, Object> {
/**
* handler
*/
public Object handleRequest(S3Event event, Context context) {
S3Entity s3Entity = event.getRecords().get(0).getS3();
S3ObjectEntity objectEntity = s3Entity.getObject();
// 任意。ここでは汎用的に使うため、S3のkeyからアプリケーション名を取得するようにしている
String applicationName = getApplicationName(objectEntity.getKey());
// 任意。ここではバケット名をデプロイメントグループ名に設定
String deploymentGroupName = s3Entity.getBucket().getName();
String description = String.format("Deployed by LambdaDeployFunction %s",
DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSSxxxxx zzz")
.format(ZonedDateTime.now(ZoneId.of("JST", ZoneId.SHORT_IDS))));
// リビジョンのアップロード先(S3)
S3Location s3Location = new S3Location();
s3Location.setBucket(s3Entity.getBucket().getName());
s3Location.setKey(objectEntity.getKey());
s3Location.setETag(objectEntity.geteTag());
s3Location.setVersion(objectEntity.getVersionId());
// jarファイルの場合は"zip"で良い
s3Location.setBundleType(BundleType.Zip);
RevisionLocation revision = new RevisionLocation();
// リビジョンの種類を指定。S3の他にGitHubが指定できる。
revision.setRevisionType(RevisionLocationType.S3);
revision.setS3Location(s3Location);
// デプロイリクエスト
CreateDeploymentRequest createDeploymentRequest = new CreateDeploymentRequest();
createDeploymentRequest.setApplicationName(applicationName);
// CodeDeployDefault.OneAtATime: 一台ずつデプロイ
// CodeDeployDefault.AllAtOnce: 一気に全台デプロイ
// CodeDeployDefault.HalfAtATime: 半分ずつデプロイ
createDeploymentRequest.setDeploymentConfigName("CodeDeployDefault.OneAtATime");
createDeploymentRequest.setDeploymentGroupName(deploymentGroupName);
createDeploymentRequest.setRevision(revision);
createDeploymentRequest.setDescription(description);
AmazonCodeDeploy codeDeploy = new AmazonCodeDeployClient();
codeDeploy.setRegion(Region.getRegion(Regions.AP_NORTHEAST_1));
// デプロイリクエストを投げる
codeDeploy.createDeployment(createDeploymentRequest);
return null;
}
}
[Lambda]実行権限を設定
以下の様な内容のpolicyをattachしたroleを作成し、Lambda functionの"Existing role"に設定します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["logs:*"],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": ["codedeploy:*"],
"Resource": ["*"]
}
]
}
[Lambda]Trigger設定
作成したLambda functionのTriggerを設定します。
注意点として、aws deploy pushでアップロードされた時のイベントはObjectPutではなくObjectCreatedByCompleteMultipartUploadになるので、Event typeに"ObjectCreatedByCompleteMultipartUpload"を設定します。
以上で、開発者が任意のブランチにpushすると複数サーバにローリングデプロイされる流れが完成です。