まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:pipeline

無料でウェブアプリケーションをデプロイできる数少ない環境となった Heroku ですが、この無料環境内で GitHub 連携による自動デプロイ(いわゆる "GitOps" っぽいデプロイ)を簡単に実現できそうなことがわかりました。手順を以下に紹介しますが、前提条件として以下の環境が揃っていることを事前に確認してください:

(1) Heroku アカウント取得&セットアップ済み
(2) GitHub アカウント取得&セットアップ済み
(3) 自分の PC に git コマンドと Node.js がインストール済み※

※ (3) は手元での動作確認が不要であればインストールされていなくても可


【アプリケーションの準備】
まず自動デプロイを行う対象となるウェブアプリケーションを GitHub に用意します。これは Heroku の dyno で動かせる状態になっていれば何でもいいのですが、以下ではこちらで用意したシンプルなウェブアプリケーションを使って紹介します(自分のアプリを使う場合は、そのアプリを GitHub のリポジトリとして登録してください)。

用意したサンプルはこちらです:
https://github.com/dotnsf/simpleweb

2022020601



内容は後述する動作確認時にも再度紹介しますが、シンプルなメッセージを表示するだけのウェブアプリケーションです:
20220206


このサンプルを使う場合は GitHub リポジトリのページ右上のボタンから "fork" して使ってください:
2022020602


fork すると、用意したサンプルアプリケーションが fork した人のリポジトリとして利用できるようになります。以下では fork 先のリポジトリ URL が https://github.com/yourname/simpleweb となったと仮定して説明を続けます(yourname の部分を自分のものに読み替えてください)。なおパイプライン連携の対象となるリポジトリは public でも private でも以下の作業はできます。

実はこの状態から heroku 側の設定に進むこともできるのですが、このアプリケーションを一度手元で動かしてみることにします(上述の (3) の準備ができていれば可能です。できていない人は読み飛ばしてください)。ターミナルから以下のコマンドを実行して、fork した自分のリポジトリを clone します:
$ git clone https://github.com/yourname/simpleweb

そして依存ライブラリを導入後に node コマンドで実行します:
$ cd simpleweb

$ npm install

$ node app

アプリケーションが起動すると 8080 番ポートで HTTP リクエストを待ち受けます。最後にウェブブラウザで http://localhost:8080/ にアクセスします:
2022020701

↑こんな感じの「ハローワールド!」が表示されれば成功です。機能としてはこれだけの、ごくシンプルなウェブアプリケーションです。このアプリケーションが GitHub のリポジトリに登録された状態になっています。


【heroku 連携】
ではこのアプリケーション(自分で用意したアプリケーションを使う場合はそのアプリケーション)の GitHub リポジトリを heroku のパイプラインやデプロイ先と連携して動くように設定します。

大まかな流れとしては、
1. heroku に(空の)アプリを登録
2. 1. のアプリにデプロイするためのパイプラインを作成
3. GitHub リポジトリとパイプラインを連携(GitHub が変更されたら自動的にパイプラインが実行されるようにする)

のようになります。1. から順に行っていきます。

1. まずは heroku にログインします。heroku アカウント未取得の場合は "Sign Up" リンクから作成することもできます:
https://www.heroku.com/

2022020702


ログインに成功すると以下のような画面になります。過去にアプリケーションを登録したことがあると初期画面にアプリケーション一覧が表示されますが、初めての場合はアプリケーションは表示されず、このような登録画面になります:
2022020703


では先程 GitHub に用意(fork)したアプリケーションが heroku 上で動かすことができるように登録します。画面右上のボタンから "New" - "Create new app" を選択します:
2022020704


そしてアプリの名前とデプロイ先リージョンを指定します。名前は他に使われていないものを指定する必要があります(下図では "dotnsf-simpleweb" という名称を指定しています)。リージョンは(無料版の場合は)United States か Europe のどちらかを選択します。単にアプリを作るだけならここで "Create app" ボタンを押せばいいのですが、今回はアプリと同時にパイプラインも作ってしまいましょう。というわけで "Add to pipeline" ボタンをクリックします:
2022020705


2. 続けてパイプラインの作成を行います。上の続きで "Add to pipeline" ボタンをクリックすると "Choose a pipeline" という選択エリアが現れるので、ここで "+ Create new pipeline" を選択します(このアプリ用の新しいパイプラインを作る、という意味です):
2022020706


するとパイプラインの名前(下図では "dotnsf-simpleweb-pipeline" としています)と、デプロイ先ステージを指定する画面が現れます。名前は好きな名前でいいのですが、デプロイ先ステージは "staging"(検証用) か "production" (本番用)かを選択します。下図ではそのまま本番環境にデプロイする想定で "production" を選択しています※。 最後に "Create app" ボタンをクリックします:
2022020707

※別のアプリケーションをもう1つ作って staging に指定し、GitHub → Staging(検証用アプリ) → Production(本番運用アプリ) というパイプラインを作ることもできますが、今回は割愛します。


するとこのような画面になります。ここまでで 1. のアプリにデプロイするための 2. のパイプラインができあがりました:
2022020708


そして最後の 3. の連携設定を行います。「GitHub のリポジトリが更新された」というタイミングに合わせて、同リポジトリの main ブランチの内容をパイプラインで指定されたアプリにデプロイする、というものです。

まず同じブラウザで GitHub にログインしておきましょう。GitHub のセッションがある状態ですすめることで面倒な認証部分を省略できます:
https://github.com/

そして先程の画面の続きです。まずはデプロイ方法として "GitHub" と書かれたアイコンをクリックします:
2022020701


すると画面下部が変わり、"Connect to Github" と書かれたボタンが現れます。このボタンをクリックします:
2022020702


するとどのリポジトリと連携するかを指定する画面に切り替わります。上部で GitHub へのログインを済ませていれば、後は自分のリポジトリを指定するだけです。自分の GitHub ID が指定されている状態で、対象リポジトリ(fork したリポジトリ)を指定するので "simpleweb" と入力して "Search" ボタンをクリックします:
2022020703


すると自分のリポジトリ内の simpleweb リポジトリ(fork したリポジトリ)が見つかります。その横の "Connect" ボタンをクリック:
2022020704


ここまでの作業でパイプラインと GitHub の simpleweb リポジトリとがつながりました。後は対象ブランチを選択しての自動デプロイ機能の設定だけです:
2022020705


画面下にスクロールすると、GitHub リポジトリのどのブランチからアプリケーションをビルドして自動デプロイするか、という選択画面になります。今回は main ブランチをそのまま使うことにして(つまり main ブランチに直接変更を加えることにして)"main" を指定したまま "Enable Automatic Deploys" ボタンをクリックします:
2022020706


すると画面の表示内容が切り替わり、main ブランチからの自動デプロイが有効になったことがわかります。これで事前準備が整いました:
2022020707


ちなみに、この時点で heroku のパイプライン画面を表示すると、このように GitHub から連携したパイプラインが自分の simpleweb アプリ(Production)につながっている様子が確認できます:
2022020705



【動作確認】
ではここまで設定した内容が正しく動作することを確認してみます。ある程度 git コマンドの操作がわかる人であれば、ローカルリポジトリの内容を変更して、git add して、git commit して、git push する・・・という一連の流れを実施いただいてもいいのですが、今回はウェブ画面だけで同じことを行ってみます。

改めて GitHub の(自分の)デプロイ対象リポジトリ(https://github.com/yourname/simpleweb)をウェブブラウザで開きます:
2022020701


ここに変更を加えます。見た目で変更がわかりやすいのは web/ フォルダ以下にファイルを追加したり、既存の index.html を変更したりすることですが、今回は後者でいきましょう。web/ フォルダを選択後に index.html ファイルを選択し、画面右上の鉛筆マークをクリックして編集画面に切り替えます:
2022020702


index.html をブラウザで編集する画面に切り替わるので、何か変更を加えてみます。以下の例では見た目にもわかりやすそうな所で、26 行目の「ハローワールド!」と表示されていた部分を「ハロー heroku ワールド!」と書き換えてみました:
2022020703


画面を下にスクロールすると、git commit のオプション入力画面になります。コミットメッセージ(下の例では "index.html メッセージ変更")を入力し、対象ブランチはデフォルトの "Commit directly to the main branch" が選択されたままにします。最後に "Commit changes" ボタンをクリックして git commit します(サーバー側を直接 git commit するので、 git push は不要です):
2022020704


コミットしました:
2022020706


先程のパイプライン画面を表示すると、GitHub リポジトリのコミットが行われたことを検知して、パイプラインが稼働している様子が確認できます(シンプルなパイプラインなのですぐに稼働後、つまりデプロイ後の状態に切り替わってしまいます・・。下図はデプロイ後の画面です):
2022020707


では heroku 上にデプロイされた変更後のアプリケーションを確認してみます。https://(アプリ名).herokuapp.com/ を開くか、またはパイプライン画面右下の "Open app" と書かれた箇所をクリックして、heroku 上のアプリケーションをウェブブラウザで開きます:
2022020707


heroku 上で稼働しているアプリケーションが開き、先程加えた変更が有効になっていることを確認します:
2022020708


これで GitHub リポジトリと連携した heroku デプロイ用パイプラインが作れて、かつ自動実行も有効になり、GitHub リポジトリに変更が加わればウェブアプリケーションにも反映される、という仕組みの構築ができました。

現実的にはステージング環境やローカル環境で動作確認してから反映、という流れになると思いますが、もちろんその方法(手動で git コマンドを使ってコミット&プッシュ)でも自動的に新しいアプリケーションに切り替えることもできます。またそもそもの Git ブランチ戦略をどうするか?という課題も出てくると思っています。

一方で、CLI を使わなくても(Git リポジトリをウェブで更新さえすれば)公開アプリケーションの更新ができるので、普段 CLI を使わないような、あまり詳しくない人でも(直接アプリケーションの挙動に影響しない範囲での変更であれば)コンテンツの変更管理ができる仕組みが構築できたと思っています。こういう仕組みが簡単に作れてしまう点は heroku の強みだと思います。

「国際化」に対応したウェブアプリケーションを Node.js で作る方法を調べたので、メモ替わりに残しておきます。

ここでの「国際化(internationalization, i18n)」はウェブブラウザで設定した言語によって自動的に英語表記にしたり、日本語にしたり、・・・という切り替えを行えるようなものです。自動翻訳とかそういうものではありません。またブラウザで設定した言語は HTTP リクエスト時に "Accept-Language" ヘッダで送信されることになるので、後述の動作確認は curl コマンドでこのヘッダを指定して行っています。

このような国際化対応アプリケーションを Node.js で、正確には Node.js + Express + EJS の環境で作ってみました。

Node.js で国際化対応アプリケーションを作る場合、i18n というパッケージを使うのが手っ取り早いです:
https://www.npmjs.com/package/i18n

ソースコード(app.js)はこんな感じにしました。余計な部分を削ぎ落として、最小限必要な部分だけを残しています(赤字部分が i18n 関連の箇所です):
//. app.js

var express = require( 'express' ),
    fs = require( 'fs' ),
    ejs = require( 'ejs' ),
    i18n = require( 'i18n' ),
    request = require( 'request' ),
    session = require( 'express-session' ),
    app = express();

var port = 3000;

app.set( 'views', __dirname + '/public' );
app.set( 'view engine', 'ejs' );

i18n.configure({
  locales: ['en', 'ja'],
  directory: __dirname + '/locales'
});
app.use( i18n.init );

app.get( '/', function( req, res ){
  res.render( 'index' );
});

app.listen( port );
console.log( "server starting on " + port + " ..." );

今回は英語(en)と日本語(ja)に対応したアプリケーションにしました。

また '/' にアクセスした時に ejs の index テンプレートを使った画面が表示されるような内容にしています。ちなみに index テンプレート(public/index.ejs)の内容は以下のようになっています:
<html>
<head>
<title><%= __('subject') %></title>
</head>
<body>
<h1><%= __('subject') %></h1>
<hr/>
<%= __('body') %>
</body>
</html>


テンプレート内で subject と body という2つの変数を使った表記を行っています。実際にはこれらの部分に言語設定に合わせた内容が表示されることになります。

そして言語ファイルを以下のように用意します:

(英語用: locales/en.json)
{
  "subject": "subject",
  "body": "body"
}


(日本語用: locales/ja.json)
{
  "subject": "サブジェクト",
  "body": "本文"
}

英語設定で利用した場合、上記の subject 変数部分は "subject", body 変数部分は "body" と表示されます。また日本語設定の場合、それぞれ "サブジェクト" と "本文" となります。


これで準備できました。 npm install して実行(node app)します:
$ npm install
$ node app

確認は別の端末から curl で行いました。まずは Accept-Language を en(英語)にしてアクセス:
$ curl http://localhost:3000/ -H 'Accept-Language: en'

<html>
<head>
<title>subject</title>
</head>
<body>
<h1>subject</h1>
<hr/>
body
</body>
</html>
>

次は日本語設定でアクセスした場合:
$ curl http://localhost:3000/ -H 'Accept-Language: ja'

<html>
<head>
<title>サブジェクト</title>
</head>
<body>
<h1>サブジェクト</h1>
<hr/>
本文
</body>
</html>


期待通りに動いています!


アプリケーションの国際化そのものはこれだけで出来ました。そして問題になるのは「どうやって色んな言語用の JSON リソースファイルを用意するか?」です。1つ1つ翻訳サービスなどを使いながら作る、という方法もありますが、そんな言語リソースファイルの翻訳作業は IBM Cloud の Globalization Pipeline サービスを使うと英語のリソースファイルから各言語に翻訳したリソースファイルをまとめて作ることができてとても便利です。このサービスについては以前のブログで使い方も含めて紹介しているので参照ください:
Globalization Pipeline サービスがリリースされました!


と、最後は宣伝でしたw

アプリケーションの国際化を意識する場合に多言語対応は避けて通れなくなります。要するに画面に表示される文字列を変数化して、実際の表示に使われる中身をブラウザの言語設定などから動的に変更して表示する、という考え方です。したがって各変数に対応する文字列を各言語ごとに用意する必要があります。特に Java などでは .properties リソースファイルとして用意します。

このリソースファイル、日本語と英語程度であれば普通に用意できるかもしれませんが、多言語対応のメリットを考えると、フランス語やドイツ語、中国語など、リソースファイルを用意するだけでその言語向けの画面を用意できます(そのための多言語対応です)。しかしこれらの言語にリソースファイルを翻訳するのは言うほど簡単ではないはずです。事実、自分も日本語と英語以外では翻訳サービスを併用しないと無理だし、その結果作成されたリソースファイルがどの程度正しいのか(おかしくないのか)は正直よくわかりません。ましてやアラビア語のような表記方向すら異なるような文字だと、リソースファイルの編集方法すらよくわかってなかったりします。

そんなリソースファイルの翻訳に便利なサービスの1つが IBM Bluemix から提供されている Globalization Pipeline です。以前からベータリリースはされていましたが、2016年7月に正式リリースを迎え、REST API による操作もサポートされました:
2016071501


使い方は基本言語となる英語のリソースファイルを用意すれば、日本語を含む9言語(元の英語まで含めると10言語)のリソースファイルを自動生成してくれます。使い方も簡単です:
2016071502


このサービスの使い方について、以前に(ベータだった頃に)ブログで紹介しています。ちとUIが変更になっているようなので、参考程度ですが:


日本時間の 2015/Nov/17 に IBM Bluemix 内に Globalization Pipeline サービスがベータリリースされました:
2015111701


このサービスはアプリケーションの国際化を促進するための機能を提供しています。具体的には特定言語で記述された文字リソースファイルを元に他言語翻訳した文字リソースファイルを生成してくれます。

具体的な使い方を以下で説明します。まずは翻訳元になる文字リソースファイルを用意します。この例では Java 言語でよく使われる .properties 形式の英語リソースファイル(resource.properties)を用意しました。このリソースを元にして英語以外の文字リソースを生成してみます:
common.body.dndavailable=Drag & Drop to change location.
common.body.htmltagavailable=Some HTML tags(ex. &lt;br/&gt;(=new line)) are available.
common.body.update=Information Updated.
common.body.updatelatlng=Geo Information Updated.
common.body.clickfordetail=Click for detail page.
common.body.clickdescformap=Click underlined description for map.
common.body.notactivated=(This image is not visible to other. Go Edit page to activate.)
common.body.activate=Activate
common.body.twitterlinkmsg='s twitter profile.
common.body.fordeveloper=For Developer
common.body.apidownload=Download ManholeMap API here
common.body.iphoneapplink=Download iPhone App.
common.body.apiziplastmodified=Last Modified
common.body.logintoseefaceicon=Sign in with Twitter to see face icon
common.body.nomsiesupported=MSIE not supported in this site.
common.body.imgoftoday=Manhole of Today
:

まず Bluemix 上に Globalization Pipeline サービスを生成します。ランタイムにバインドしてもしなくても構いません:
2015111701


Bluemix のダッシュボード画面などから生成したサービスを選びます:
2015111702


Globalization Pipeline サービスの画面が表示されます。最初は「概要」タブの内容が表示されているはずです。ここでは新規にリソースバンドルを定義するので、「新規バンドル」ボタンをクリックします:
2015111702


新規バンドルの定義画面になったらバンドルID(バンドルごとにつけるユニークな名称、以下の例では ManholeBundle)、翻訳元リソースファイルの言語(今回は英語リソースから翻訳するので英語)、「参照」ボタンをクリックして、翻訳元リソースファイルを指定し、そのファイルフォーマット(今回は Java の Properties ファイル形式)を指定します。そして翻訳先言語を指定するために「追加」をクリックします:
2015111703


今回は翻訳元のリソースファイルが英語で、その翻訳結果として「フランス語」、「日本語」、「韓国語」をチェックしました。実際に必要な翻訳結果を必要なだけ指定してください。
2015111704


自分が必要な翻訳言語が全て選択されていることを確認して「保存」ボタンをクリックします:
2015111705


しばらく処理が行われた後、画面は「バンドル」タブに切り替わります。成功していると「正常に保存されました」をいうメッセージが表示され、その下に自分が指定した翻訳元のバンドル ID が表示されています。処理結果を確認するため、「操作」と書かれた箇所の目のアイコンをクリックします:
2015111706


すると翻訳先として指定した言語ごとに翻訳結果が表示されています。この例では3つの言語それぞれで16箇所のリソース翻訳が行われ、全て成功していることがわかります。では日本語の翻訳結果を確認してみましょう。日本語と書かれた行の目のアイコンをクリックします:
2015111707


日本語リソースの翻訳結果が表示されます。ざっと見て翻訳結果が正しそうかどうかが分かります。翻訳結果として手で修正したい場合は、修正したい文字列の右にあるペンアイコンをクリックします:
2015111708


すると翻訳結果を編集できるダイアログがポップアップ表示されます。この中で翻訳を変更し、「更新」ボタンをクリックすると翻訳結果を上書きできます:
2015111709


改めて一覧で翻訳結果を確認します。編集が必要な箇所があれば上記手順で修正し、特に問題がなくなったら「ダウンロード」ボタンでこのリソースファイルをダウンロードしてみましょう:
2015111710


日本語リソースのダウンロード方法を確認するダイアログがポップアップ表示されます。ここではダウンロードファイルのフォーマットを指定しますが、今回は Java Properties ファイルを元に作ったので、翻訳結果も Java Properties ファイルにチェックして「ダウンロード」ボタンをクリックします:
2015111711


すると、(バンドルID)_(言語).properties というファイル名でダウンロードが開始されます:
2015111712


このダウンロード結果を改めてテキストエディタで開くとこのような結果になっていることが分かります。日本語部分が正しく数値参照文字列になっていることがわかります。この形式であればそのまま日本語プロパティファイルとして使えそうです:
#Generated by IBM Globalization
#Tue Nov 17 14:19:17 UTC 2015
common.body.nomsiesupported=MSIE\u306F\u3053\u306E\u30B5\u30A4\u30C8\u3067\u652F\u3048\u3089\u308C\u307E\u305B\u3093\u3067\u3057\u305F\u3002
common.body.update=\u60C5\u5831\u306F\u66F4\u65B0\u3057\u307E\u3057\u305F\u3002
common.body.iphoneapplink=iPhone\u9069\u7528\u6027\u3092\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9\u3057\u3066\u304F\u3060\u3055\u3044\u3002
common.body.apiziplastmodified=\u6700\u7D42\u5909\u66F4\u65E5
common.body.apidownload=ManholeMap API \u306E\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9
common.body.fordeveloper=\u958B\u767A\u8005\u306E\u305F\u3081\u306B
common.body.dndavailable=\u30C9\u30E9\u30C3\u30B0\u30FB\u30A2\u30F3\u30C9\u30FB\u30C9\u30ED\u30C3\u30D7\u3057\u3066\u5834\u6240\u3092\u5909\u3048\u3066\u4E0B\u3055\u3044\u3002
common.body.twitterlinkmsg=\u3055\u3048\u305A\u308A\u304C\u3001\u624B\u77ED\u306B\u8A18\u8FF0\u3059\u308B\u3068\u3044\u3046\u3053\u3068\u3067\u3059\u3002
common.body.clickdescformap=\u5730\u56F3\u306B\u306F\u4E0B\u7DDA\u3092\u3072\u304B\u308C\u305F\u8A18\u8FF0\u3092\u30AF\u30EA\u30C3\u30AF\u3057\u3066\u304F\u3060\u3055\u3044\u3002
common.body.imgoftoday=\u4ECA\u65E5\u306E\u30DE\u30F3\u30DB\u30FC\u30EB
common.body.notactivated=(\u3053\u306E\u30A4\u30E1\u30FC\u30B8\u306F\u3001\u305D\u306E\u4ED6\u3067\u898B\u308B\u3053\u3068\u304C\u3067\u304D\u307E\u305B\u3093\u3002\u6D3B\u52D5\u7684\u306B\u3059\u308B\u3079\u304D\u30DA\u30FC\u30B8\u3092\u7DE8\u96C6\u3057\u306B\u884C\u3063\u3066\u4E0B\u3055\u3044\u3002)
common.body.updatelatlng=\u5165\u308A\u6C5F\u60C5\u5831\u306F\u66F4\u65B0\u3057\u307E\u3057\u305F\u3002
common.body.htmltagavailable=\u3042\u308BHTML\u306F\u30BF\u30B0\u3092\u3064\u3051\u307E\u3059(ex\u3002<br/>(\=new\u7DDA))\u306F\u5229\u7528\u3067\u304D\u307E\u3059\u3002
common.body.logintoseefaceicon=\u3055\u3048\u305A\u308A\u3067\u767B\u9332\u3057\u3066\u9854\u30A2\u30A4\u30B3\u30F3\u3092\u898B\u3066\u4E0B\u3055\u3044
common.body.activate=\u6D3B\u52D5\u7684\u306B\u3057\u3066\u4E0B\u3055\u3044
common.body.clickfordetail=\u30AF\u30EA\u30C3\u30AF\u3059\u308B\u3068\u7D30\u90E8\u30DA\u30FC\u30B8\u306B\u306A\u308A\u307E\u3059\u3002
  :


同様にして、フランス語と韓国語の翻訳結果も確認できます。必要であれば同様に編集して、同様にダウンロード可能です:
2015111713

2015111714


確かにアプリケーションの国際化では翻訳が面倒だし、同じ処理を色んな言語に対して行わないと行けないという点は不便でした。こういったリソースファイル特化の翻訳サービスは多言語対応のアプリケーション開発を行う上では便利です。

このページのトップヘ