SpringBootで構築したアプリケーションがgitにpushされたらCodeDeployで自動デプロイされる仕組み


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でもそんなに変わらない)

## 大まかな流れ
開発者

1. 任意の(ここでは例としてdevelop) branchにpush

Jenkins

1. 5分毎にポーリングし、develop branchが更新されていればpull
1. Test & Build(Maven)
1. Test & Buildが成功したらS3にリビジョンをアップロード

Lambda 

1. リビジョンがアップロードされるとそれをトリガーとしてLambda functionが起動
1. Lambda functionがCodeDeployにデプロイリクエストを投げる

CodeDeploy

1. CodeDeployから各インスタンスにSpringBootアプリがdeployされ、新しいバージョンのアプリが起動する


図にするとこんな感じになります。
![CI.png](https://qiita-image-store.s3.amazonaws.com/0/63643/cd406a67-086f-fa95-2fb3-816d841dc0c1.png)


## 問題点
### 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を試す](http://qiita.com/yoshidan/items/696cfa68f3dbbad8140c)

## CodeDeploy
以下を参考にCodeDeployの設定を行います。
[EC2デプロイのためにCodeDeployを導入する](http://qiita.com/yukofeb/items/e077fc8755416c904032)

appspec.ymlは今回の構成だと以下のような流れになります。

1. 起動中のSpringBoot serviceを停止する
1. Deploy("target/apps.jar"をlinuxサーバの"/var/src/"に配置する)
1. SpringBoot serviceを起動

```appspec.yml
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はそれぞれ以下の様な内容になります。

```appstop.sh
#!/bin/sh
service [:service名] stop
```

```appstart.sh
#!/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。

```bash
#!/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を作成します。  
以下、説明はソースコメントに記します。

```CodeDeoloy.java
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"に設定します。

```json
{
  "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すると複数サーバにローリングデプロイされる流れが完成です。