Corredor

ウェブ、プログラミングの勉強メモ。

1つのコマンドで複数の Node.js サーバを起動する

$ node start-servers.js みたいな感じで、1つのコマンドを叩くだけで、複数のポートを使用した Node.js サーバを起動できないかなぁ〜と思っていた。

つまり、例えば、1つのコマンドを叩くだけで、

  • http://localhost:3000/ でメインの Web アプリを見られるようにし、
  • http://localhost:8888/ で管理ツールの Web アプリを見られるようにし、
  • https://localhost:8443/ で HTTPS サーバも見られるようにしたい

ということだ。

これまではサーバを起動する Node.js ファイルを複数作っていたので、$ node server1.js & node server2.js みたいに並列実行すればいいのか…?とか思っていたのだけど、Node.js サーバは1つの JS ファイルから複数 listen() (= 起動) できることに気が付き、以下のようにやってみた。

const http  = require('http');
const https = require('https');
const fs    = require('fs');

// 一気に繋げて書いた場合
const server1 = http.createServer((req, res) => {
  console.log('Port 3000');
  res.writeHead(200);
  res.end('Hello World : 3000');
}).listen(3000);

// 分けて書いた場合
const server2 = http.createServer();
server2.on('request', (req, res) => {
  console.log('Port 8888');
  res.writeHead(200);
  res.end('Hello World : 8888');
});
server2.listen(8888);

// HTTPS サーバも混ぜてみる
const server3 = https.createServer({
  key : fs.readFileSync('key.pem'),
  cert: fs.readFileSync('cert.pem')
}, (req, res) => {
  console.log('HTTPS Port 8443');
  res.writeHead(200);
  res.end('Hello World : 8443 (HTTPS)');
}).listen(8443);

console.log('サーバ起動 :')
console.log('  http://localhost:3000/');
console.log('  http://localhost:8888/');
console.log('  https://localhost:8443/');

server1server2 は書き方が違うだけで挙動は同じ。server3 は前回紹介したオレオレ証明書を使用した HTTPS サーバ。HTTP サーバと HTTPS サーバの共存も問題なし。

複数サーバを1ファイルで起動したので、$ node start-servers.js みたいな形で1コマンドで実行できる。閉じる時も Ctrl + C で3つとも落ちる。コレで良い感じ。

Node.js でオレオレ証明書を利用した簡易 HTTPS サーバを立てる

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術


※2018年1月29日の記事の改善版 (というかソチラの記事が下書き版だったのに間違えてアップしていた) です。

neos21.hatenablog.com


テストのために自己署名証明書を使用するサーバを立てることになった。

そこで、MacOS に標準で入っている openssl コマンドを利用して自己署名証明書を用意し、Node.js で簡易的な HTTPS サーバを立ててみる。

オレオレ証明書を作る

まずは自己署名証明書を作る。自己署名証明書とは要するに「認証局のお墨付きがない証明書」のことで、データが改竄されていないことの証明には使えない。今回はあくまで開発用に作るが、本来オレオレ証明書は使うべきでないシロモノなので、お忘れなきよう。

MacOS は最初から openssl コマンドが使用できたが、Linux などで openssl コマンドがない場合は別途インストールする。

# yum を使っている場合は以下で
$ yum install openssl

# apt の場合は以下で
$ apt-get install openssl

Windows の場合は「Win32 OpenSSL」というツールをインストールすれば良いようだ (未検証)。

openssl コマンドを使って、秘密鍵 → 公開鍵 → デジタル証明書、の順に作る。

# 秘密鍵を作る → key.pem ができる
$ openssl genrsa -out key.pem 1024

# 公開鍵を作る : key.pem を指定する。csr.pem ができる
$ openssl req -new -key key.pem -out csr.pem
  # 対話式で国だのなんだの聞かれるので、適当に入力したり、空欄で飛ばしたり

# デジタル証明書 : key.pem と csr.pem を指定する。cert.pem ができる
$ openssl x509 -req -in csr.pem -signkey key.pem -out cert.pem

これで key.pemcsr.pemcert.pem という3つのファイルができる。

Node.js で簡易 HTTPS サーバを立てる

次に、Node.js に組み込みの https モジュールを使って、簡易 HTTPS サーバを作る。今回は Node.js v6.11.0 にて検証。

作業ディレクトリに my-server.js (名前は任意) という空ファイルを作成し、同じディレクトリに key.pemcert.pem を格納しておく (csr.pem は今回使わない)。

my-server.js を開き、以下のように実装する。

const fs    = require('fs');
const https = require('https');

// 秘密鍵とデジタル証明書ファイルを指定する
const options = { 
  key : fs.readFileSync('key.pem'),
  cert: fs.readFileSync('cert.pem')
};

// サーバを起動する
https.createServer(options, (req, res) => {
  console.log('リクエストを受信');
  // レスポンスの設定
  res.writeHead(200);
  res.end('Hello World');
}).listen(8443);  // ポートを指定して待受スタート

console.log('自己署名証明書サーバを起動');

https.createServer() の第2引数が、リクエストを受け取った時に実行される関数となる。今回は HTTP ステータスコード 200 を返し、画面に Hello World とだけ表示させることにする。ポートは .listen(8443) 部分で指定しているとおり、8443 となる。

my-server.js が実装できたら、Node.js でファイルを実行する。すると HTTPS サーバが起動する。

$ node my-server.js

自己署名証明書サーバを起動

ターミナルはこのまま待機状態になるので放置。

ブラウザを開き、https://localhost:8443/ にアクセスすると、自己署名証明書を使用している旨の警告が表示されるかと思う。警告を無視してページを開くようにすると、画面に「Hello World」と表示されるはずだ。この際、ターミナルには「リクエストを受信」のログが出ているはず。

終わり

コレにて完了。今回はとりあえず https://localhost:8443/ にアクセスするとオレオレ証明書を利用したページが開ける、というだけの簡素なサーバを立ててみた。ベースは http モジュールと同様なようなので、カスタマイズも可能だ。

Angular In Memory Web API を使ってモックサーバを立てる

Angularデベロッパーズガイド  高速かつ堅牢に動作するフロントエンドフレームワーク

Angularデベロッパーズガイド 高速かつ堅牢に動作するフロントエンドフレームワーク

  • 作者: 宇野陽太,奥野賢太郎,金井健一,林優一,吉田徹生,稲富駿,丸山弘詩
  • 出版社/メーカー: インプレス
  • 発売日: 2017/12/15
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

AngularによるモダンWeb開発 実践編~実際の開発で必要な知識を凝縮~

AngularによるモダンWeb開発 実践編~実際の開発で必要な知識を凝縮~

Angular アプリの開発中、Web API サーバをモック化する時は、angular-in-memory-web-api を使うと簡単にモック API サーバが用意できる。

少々つまづいたところがあったので、実装の仕方を確認していこう。

プロジェクトを用意する

まずは Angular CLI の ng new コマンドで Angular プロジェクトの雛形を作ろう。今回は @angular/cli@1.6.4 を使用して、Angular5 系の雛形を作成した。

$ ng new api-example

雛形ができたら、Angular In Memory Web API をインストールする。

$ npm install angular-in-memory-web-api --save

コレでひとまず、$ npm start でサーバが起動するか確認しておこう。

HTTP 通信する画面を作る

今回は AppComponent にて、POST 送信を行う簡単なプログラムを用意する。今回は HttpClient で実装したが、以前の Http モジュールを利用しても問題ない。

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { environment } from '../environments/environment';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  /** 通信結果を表示するための項目 */
  public result: any;

  /**
   * コンストラクタ
   *
   * @param http HttpClient
   */
  constructor(private http: HttpClient) {}

  /**
   * GET 通信する
   *
   * @param path ドメイン以下のパス
   */
  doGet(path: string): void {
    // サーバ URL : 環境変数によって実際のサーバ URL にするか、モックサーバを示す文字列にするか切り替える
    const serverUrl = environment.production ? 'http://example.com/' : 'mock-server/';
    // サーバ URL とパスを結合して URL を生成し GET 通信する
    this.http.get(serverUrl + path)
      .toPromise()
      .then((response) => {
        this.result = response;
      })
      .catch((error) => {
        this.result = error;
      });
  }
}

HTML 側は以下のとおり。

<h1>Angular In Memory Web API Example</h1>
<ul>
  <li><button type="button" (click)="doGet('users')">ユーザ情報を取得する</button></li>
</ul>
<div *ngIf="result">
  <h2>結果</h2>
  <p>{{ result | json }}</p>
</div>

今回は簡単にするため、サービスクラスも作っていないし、HTML 中に doGet('users') と URL 文字列の一部をもたせたりしていて、かなりお行儀の悪いコードだが、お許しいただきたい。

ポイントは、HttpClient で通信するサーバ URL を開発中だけ http://example.com/ ではなく mock-server/ となるよう切り替えているところ。コレにより、「ユーザ情報を取得する」ボタンを押下すると、開発中は mock-server/users という URL に HTTP 通信を試みるワケである。

コンポーネントができたら、AppModuleimportsHttpClientModule を追加しておく。コレでひとまず HTTP 通信を行おうとする画面ができあがる。

import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule  // ← 追加
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Web API モックを構成するサービスクラスを作成する

次に、Web API のモックとして、URL やレスポンスデータを定義するサービスクラスを作成する。

$ npm run ng generate service mock-web-api

このコマンドで src/app/mock-web-api.service.ts を作成したら、以下のように実装する。

import { Injectable } from '@angular/core';

import { InMemoryDbService } from 'angular-in-memory-web-api';

@Injectable()
export class MockWebApiService implements InMemoryDbService {
  /** モックデータ : 標準的な Web API の URL と対応させるため、データは配列で定義し、各要素は id プロパティが必須 */
  private api: any = {
    // ユーザ情報とか
    users: [
      { id: 1, name: 'Marty' },
      { id: 2, name: 'Jennifer' }
    ]
  };
  
  /**
   * InMemoryDbService から継承 : モックデータを作成する
   *
   * @return モックデータ
   */
  public createDb(): any {
    return this.api;
  }
}

ポイントは InMemoryDbService を継承している点だ。createDb() もオーバーライドしているメソッドで、このメソッドで返却したオブジェクトが Web API の URL と自動的に対応するようになる。

つまり、この設定だけで以下の URL でそれぞれデータをアクセスできるようになる、ということだ。

  • mock-server/usersthis.api.users の配列データ
  • mock-server/users/1this.api.users の配列のうち、id プロパティが 1 のデータ

この「オブジェクトと URL の自動マッピング」については、あとで詳しく詳細する。

サービスクラスができたら、コレをモックサーバとして動作させるために、AppModule に以下のように追加する。

import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';

import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';

import { AppComponent } from './app.component';
import { MockWebApiService } from './mock-web-api.service';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    HttpClientInMemoryWebApiModule.forRoot(MockWebApiService)  // ← 追加
  ],
  providers: [
    MockWebApiService  // ← 追加
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

コレで完了。

開発中は mock-server/users にアクセスすることになり、MockWebApiService にて定義した users のデータが返却されるようになる。

「ユーザ情報を取得する」ボタン

  ↓ 以下が取得・画面表示される

[ { "id": 1, "name": "Marty" }, { "id": 2, "name": "Jennifer" } ]

ID を指定して特定データのみ取得する

さて、標準的な RESTful API の URL 設計に基づけば、/users はデータを一覧形式で取得でき、/users/2 のように ID を指定すれば、個別の ID が取れるはずだ。

Angular In Memory Web API はこうした RESTful API の URL を自動的に判別してデータを返してくれるので、いきなり /users/2 にアクセスして Jennifer のデータのみを取得することができる。

実際にやってみよう。HTML に以下の1行を追加する。

<h1>Angular In Memory Web API Example</h1>
<ul>
  <li><button type="button" (click)="doGet('users')">ユーザ情報を取得する</button></li>
  <li><button type="button" (click)="doGet('users/2')">ID : 2 のユーザ情報を取得する</button></li>  <!-- ←追加 -->
</ul>
<div *ngIf="result">
  <h2>結果</h2>
  <p>{{ result | json }}</p>
</div>

コレだけで、「ID : 2 のユーザ情報を取得する」ボタンを押すと、以下のようなデータが取得できる。

{ "id": 2, "name": "Jennifer" }

その他にも、クエリパラメータなども受け付けてくれるようだ。詳しくは以下の「HTTP request handling」を参照。

コレは便利だ。しかし問題が…。

モックデータオブジェクト・URL に制約ができる

createDb() で返却したオブジェクトが自動的に URL とマッピングされるのは便利だが、この機能のためにオブジェクトの内容に以下のような制約が発生する。

  • this.api のトップレベルのキー名が URL となる
    • ここでは users というキー名がそのまま /users というパスになる
  • 各キーは配列でデータを持たなくてはならない
    • users: { name: 'Marty' } というようにいきなりオブジェクトを1つ返却する作りにはできない
    • 後述するカスタムハンドリングでレスポンスデータは調整可能だが、オブジェクトの型をチェックしているので、定義時点では配列化が必須。
  • 配列内の各データは URL で指定できる ID となる id プロパティを持たなくてはならない

そして、このようなオブジェクトの制約により、http://example.com/book/genres のように2階層以上くだるパスが扱えないという問題が発生している。標準的な RESTful API の URL 設計に基づいていればこのような URL になることは珍しいかもしれないが、それにしてもモックサーバ用ライブラリの都合で URL が決まってしまうのはつらい。

  • api オブジェクトのキー名を 'book/genres' と指定してもダメで、Collection 'book' not found" というエラーになってしまう。
  • book: { genres: { ... } } のように入れ子のオブジェクトにしたら…?と思ったが、コレだと book が配列でないためにエラーになる。

2階層以上のパスを解釈させる方法

では、どうやって /book/genres のような2階層以上のパスを解釈させるか、というと、InMemoryDbService が持つ parseRequestUrl() というメソッドをオーバーライドし、リクエスト URL を内部的に1階層のパスに変換することで対処できる。

実際にやってみる。MockWebApiService に以下のように bookGenres データと parseRequestUrl() メソッドを追加する。

import { InMemoryDbService, ParsedRequestUrl, RequestInfoUtilities } from 'angular-in-memory-web-api';

@Injectable()
export class MockWebApiService implements InMemoryDbService {
  /** モックデータ : 標準的な Web API の URL と対応させるため、データは配列で定義し、各要素は id プロパティが必須 */
  private api: any = {
    // ユーザ情報とか
    users: [ /* …中略… */ ],
    // 本のジャンル名 : '/book/genres' でアクセスされた時に対応させるデータ
    bookGenres: [
      { id: 1, genre: 'Sience Fiction' },
      { id: 2, genre: 'Drama' },
      { id: 3, genre: 'History' }
    ],
  };
  // …中略…
  
  /**
   * InMemoryDbService から継承 : リクエスト URL を変換する
   *
   * @param url リクエスト URL
   * @param utils リソース情報のユーティリティ
   * @return 変換された URL 情報
   */
  public parseRequestUrl(url: string, utils: RequestInfoUtilities): ParsedRequestUrl {
    // 'book/genres' という URL を 'bookGenres' に変換する
    const replacedUrl = url.replace('book/genres', 'bookGenres');
    // リクエスト情報を変換する
    return utils.parseRequestUrl(replacedUrl);
  }

parseRequestUrl() の第1引数の URL 文字列を replace() で変換し、第2引数の RequestInfoUtilities を使って ParsedRequestUrl 型に変えてやれば良い。

ココでの URL 置換処理は全ての通信に影響するので、慎重に変換したい。例えば以下のような汎用的な置換処理でも同等の結果は得られるが、こうすると今度は /users/2 といった URL が /users2 のようになってしまうため、あまり使えない。

const replacedUrl = url
  .replace('mock-server/', '')  // 先頭の 'mock-server/' を除去する : これによりトップレベルのパスは置換対象外にする
  .replace(/\/./g, (match) => {
    // '/a' 部分を取得し、'A' と大文字化する : これにより 'book/genres' は最終的に 'bookGenres' と置換される
    return match.replace(/\//g, '').toUpperCase();
  })
  .replace(/^/, 'mock-server/');  // 先頭に 'mock-server/' を再度付与する

面倒だが、必要なパスだけ置換するようにした方が良いかも。

実際にデータを取得してみる

それでは実際にデータを取得してみよう。コンポーネントの HTML に以下を追加する。

<li><button type="button" (click)="doGet('book/genres')">本のジャンル情報を取得する</button></li>

これで mock-server/book/genres への HTTP 通信が発生するが、parseRequestUrl() が URL を /mock-server/bookGenres に変換するため、api に宣言された bookGenres オブジェクトが取得され、以下の結果が得られる。

[ { "id": 1, "genre": "Sience Fiction" }, { "id": 2, "genre": "Drama" }, { "id": 3, "genre": "History" } ]

思ったように動いてくれた。

レスポンスデータをカスタマイズする

今度は全く別の例。

次は、/blood-type というパスにアクセスした時に、配列形式ではなく単一のオブジェクトを返却するようにしたい。

Angular In Memory Web API の標準仕様では、前述の /users のように、配列形式でしかデータを取得できないはずだ。しかし、responseInterceptor() というメソッドをオーバーライドすると、特定のパスへのアクセス時にレスポンスデータをカスタマイズできるのだ。

実際に実装してみる。MockWebApiService に以下のように blood-type モックデータと responseInterceptor() メソッドを追加する。

import { Injectable } from '@angular/core';

import { InMemoryDbService, ParsedRequestUrl, RequestInfoUtilities } from 'angular-in-memory-web-api';

@Injectable()
export class MockWebApiService implements InMemoryDbService {
  /** モックデータ : 標準的な Web API の URL と対応させるため、データは配列で定義し、各要素は id プロパティが必須 */
  private api: any = {
    users: [ /* 中略 */ ],
    bookGenres: [ /* 中略 */ ],
    // 血液型情報 : '/blood-type' で内部のオブジェクト部分のみ返却させる
    'blood-type': [
      {
        id: 0,
        // この部分を返却する
        data: {
          A: 'A型',
          B: 'B型',
          AB: 'AB型',
          O: 'O型'
        }
      }
    ]
  };
  
  // …中略…
  
  
  /**
   * InMemoryDbService から継承 : データを返却する処理
   *
   * @param responseOptions レスポンスデータを含むオブジェクト
   * @param requestInfo リクエスト情報
   */
  public responseInterceptor(responseOptions: any, requestInfo: any): any {
    // '/blood-type' へのアクセス時は内部のオブジェクトデータを返却する
    if(requestInfo.collectionName === 'blood-type') {
      responseOptions.body = this.api['blood-type'][0].data;
    }
    
    return responseOptions;
  }

responseInterceptor()responseOptions.body にお好みのデータを代入し直してやれば、レスポンスデータを変更できる。ココでは、キー blood-type の配列0番目の要素が持つ data プロパティの値、つまり連想配列オブジェクトを返却するようにした。

なお、ココで参照している requestInfo.collectionName は、parseRequestUrl() メソッドで変換した後の URL となる。つまり、先程の書籍一覧のレスポンスデータを操作したい場合は、'book/genres' ではなく 'bookGenres' であるかどうかでチェックしないといけない。

HTML 側にこのパスを叩くボタンを追加して、実際に動作させてみよう。

<li><button type="button" (click)="doGet('blood-type')">血液型情報を取得する</button></li>

これで、以下のような結果が得られるはずだ。

{ "A": "A型", "B": "B型", "AB": "AB型", "O": "O型" }

まとめ

  • angular-in-memory-web-api というパッケージを使うと、Angular4 以降のアプリでモック API を実現できる。
  • InMemoryDbService を継承したクラスを用意し、createDb() メソッドをオーバーライドすることで、URL と返却データを定義する。
  • parseRequestUrl() をオーバーライドして URL 文字列を変換すれば、2階層以上のパスでもアクセスできる。
  • responseInterceptor() をオーバーライドすると、特定のパスでレスポンスデータをカスタマイズできる。

かなり機能が多く、まだまだ紹介しきれていないノウハウも多々ある。設定の癖も強いので、一つずつ調べながら実装しよう。

Jenkins の Multibranch Pipieline を試した

Jenkins

Jenkins

前回、Jenkins でジョブをスクリプト形式で書ける「Declarative Pipeline」という記法を試した。

今回は、このスクリプトファイルを複数のブランチに適用しやすくするための「Multibranch Pipeline」というモノを試してみる。

Multibranch Pipeline とは

通常の Pipeline でブランチごとにジョブを実行したい場合、Pipeline スクリプトファイル内でトリガーの契機となったブランチを判定したりする必要があった。この時、ワークスペースは全部ランチで共有されており、ブランチごとに異なる依存ライブラリを扱ったりしている時に不都合が出やすかった。

マルチブランチ・パイプラインは、Git リポジトリと自動連携して複数ブランチを個別に管理してくれる仕組みだ。スクリプトファイル内でブランチを選択してチェックアウトする必要はなく、Git リポジトリへの Push を検知して自動的に対象ブランチ向けのジョブを実行してくれるのだ。

Multibranch Pipeline を作ってみる

まずは Jenkins 管理画面のメニューから「新規ジョブ作成」を選び、「Multibranch Pipeline」を作る。

設定画面での設定項目は以下のとおり。

  • Branch Sources
    • 「Add source」ボタンより「Git」を選び、対象のリポジトリを設定する。GitHub との連携時は「GitHub」より設定する。
    • 「高度な設定」ボタンを押すと、「対象ブランチ」「対象外ブランチ」を指定できる。ワイルドカードが使用できるので、「feat/ で始まるブランチは Jenkinsfile が存在してもジョブを実行させたくない」場合は、「対象外ブランチ」に「feat/*」と設定すればよい。
  • Build Configuration
    • リポジトリ中の Jenkinsfile のパスを指定する。大抵はルートディレクトリに「Jenkinsfile」という名前でスクリプトファイルを置くと思うので、そのままで良い。
  • あとはお好みで「不要アイテムの扱い」など。

ここまで設定できたら、対象のリポジトリのルートディレクトリに Jenkinsfile を作り、以下のように実装してみよう。

pipeline {
  agent any
  stages {
    stage('npm install') {
      steps {
        nodejs(configId: '★', nodeJSInstallationName: 'my nodejs') {
          bat 'npm install'
        }
      }
    }
    stage('UT 実行') {
      steps {
        nodejs(configId: '★', nodeJSInstallationName: 'my nodejs') {
          bat 'npm test'
        }
      }
    }
  }
}

内容は前回の記事で作った「npm install 後に npm test を実行する」というジョブのスクリプトとほぼ同じ。違うのは「Git チェックアウト」の stage がなくなったこと。ブランチのチェックアウトはジョブが勝手にやってくれるので、Jenkinsfile での指定は不要なのだ。

このファイルを git addgit commit して、git push してみよう。Git リポジトリとの連携ができていれば、Jenkins のジョブが自動的に実行されるはずだ。

試しに、別の名前のブランチを新たに作って git push してみよう。先程のブランチのジョブとは別のワークスペースができ、当該ブランチに対するジョブとして実行される。ブランチ別にサブジョブとして管理されるので、Jenkins の管理画面としても見やすい。例えば「master ブランチはテストをクリアしているが、feat/test1 ブランチはテストが失敗している」といった見極めも容易だ。

特定ブランチのみ異なる Jenkinsfile を実行させる

特定のブランチのみ、同リポジトリ内の Jenkinsfile ではなく、別リポジトリで管理している Jenkinsfile を実行する方法。

実はサブジョブも一つの Pipeline ジョブとして扱うことができ、ブランチごとに個別の設定が可能なのだ。

マルチブランチ・パイプラインの「Branches」から任意のブランチを選択し、「設定の参照」に移動する。すると通常の Pipeline ジョブとほぼ同等の設定画面が現れるはずだ。

ココで最下部にある「Pipeline」のプルダウンにて、「Pipeline from multibranch configuration」から「Pipeline script」を選べばその場にスクリプトが書けるし、「Pipeline script from SCM」を選択して別リポジトリを選択すれば、異なる Jenkinsfile を実行できる。

例えば「develop ブランチまでは UT を実行したいが、master ブランチではイチイチ UT を回す必要はない。master ブランチへの Push 時はリリース作業を行う別のスクリプトを実行させたい」といった場合に利用できるかと思う。

ただし、Multibranch Pipeline の設定に沿わないブランチが出てくるのはあまり管理しやすい状態ではないので、多用は控えたい。

特定のブランチのみ実行する stage を書く

次は、1つの Jenkinsfile 内で、ブランチを判別して特定のブランチのみ処理を行わせる方法。

whenbranch というシンタックスを使って、以下のように書けば良い。

pipeline {
  agent any
  stages {
    stage('master ブランチのみ処理する') {
      when {
        branch 'master'
      }
      steps {
        echo 'master ブランチのみ実行します'
      }
    }
    stage('feat/ 始まりのブランチのみ処理する') {
      when {
        branch 'feat/*'
      }
      steps {
        echo 'feat/ 始まりのブランチのみ実行します'
      }
    }
  }
}

このようなスクリプトにすると、master ブランチで実行された場合は「master ブランチのみ処理する」の stage のみ実行され、「feat/ 始まりのブランチのみ処理する」stage はスキップされる。

branch 'feat/*' のようにワイルドカードも使える。Jenkins 管理画面における「対象ブランチ」「対象外ブランチ」の書き方と同じだ。

not { when { branch という構造にすると、当該ブランチ以外なら実行する、という stage も書ける。

こちらもあまり多用すると、1つの Jenkinsfile に条件分岐が多く登場し、こんがらがるので、最低限に留めたい。

Multibranch Pipeline のジョブの後に別のジョブを実行する

Multibranch Pipeline のジョブの後に、別のジョブを実行するためには、次のように設定する。

後続のジョブの設定にて、「Build Triggers」→「他プロジェクトの後にビルド」を選択し、対象プロジェクトを「【マルチブランチ・パイプライン名】/【ブランチ名】」と指定する。例えば「my-multi/master」といった形だ。

単に「my-multi」とマルチブランチ・パイプラインの名前を指定するだけではダメで、入力補完でもこの時点までは入力エラー扱いになるので指定できないかのように見えるが、スラッシュ / まで入力すると候補が登場する。

コレで、「Multibranch Pipeline を使って全ブランチで UT を実行するが、master ブランチのみはその後に別のリリースジョブを実行する」といった設定が可能になる。

おわり

なかなか Jenkins も奥が深い…。ジョブの構成やスクリプトファイルの管理方法に色々悩むが、あんまり綺麗に構造化しきろうとは思わない方が良いのかも。