JenkinsとECSとSpot Fleetを使ってコンテナ上でJenkins Jobを実行する

## 背景
クラウドサービスではインスタンスが時間課金のため、JenkinsなどのCIサーバを起動すると利用しない時間は無駄なコストが発生してしまいます。
コンバートなどの高負荷ジョブを実行している場合はインスタンスタイプも大きなものを用意する必要があるため、AWS Batchを利用したくなります。
[AWS Batch – AWSでバッチ処理ジョブを実行する](https://aws.amazon.com/jp/blogs/news/aws-batch-run-batch-computing-jobs-on-aws/)
ですが、AWS Batchはプレビューのため、申請しないと使うことができません。
そんな時、以下の二つの記事
[Jenkinsのジョブの実行環境にAmazon EC2 Container Service ( ECS ) を活用](http://haws.haw.co.jp/tech/jenkins-job-on-ecs/)
[Spot FleetでAmazon ECSクラスタを強力に](http://aws.typepad.com/sajp/2016/01/powering-your-amazon-ecs-clusters-with-spot-fleet.html)
を見つけたので、JenkinsとECSとSpot Fleetをうまく組み合わせられないかということで試してみました。

本記事で利用しているCFnテンプレートはGitHubで公開しています。
https://github.com/shigewa/jenkins-using-ecs-and-spotfleet

## aws cliのバージョン確認

```
aws --version
aws-cli/1.11.32 Python/2.7.10 Darwin/15.6.0 botocore/1.4.89
```



## 構成概要

![Jenkins&ECS&SpotFleet](https://github.com/shigewa/jenkins-using-ecs-and-spotfleet/blob/master/images/diagram.png "Jenkins&ECS&SpotFleet")

## Jenkinsインスタンス構築前の準備

### SpotFleetが利用するIAMロール作成

* SpotFleet初回利用時にデフォルトで作成されるIAMロールが存在するか確認

``` 
aws iam get-role \
         --role-name "aws-ec2-spot-fleet-role"
```
存在する場合は `JenkinsインスタンスとECS Clusterが利用するIAMロールの作成` に進んでください。

* デフォルトIAMロールを作成

```
aws cloudformation --region ap-northeast-1 create-stack \
--stack-name SpotFleetRole \
--capabilities CAPABILITY_NAMED_IAM \
--template-body file://create-spotfleet-role.yaml
```

* ロールのARNを確認

```
SPOT_FLEET_ROLE_ARN=$( \
  aws iam get-role \
         --role-name "aws-ec2-spot-fleet-role" \
         --query "Role.Arn" \
         --output text \
) \
  && echo "${SPOT_FLEET_ROLE_ARN}"
```

### JenkinsインスタンスとECS Clusterが利用するIAMロール、インスタンスプロファイルの作成
* IAMロール名の決定

```
JENKINS_ROLE_NAME='Jenkins_Instance_Role'
```

* 同じ名前のロールがないことを確認
既に存在する場合は別名にするもしくは `IAMロールの確認作業` までスキップしてください。

```
aws iam get-role \
         --role-name ${JENKINS_ROLE_NAME}
```

* IAMロールポリシーの決定
管理ポリシーを確認します。

```
aws iam list-policies \
        --scope AWS \
        --max-items 1000 \
        --query 'Policies[].PolicyName'
```

利用するポリシーを決めます。
以下は設定例です。

```
IAM_POLICY_NAME1='AmazonEC2FullAccess'
IAM_POLICY_NAME2='AmazonEC2ContainerServiceFullAccess'
```

* IAMロールにアタッチするポリシーのARNを取得

```
IAM_POLICY_ARN1=$( \
  aws iam list-policies \
    --max-items 1000 \
    --query "Policies[?PolicyName==\`${IAM_POLICY_NAME1}\`].Arn" \
    --output text \
) \
  && echo "${IAM_POLICY_ARN1}"
```
```
IAM_POLICY_ARN2=$( \
  aws iam list-policies \
    --max-items 1000 \
    --query "Policies[?PolicyName==\`${IAM_POLICY_NAME2}\`].Arn" \
    --output text \
) \
  && echo "${IAM_POLICY_ARN2}"
```

```
aws cloudformation --region ap-northeast-1 create-stack \
--stack-name ${JENKINS_ROLE_NAME} \
--capabilities CAPABILITY_NAMED_IAM \
--template-body file://create-jenkins-instance-role.yaml \
--parameters \
ParameterKey=AttachJenkinsRoleARNs,ParameterValue="${IAM_POLICY_ARN1}\,${IAM_POLICY_ARN2} \
ParameterKey=RoleName,ParameterValue="{JENKINS_ROLE_NAME}"
```

* 作成したIAMロールとインスタンスプロファイルのARNを確認

```
JENKINS_INSTANCE_PROFILE_ARN=$( \
  aws iam list-instance-profiles \
  --query "InstanceProfiles[?contains(Arn,\`${JENKINS_ROLE_NAME}\`)].Arn" \
  --output text \
  )
  && echo "${JENKINS_INSTANCE_PROFILE_ARN}"
```
```
JENKINS_ROLE_ARN=$( \
  aws iam list-instance-profiles \
  --query "InstanceProfiles[?contains(Arn,\`${JENKINS_ROLE_NAME}\`)].Roles[].Arn" \
  --output text \
  )
  && echo "${JENKINS_ROLE_ARN}"
```
結果が返却されない場合はCFnがスタック生成中の可能性がありますので、時間を置いて再実行してみてください。

## Jenkinsインスタンスの構築
* 以下の変数は実行環境に応じて修正してください。

```
REAGION="ap-northeast-1"
VPC_ID="hogehoge"
JENKINS_MASRER_SUBNET="fugafuga"
JENKINS_SLAVE_SUBNET='hoge\,fuga'
IAM_FLEET_ROLE_ARN=$( \
  aws iam get-role \
         --role-name "aws-ec2-spot-fleet-role" \
         --query "Role.Arn" \
         --output text \
) \
IAM_INSTANCE_PROFILE_ARN=$( \
  aws iam list-instance-profiles \
  --query "InstanceProfiles[?contains(Arn,\`${JENKINS_ROLE_NAME}\`)].Arn" \
  --output text \
  )
KEY_NAME="Jenkins_Using_ECS_and_SpotFleet"
MASTER_JENKINS_INSTNCE_TYPE="t2.micro"
SLAVE_JENKINS_SPOT_INSTANCE_TYPE="c3.large"
# Amazon Linux
MASTER_JENKINS_AMIID="ami-0c11b26d"
# ECS Optimized Amazon linux
SLAVE_JENKINS_AMIID="ami-08f7956f"
SLAVE_CAPACITY=1
MAX_SPOT_PRICE=0.128
```

* Master Slave Jenkinsが通信するSGを作成

```
aws cloudformation --region ${REAGION} create-stack \
--stack-name JenkinsSG \
--template-body file://create-jenkins-sg.yaml \
--parameters ParameterKey=VpcId,ParameterValue=${VPC_ID}
```

* Master Jenkinsインスタンスを作成
デフォルトでアタッチするSG等がある場合は `create-jenkins-master-instance.yaml `の `SecurityGroupIds`を修正してください

```
aws cloudformation --region ${REAGION} create-stack \
--stack-name JenkinsMasterStack \
--template-body file://create-jenkins-master-instance.yaml \
--parameters \
ParameterKey=VpcId,ParameterValue=${VPC_ID} \
ParameterKey=SubnetId,ParameterValue=${JENKINS_MASRER_SUBNET} \
ParameterKey=KeyName,ParameterValue=${KEY_NAME} \
ParameterKey=AmiId,ParameterValue=${MASTER_JENKINS_AMIID} \
ParameterKey=InstanceType,ParameterValue=${MASTER_JENKINS_INSTNCE_TYPE}
```

* Slave Jenkins用のインスタンスとECS Clusterを作成
デフォルトでアタッチするSG等がある場合は `create-spotfleet-jenkins-slave.yaml `の `SecurityGroups`を修正してください

```
aws cloudformation --region ${REAGION} create-stack \
--stack-name SpotFleetJenkinsSlave \
--template-body file://create-spotfleet-jenkins-slave.yaml \
--parameters \
ParameterKey=MaxSpotPrise,ParameterValue=${MAX_SPOT_PRICE} \
ParameterKey=TargetCapacity,ParameterValue=${SLAVE_CAPACITY} \
ParameterKey=InstanceType,ParameterValue=${SLAVE_JENKINS_SPOT_INSTANCE_TYPE} \
ParameterKey=SpotFleetAMI,ParameterValue=${SLAVE_JENKINS_AMIID} \
ParameterKey=KeyName,ParameterValue=${Diffchecker_Jenkins} \
ParameterKey=IamFleetRoleARN,ParameterValue=${IAM_FLEET_ROLE_ARN} \
ParameterKey=SubnetId,ParameterValue=${JENKINS_SLAVE_SUBNET}
```

### Jenkinsの初期設定

構築されたJenkins masterインスタンスにアクセスし、画面表示どおり `/var/lib/jenkins/secrets/initialAdminPassword` に書かれたパスワードを入力します。
特にこだわりの設定がない場合は「Install suggested plugins」を選択肢、デフォルトのプラグインをインストールします。
adminユーザー情報を入力します。

### Jenkins pluginの導入

以下の手順に従ってECS pluginを導入し、指定するECS Clusterは上記で作成したECS Clusterを指定してください。
https://wiki.jenkins-ci.org/display/JENKINS/Amazon+EC2+Container+Service+Plugin


### 環境の削除

```
aws cloudformation --region ${REAGION} delete-stack \
--stack-name SpotFleetJenkinsSlave

aws cloudformation --region ${REAGION} delete-stack \
--stack-name JenkinsMasterStack

aws cloudformation --region ${REAGION} delete-stack \
--stack-name JenkinsSG

aws cloudformation --region ${REAGION} delete-stack \
--stack-name SpotFleetRole
```

### 備考

SpotFleetを利用しているため、 `MAX_SPOT_PRICE` が低すぎる場合はスポットインスタンスが起動できません。


#### 参考URL
[Jenkins Amazon EC2 Container Service Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Amazon+EC2+Container+Service+Plugin)
[AWSで継続的インテグレーション 〜 Jenkins編 | アドカレ2013 : CFn #9](http://dev.classmethod.jp/cloud/aws/cfn-jenkins/)
[Jenkinsのジョブの実行環境にAmazon EC2 Container Service ( ECS ) を活用](http://haws.haw.co.jp/tech/jenkins-job-on-ecs/)
[[JAWS-UG CLI] IAM:#22 IAMロールの作成 (ECSコンテナインスタンス)](http://qiita.com/tcsh/items/8daed4cf04d5bc56e2a7)
[Spot FleetでAmazon ECSクラスタを強力に](http://aws.typepad.com/sajp/2016/01/powering-your-amazon-ecs-clusters-with-spot-fleet.html)