Spring Boot を題材に IBM Cloud で CI/CD

本エントリーは IBM Cloud Advent Calendar 2018 の15日目になります.

今回は Spring Boot アプリを題材に、IBM Cloud の Toolchain を用いて CI/CD を試してみたいと思います. 今回作る CI/CD のイメージは以下のようになります.
 

 
ポイントは以下のとおりです.

  • Cloud Foundry を用いて Nexus リポジトリを建てる
  • Toolchain で GitHub 上の Common モジュールを Nexus リポジトリに公開する
  • Toolchain で GitHub 上の REST API モジュールを Cloud Foundry にデプロイする

題材の Spirng Boot アプリ

今回はこちらのアプリを題材にします.


Common モジュールとして、フィボナッチ数列を計算する FibonacciSequence を作成しました.

package com.project_respite.ibmcloud_cicd.common;

import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;

public class FibonacciSequence {

    private static final BigInteger ZERO = BigInteger.ZERO;
    private static final BigInteger ONE = BigInteger.ONE;
    private static final BigInteger TWO = ONE.add(ONE);

    private FibonacciSequence(){}

    private static final Map FIBONACCI_MAP = new HashMap<>();
    static {
        FIBONACCI_MAP.put(ZERO, ZERO);
        FIBONACCI_MAP.put(ONE, ONE);
    }

    public static BigInteger nary(BigInteger n) {
        return FIBONACCI_MAP.computeIfAbsent(n, key -> nary(n.subtract(ONE)).add(nary(n.subtract(TWO))));
    }
}

 
REST API モジュールとして、 /fibonacci エンドポイントを実装しています.
REST API の中では Common モジュールの FibonacciSequence を呼び出しています.

package com.project_respite.ibmcloud_cicd.api;

import com.project_respite.ibmcloud_cicd.common.FibonacciSequence;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigInteger;

@RestController
public class SampleController {

    @GetMapping("/fibonacci")
    public String getHelloPage(@RequestParam(name = "nary") String nary) {
        return "" + FibonacciSequence.nary(new BigInteger(nary));
    }
}

 

Nexus リポジトリの作成

今回は Docker イメージを用いて、Cloud Foundry アプリとして Nexus を作成します.

$ ibmcloud cf push santea-nexus -m 1024M -k 2048M -t 180 --docker-image sonatype/nexus
'cf push santea-nexus -m 1024M -k 2048M -t 180 --docker-image sonatype/nexus' を起動しています...

Pushing app santea-nexus to org Santea / space dev as daiki.kawanuma@gmail.com...
Getting app info...
Creating app with these attributes...
+ 名前:                   santea-nexus
+ docker image:           sonatype/nexus
+ disk quota:             2G
+ health check timeout:   180
+ メモリー:               1G
  routes:
+   santea-nexus.mybluemix.net

Creating app santea-nexus...
Mapping routes...

Staging app and tracing logs...
   Cell bd7eb85f-27d5-4a51-9c74-a92b53d0a065 successfully created container for instance b82adad5-6a33-4407-9a59-339fc3b54e1c
   Staging...
   Staging process started ...
   Staging process finished
   Exit status 0
   Staging Complete
   Cell bd7eb85f-27d5-4a51-9c74-a92b53d0a065 stopping instance b82adad5-6a33-4407-9a59-339fc3b54e1c
   Cell bd7eb85f-27d5-4a51-9c74-a92b53d0a065 destroying container for instance b82adad5-6a33-4407-9a59-339fc3b54e1c
   Cell bd7eb85f-27d5-4a51-9c74-a92b53d0a065 successfully destroyed container for instance b82adad5-6a33-4407-9a59-339fc3b54e1c

Waiting for app to start...

名前:                   santea-nexus
要求された状態:         started
インスタンス:           1/1
使用:                   1G x 1 instances
routes:                 santea-nexus.mybluemix.net
最終アップロード日時:   Tue 13 Nov 12:19:53 JST 2018
スタック:               cflinuxfs2
docker image:           sonatype/nexus
start command:          /bin/sh -c ${JAVA_HOME}/bin/java   -Dnexus-work=${SONATYPE_WORK} -Dnexus-webapp-context-path=${CONTEXT_PATH}   -Xms${MIN_HEAP} -Xmx${MAX_HEAP}   -cp 'conf/:lib/*'   ${JAVA_OPTS}  
                        org.sonatype.nexus.bootstrap.Launcher ${LAUNCHER_CONF}

     状態   開始日時               CPU      メモリー       ディスク       詳細
#0   実行   2018-11-13T03:20:44Z   200.3%   525.7M of 1G   511.5M of 2G   

Cloud Foundry コマンドを解説します.

$ ibmcloud cf push santea-nexus -m 1024M -k 2048M -t 180 –docker-image sonatype/nexus

  • -m: メモリ割り当て量. 今回は1024MBを指定.
  • -k: ディスク割り当て量. 今回は2048MBを指定. 最大量は2048MB
  • -t: タイムアウト時間. 最大値は180s
  • –docker-image: Docker イメージを用いて Cloud Foundry を起動

本当は Nexus3 を使ってみたかったのですが、デフォルトの Nexus3 イメージのディスク要求量が3GBだったので断念しました(CF のディスク割り当て量は2048MBが最大量となります).

ここで一つ注意点ですが、Cloud Foundry はコンテナですので、一度停止してしまうと公開済みのパッケージも一緒に消えてしまいます. 従量課金額も Nexus 用途では割高感があるので、本番環境用には VM 等を用いることをおすすめします.

 

Continuous Delivery の作成

今回の本丸である、Continuous Delivery の構築を行なっていきます.

まずは IBM Cloud にログインしていただき、メニューから “DevOps” を選択します.

 
“ツールチェーンの作成” をクリックします.

 
検索欄に “ツールチェーン” と入力し、”ツールチェーン” を選択します.

 
“ツールチェーン名”、”リージョン”、”リソース・グループ” を入力します.
今回は “cicd-toolchain” という名前で作成しました.

 
ツールチェーンが作成されました.
ここから CI/CD に用いるコンポーネントを追加していきます. “ツールの追加” をクリックします.

 

GitHub 連携

まずは GitHub との連携を作成しましょう.
検索欄に “GitHub” と入力し、”GitHub” を選択します.

 
“GitHubサーバ” に “GitHub” を選択し、”認証” をクリックします.
(GitHub へ認証を行います)

 
“Authorize IBM-Cloud” をクリックします.

 
“リポジトリー・タイプ” に “既存” を選択します(GitHub にリポジトリが作成済みの前提)
“リポジトリーURL” に CI/CD の対象にしたいリポジトリを指定します.
最後に “統合の作成” をクリックします.

 
GitHub との連携が作成されました.

同様に REST API リポジトリも連携に追加します.

 

Slack 連携

次に Slack との連携を作成します.
まずは連携に必要な “Incoming Webhooks” を作成しましょう.

Slack 公式の手順は以下になります.

以下に私が行なった手順を列挙します. ご参考まで.

最後に表示されている画面の “Webhook URL” をコピーしておきましょう

 

さて、ツールチェーンに戻ってきまして、
検索欄に “Slack” と入力し、”Slack” を選択します.

 
“Slack Webhook” に先ほどコピーした URL を入力します.
“Slack チャンネル” に CI/CD を通知したいチャンネルを指定します.
“Slack チーム名” に Slack のワークスペース名を入力します.

 
Slack との連携が作成されました.

 
これで Slack に CI/CD の通知が届くようになりました.

 

Nexus 連携

次に Nexus との連携を作成します.

検索欄に “Nexus” と入力し、”Nexus” を選択します.

 
各パラメータを入力します.

  • 統合名: 任意でOK
  • 統合 URL: Nexus の URL. 今回であれば CF アプリの URLになります
  • リポジトリー・タイプ: 今回は Spring Boot で用いるので、 “maven リポジトリー” を選択
  • ユーザー ID: Nexus にログインするためのユーザーID. Docker イメージのデフォルトは “admin”
  • 認証トークン: Nexus にログインするためのパスワード. Docker イメージのデフォルトは “admin123”
  • リリース URL: 統合URLに “/content/repositories/releases/” を加えた形
  • スナップショットURL: 統合URLに “/content/repositories/snapshots/” を加えた形
  • ミラーまたはパブリック URL: 今回は設定なし

 
Nexus との連携が作成されました.

 

Pipeline 作成

いよいよ本丸of本丸の Pipeline を作成します.

検索欄に “Delivery Pipeline” と入力し、”Delivery Pipeline” を選択します.

 
“Pipeline名” に任意の名前を入力し、 “統合の作成” をクリックします.

 
Pipeline が作成されました.

 
“ステージの追加” をクリックします.

 
まず初めにビルドジョブを作成します.
(本質的にはこのビルドジョブと後続のパブリッシュジョブを分ける必要はありません)

入力として GitHub の master ブランチへの push をトリガーとします.

 
ジョブを記述します.
“ジョブの追加” をクリックし、 “ビルド” を選択します.

 
“ビルダー・タイプ” に “Gradle” を指定します.

“ビルド・スクリプト” は以下のような感じです.

#!/bin/bash

export JAVA_HOME=~/java8
export PATH=$JAVA_HOME/bin:$PATH

./gradlew build -Dcom.ibm.jsse2.overrideDefaultTLS=true

IBM JDK 8 を用いて Gradle Wrapper でビルドします.
-Dcom.ibm.jsse2.overrideDefaultTLS=true は、IBM JDK を用いると SSL HandShake でコケるようだったのでVMオプションに追加しました.

最後に保存をクリックします.

 

続けてパブリッシュジョブを作成します.
再度、 “ステージの追加” をクリックします.

 
“入力タイプ” は “ビルド成果物” を指定します.
前段のビルド結果をそのまま引き継ぐ形です.

 
ビルドジョブを追加し、
今度は “ビルダー・タイプ” として “Gradle … Nexus” を指定します.
(これにより、Nexus 連携を環境変数として取得できるようになります)

ビルドスクリプトは以下のようになります.

#!/bin/bash

export JAVA_HOME=~/java8
export PATH=$JAVA_HOME/bin:$PATH

./gradlew publishMyLibraryPublicationToMavenRepository -Dcom.ibm.jsse2.overrideDefaultTLS=true

 
ここで、環境変数はどこで用いているのかという疑問を持つ方がいらっしゃるかと思いますが、
それは build.gradle に記載されています.

group 'com.project-respite'
version '1.0.1-SNAPSHOT'

apply plugin: 'java'
apply plugin: 'maven-publish'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

jar {
    baseName = 'common'
    version = '1.0.1'
}

publishing {
    publications {
        myLibrary(MavenPublication) {
            from components.java
        }
    }
    repositories {
        maven {
            url = System.getenv('MAVEN_SNAPSHOT_URL')
            credentials {
                username = System.getenv('MAVEN_USER_ID')
                password = System.getenv('MAVEN_TOKEN')
            }
        }
    }
}

Nexus 連携を加えたことで、 CI/CD 上から環境変数で Nexus にアクセスできるようになっています.
 
これで、無事 Nexus に Common モジュールを公開できました.

 

続いて REST API モジュールの Pipeline を作成していきます.

“API Pipeline” という名前で Delivery Pipeline を作成します.

 
入力として、今度は restapi リポジトリを指定します.

 
ビルドジョブを作成します.

“ビルダー・タイプ” に “Gradle … Nexus” を指定します.
これは、Nexus にアップロードされている Common モジュールを依存関係としてインポートするためです.

ビルドスクリプトは以下のようになります.

#!/bin/bash
export JAVA_HOME=~/java8
export PATH=$JAVA_HOME/bin:$PATH

./gradlew war -Dcom.ibm.jsse2.overrideDefaultTLS=true
cp manifest.yml build/libs/manifest.yml

Common モジュールと同様に gradle wrapper を用いてビルドを行います.
ビルドによって生成された war は build/libs に配置されるようになっています.
同ディレクトリに manifest.yml をコピーします.
(これは後述するデプロイジョブで必要)

“ビルド・アーカイブ・ディレクトリー” として “build/libs” を指定します.
これが後続のジョブの入力になります.

build.gradle は以下になります.
“MAVEN_SNAPSHOT_URL” で nexus リポジトリを参照しています.

buildscript {
	ext {
		springBootVersion = '2.0.4.RELEASE'
	}
	repositories {
		mavenCentral()
		maven {
			url = System.getenv('MAVEN_SNAPSHOT_URL')
		}
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
	}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'war'

war {
	enabled = true
	archiveName 'api.war'
}

group = 'com.project_respite.ibmcloud_cicd'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
	mavenCentral()
	maven {
		url = System.getenv('MAVEN_SNAPSHOT_URL')
	}
}


dependencies {
	implementation('com.project-respite:ibmcloud-cicd-common:1.0.1-SNAPSHOT')
	implementation('org.springframework.boot:spring-boot-starter-web')
	testImplementation('org.springframework.boot:spring-boot-starter-test')
}

 

最後にデプロイジョブを作成します.

“入力タイプ” に “ビルド成果物” を指定し、 “ステージ”, “ジョブ” にそれぞれ “API BUILD”, “Gradle Build” を指定します. これにより Gradle Build の成果物である build/libs ディレクトリが入力として渡されます.

 
デプロイジョブを追加します.

 
Cloud Foundry へのデプロイを記述します.
リージョンや組織等はお好みのものを指定してください.

コマンドはとてもシンプルに cf push だけです.

#!/bin/bash
cf push

具体的な内容は manifest.yml に記載しています.

applications:
  - name: ibmcloud-cicd-restapi
    buildpack: liberty-for-java
    memory: 512M
    instances: 1
    path: api.war
    timeout: 180

 
環境依存の値を使いたい場合は環境変数に key/value を追加し、 “${CF_APP}” のように参照しましょう.
 
無事、Cloud Foundry へデプロイが行えました.

 
Slack にも以下のように通知が来ています.

 

最後に Cloud Foundry アプリが実際に正しく動いているか確認します.

いい感じにフィボナッチ数列の値が計算できています.

 

まとめ

今回は IBM Cloud の Toolchain を用いて CI/CD 環境を構築しました.
Common モジュールのビルド&パブリッシュ、REST API モジュールのビルド&デプロイを実現しました.
これらのことが全て PaaS プラットフォーム上で実現できるので大変お手軽です.

CI/CD 環境をご検討の方は是非使ってみてください!

 

以上です.