EatSmartシステム部ブログ

ウェブサイトの開発や運営に関する情報です。

仮想環境上でrailsを動かすまでの流れ

 今回は、イートスマート1年目エンジニアが仮想環境を構築後、railsを起動するまでの一連の流れをざっとみていきたいと思います。※VirtualBox及びCentOS7の各種設定については、割愛させて頂きます。

環境について

Windows 10
VirtualBox 5.2
・CentOS7
Ruby 2.5.0
Rails 5.2.0

パッケージのアップデート及びgitのインストールなど

sudo yum update
sudo yum install -y git
sudo yum install -y bzip2 gcc openssl-devel readline-devel zlib-devel

rbenv(rubyのバージョン管理に必要)及びruby-build(rubyのインストールに必要)の設定

git clone https://github.com/rbenv/rbenv.git ~/.rbenv
git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
上記のクローン後、~/.bash_profileなどにPATHの設定をして反映させる。うまくパスが通ると rvenb -v でバージョンの確認ができます。

事前の準備を終えた後、Ruby,Railsをそれぞれインストールしていきます。

Rubyのインストール

rvenb install -l インストール可能なrubyの一覧が確認できる
rvenb install 2.5.0 開発に必要なrubyのバージョンを入れる
rvenb global 2.5.0 システム上で使いたいrubyのバージョンを指定
ruby -v rubyのバージョン確認ができるようになる
which ruby 適宜パスが問題ないか確認

Railsのインストール

gem update --system Rubygem本体のアップデート
gem install --no-ri --no-rdoc rails ドキュメントを不要にするオプションをつけることでgemのインストールが早くなる
gem install -v 5.2.0 rails バージョン指定してインストール
rbenv rehash ~/.rbenv/shims/以下にコマンドを置いて、変更を反映させる

ざっくりではありますが、以上が大まかな流れになります!

rails sが成功するまで、いくつか難所がありますが基本❶ターミナルのエラー通りに試しパッケージなどで足りないものがあれば補っていき❷Google検索で先人達がハマった症例を参考に進めていけばゴールにたどり着けるはずです。

最後に自分がハマったポイント

bundle installで、gem一式をインストールすればほぼ設定完了なのですが、よくあるハマりポイントとして、rmagick(ruby上での画像処理に必要なライブラリ)とimagemagickのバージョンの対応が合わないと、rmagickがインストールできないことがあります。 以下の記事が代表的なケースの処方箋になります。

しかし、私のケースではそもそもimagemagickが入ってなかったことで、rmagickのインストールができないということが問題にハマりました。結局、yumからimagemagick及びそれに関連するライブラリを入れ解決。rmagickの利用には、imagemagickが必要になるので、同じような問題に当たった時にはその周辺の設定を疑っていくことで解決が早くなるかと思います。
参考:ImageMagick - Wikipedia
※おじい様の魔法使いのアイコンが素敵です。

Nginx/Apacheのアクセスログを可視化してみる

今回は、Nginx/Apacheアクセスログの可視化について書きたいと思います。

イートスマート社では、WEBサービスの解析にGoogleアナリティクスを利用しています。 Googleアナリティクスでサイトへ訪問するユーザーやアクセス状況の分析を行い、日々サービスの改善につなげています。 しかし、Googleアナリティクスだけではユーザーからのリクエストへどのような応答を行ったのか詳細はわかりません。 そこで、Nginx/Apacheアクセスログを可視化することにしてみました。

アクセスログの可視化

アクセスログの可視化には、Kibanaを利用することにしました。 KibanaとElasticsearchの環境構築にはDocker Imageを利用しました。

アクセスログの設定変更

大量のアクセスを捌くためにサーバのチューニングは行っていましたが、応答の詳細を把握するためにアクセスログの項目を見直しました。

Nginxへ設定したのは以下の項目です。

項目 説明
$request_uri リクエストされたページを判別するため
$status ユーザーへの応答を把握するため
$request_time ユーザーへの応答にかかった時間を把握するため
$upstream_addr リクエストを処理したバックエンドサーバを特定するため
$upstream_status バックエンドサーバの応答を把握するため
$upstream_response_time バックエンドサーバの応答にかかった時間を把握するため
$cookie_JSESSIONID リクエストされたセッションを特定するため

Apacheへ設定したのは以下の項目です。

項目 説明
%T ユーザーへの応答にかかった時間を把握するため
%D ユーザーへの応答にかかった時間を把握するため
%{JSESSIONID}C リクエストされたセッションを特定するため
%{%Y-%m-%dT%T}t.%{msec_frac}t%{%z}t Kibanaのリクエスト日時として利用するため

既存のログ解析スクリプトへの影響を少なくするため、末尾に追加しました。

LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %T %D %{JSESSIONID}C %{%Y-%m-%dT%T}t.%{msec_frac}t%{%z}t" combined-es

以上の設定を反映することで、可視化に必要なデータを揃えることが出来ました。

アクセスログの登録

Elasticsearchへアクセスログと登録する方法として、Bulk APIを利用しました。 当初はtd-agentを利用して登録しようとしましたが、以下の理由から利用を諦めました。

  • td-agentが導入されていないサーバが存在する
  • 本番環境のネットワークからElasticsearchが稼働するネットワークへ直接接続出来ない

登録するアクセスログは前日分のバックアップを利用します。(このため、可視化出来るのは翌日以降になってしまいます。) Bulk APIで登録するため、アクセスログJSON形式へ変換する必要があります。JSONへの変換は、Nginx/Apacheそれぞれ以下のように行いました。

Nginx

JSON=`echo "${line}" | perl -F'\t' -nale '%h=map{split/:/,$_,2}@F;print"{\"index\":{}}\\\\n{\"\@timestamp\":\"$h{time}\",\"request_uri\":\"$h{request_uri}\", \"status\":$h{status}, \"request_time\":$h{request_time},\"jsessionid\":\"$h{jsessionid}\"}"'`

Apache

JSON=`echo ${line} | awk -F'\\\\[|\\\\]|"' '{print $4" "$5" "$9}' | awk '{print "{\"index\":{}}\\\\n{\"@timestamp\":\""$6"\", \"request_uri\":\""$2"\", \"status\":"$4", \"request_time\":"$7/1000000"}"}'`

まとめ

以上で、Nginx/Apacheアクセスログの可視化を行うことが出来ました。 ここから、可視化したい内容のVisualizationやSaved Searchを作成してDashboardを作ることになります。 いまのところリクエスト応答の可視化に必要な項目に絞っていますが、今後必要に応じて項目の追加が行われると思います。

docker+Node.js(express+pg-promise)で簡単なAPIサーバーを作成

インフラの構成を変更して自由度が上がったので、サービスに必要な簡単なAPIをdocker+Node.jsで構築してみました。 eatsmart.hatenablog.com

今までNode.jsは、sassを使うために呪文のようにnpmを使った程度なので、一から調べながら始めたのですが、結構簡単にできたなあという印象です。

まあ、簡単な処理しかしないAPIですが。

環境の構築

まずは、環境を構築します。

Node.jsのプロジェクトを作るために、プロジェクトのルートフォルダで

$ npm init

と実行します。プロジェクトの諸元を聞かれるので、適当に答えます。 これにより、プロジェクトフォルダにpackage.jsonファイルが生成されます。

次に、Node.jsでRESTサービスを作るには、 Express - Node.js Web アプリケーション・フレームワーク というライブラリを使用するのが簡単らしいので、express(とbody-parser)をプロジェクトにインストールします。

$ npm install express --save
$ npm install body-parser --save

ここで、--saveオプションを付けることで、プロジェクトのpackage.jsonに依存関係を追記してくれるようです。

package.json

  "dependencies": {
    "body-parser": "^1.18.3",
    "express": "^4.16.3",
  }

さらに、今回はDBとしてPostgreSQLに接続するために、pg_promiseというライブラリを使用します。

$ npm install pg-promise --save

これで、package.jsonは、以下のようになりました。

  "dependencies": {
    "body-parser": "^1.18.3",
    "express": "^4.16.3",
    "pg-promise": "^8.4.6"
  }

実装

次は実装です。この辺はNode.js+expressの作法を調査して則ることにしました。

expressではリクエストのエンドポイントをディレクトリ単位でルーティングするのが良さそうですが、今回は現時点では機能が少ないので、アプリケーション本体のapp.jsと、データ(今回はタイムライン)の一覧を返すアクションのtimeine.jsで構成することにしました。

プロジェクトのルートから

/
├─app/
│ ├─app.js
│ └─routes/
│   └─timeline.js
└─package.json

という構成にしました。 (express-generatorというのを使って構成するのも良さそう。)

あとは、ざっくりapp.jsは

const express    = require('express');
const app        = express();
const bodyParser = require('body-parser');

app.use(bodyParser.urlencoded({ extended: true }));
/* 開発環境でのCORS対応のため
app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
  next();
});
*/
app.use('/timeline',require('./routes/timeline'));

//サーバ起動
const port = process.env.PORT || 3000;
app.listen(port);
console.log('server start listen on port ' + port);

としました。

/timeline以下をtimeline.jsにルーティングする設定をして、サーバーを起動する感じです。

途中のコメントアウトは、ローカル環境でwebサーバーとAPIサーバーを立てるとドメイン(localhostのポート番号)を違えるため、CORSに引っかかってしまうので許可するための設定です。

timeline.jsは、

const express = require('express');
const router = express.Router();
const pgp = require('pg-promise')();
const db = pgp(process.env.DB_CONNECT_STRING);

router.get('/list', function (req, res) {
  let name = req.query.name;

  db.any("SELECT TIMELINE_TEXT FROM TIMELINE_TABLE WHERE TIMELINE_NAME=$1",[name])
    .then(function (data) {
      let list = [];
      for(let result of data){
        list.push(result['timeline_text']);
      }
      res.json(list);
    });
});

module.exports = router;

としています。/timeline配下の/listを処理するようにしています。

DB接続は、環境変数で与えるようにしました。 クエリパラメータで与えられたnameという値をSQLプレースホルダーに渡して、結果の一覧をJSONで返却しています。

動作確認

DB接続先文字列を環境変数で与えるようにしたので、APIサーバーの起動は、プロジェクトルートフォルダで

$ export DB_CONNECT_STRING=postgres://[user]:[password]@[server]:[port]/[DB名]
$ node app/app.js

とします。

curlコマンドで動作の確認ができました。

$ curl http://server:3000/timeline/list?name=test
["timeline text 1","timeline text 2","timeline text 3","timeline text 4","timeline text 5"]

dockerコンテナ化

次に、本番サーバー用に、dockerコンテナ化します。 公式のnodeのdockerイメージがあるので、プロジェクトフォルダを詰め込んでnpm installするだけで良さそうです。

Dockerfileは以下のようにしました。

FROM node:10
MAINTAINER H.Miyake

ADD app ./app
ADD package.json ./

RUN npm install

CMD ["node","app/app.js"]

このファイルをプロジェクトのルートフォルダに置いて、gitにコミットします。 その後、(最近git-flow的にやっているので)releaseブランチを切って、

$ docker build -t [tag_name] http://XXXXX/XXXX/XXXX.git#release

でビルドできます。(以下でやっていましたね。)

eatsmart.hatenablog.com

あとは、imageをpushしておいて、本番サーバーで

$ docker pull [tag_name]
$ docker run -d \
--name [name] \
--restart=always \
--network=[docker network名] \
-e DB_CONNECT_STRING=postgres://[user]:[password]@[server]:[port]/[DB名] \
-it [tag_name]

で起動できます。

docker runのオプションとか、色々とネタはありますが、とりあえずそんな感じで!

Gitの基本操作について

EatSmartの新人エンジニアが第二回目のブログを更新したいと思います。
今回はGitの基本操作についてまとめてみました。

Gitとは

分散型バージョン管理システム
ローカルにリモートリポジトリの複製を作成し、複数人は各々のローカルで変更履歴を利用して自由にファイルファイルの編集やコミットができる
もともとはLinuxソースコードを効果的に管理すために開発された

主なツール:Github,GitBucket,BitBucuket,GitLab

リポジトリ

ファイルやディレクトリの状態を記録する場所。保存された状態は内容の変更履歴として格納されている。変更履歴を管理したいディレクトリをリポジトリの管理下に置くことでそのディレクトリ内のファイルやディレクトリの変更履歴を記録することができる

◇コミット

ディレクトリやファイルの状態を記録するための操作のこと
作業ディレクトリ上で一区切りついた時にコミットしてそれまでの作業を一旦保存する
記録するファイルを一時的に登録する場所をインデックスという

◇クローン

リモートリポジトリを複製してローカルリポジトリを作成すること
サーバーが保持しているデータをほぼ全てローカルにコピーする

◇ブランチ

作業履歴を枝分かれさせて記録していくためのもの

◆ masterブランチ

リポジトリに最初のコミットを行うと自動で作成されるブランチ
ブランチは複数作ることができブランチを作るにはすでにあるブランチをコピーしなくてはいけない
ブランチには名前を作ることができ開発者は自由にブランチを移動できる

最終的に全てのブランチはmasterブランチに結合される
各開発者は作業ごとにブランチを作る (=ブランチをきるという)

◆ブランチの作成
git branch ブランチ名

◆ブランチ間を移動する
git checkout ブランチ名



ローカルでの作業からリモートにアップデートするまでの流れ

ワーキングツリー   ⇒   インデックス(ステージング環境)
⇒  ローカルリポジトリ ⇒  リモートリポジトリ
◆ワーキングツリー
ファイルの編集作業場所でファイルの編集や追加、削除などを行う

◆インデックス
コミットするためのファイルを記録する場所
ワーキングツリーで編集したファイルは git add でインデックスへ移動させる

◆ローカルリポジトリ
リモートリポジトリにアップロードするためコミット履歴とファイルを記録する場所
インデックス上のファイルをgit commit でローカルリポジトリにコミットする

◆リモートリポジトリ
複数人で共有する場所
ローカルコミットしたファイルをgit pushでリモートリポジトリにアップロードする

◆プルリクエス
作成したブランチをmasterブランチにマージする時の確認作業のこと
差分を確認することができ、他の開発者から確認やレビューをしてもらうことができる

◆ブランチをマージする
マージしたブランチは削除する

コマンドの基本操作

git add ワーキングツリー上で編集したファイルをステージング環境へ追加する
git commit ローカルコミットを実行する
git commit -m 新規コミットを作成する
git commit -amend 直前のコミットを上書きする
git push ローカルリポジトリのブランチをリモートリポジトリへアップロードする
git log コミット履歴を表示する
git log --merge マージコミットだけを表示する
git reflog HEADの履歴一覧を表示する
git reset HEADの位置を変更する
git remote リモートリポジトリの最新状態をローカルリポジトリのリモート追跡ブランチにダウンロードする
git merge 指定したブランチを現在のブランチに統合する
git pull   最新のリモートリポジトリの内容をローカルリポジトリに反映する
git stash まだローカルコミットしていないファイルを一時的に待避させる
git cherry-pick 他ブランチの特定のコミットを現在のブランチに取り込む


Gitを使い始めた時はマスターブランチに結合?プルする?ワーキングツリーって何?
仕組みを理解するのに凄く苦労しましたがGitHub Desktopを使ったりして操作に慣れていきました。GUIで比較的簡単に操作ができるので初学者にはオススメのツールだと思います。

GitHub Desktopのメリット

◆開発ツール、エディターに依存しない
パソコン環境と統合されているのでお気に入りのソフトウェアを使って編集することができます
◆画像の差分は表示される
◆コマンドを抽象化しGitコマンドを知らなくても簡単に使える
コマンドラインに比べて機能が絞られていますが非常にシンプルな操作性となっている
◆最速のブランチ切り替え(checkout/stash不要)
◆簡単にリモートにpushできる
Publlish/Syncボタン押すだけでローカルリポジトリとリモートリポジトリを同期させることができる

FacebookAPIのpublish_actions廃止について

2018年4月24日にセキュリティ向上の目的でFacebookから発表があった、 "New Facebook Platform Product Changes and Policy Updates" の「publish_actions」廃止について簡単にまとめてみました。

developers.facebook.com

「publish_actions」とは

ユーザーに代わって、アプリがFacebookAPI経由でタイムラインに投稿する際に「publish_actions」という権限が必要でした。
また、アプリで「publish_actions」の権限を取得するには、Facebookの審査を通す必要がありました。
※FacebookAPI Ver1.0の時代は「publish_actions」の取得に関して審査不要でしたが、Ver2.0から審査が必要となりました。

Facebookが「publish_actions」を廃止にした経緯

レイバンスパム」の様な、Facebook経由でのスパム対策の強化になります。

「publish_actions」廃止による影響

4月24日時点で「publish_actions」は廃止(審査にも提出できない)となっており、既にpublish_actions取得しているアプリに関しては、8/1まで使用可能でした。
また、引き続きアプリ経由でユーザーのタイムラインに投稿する場合、Facebookが提供しているシェアダイアログに実装を替える必要があります。

アプリへの影響

自分携わっているアプリは、元々FacebookAPI経由でタイムライン投稿していましたが、Ver2.0(2014年)で審査が必要になった時点でシェアダイアログに切り替えを行っていたので、タイムライン投稿に関する改修は、今回は行いませんでした。
また、「Facebook Login」のスコープに「publish_actions」の権限が残っていた為、設定を削除しました。


今回の「publish_actions」の廃止は、インパクトが大きい割りに(3ヵ月強の)短期で仕様変更された為、移行に苦労されている方も多いと思われますが、少しでも参考になれば幸いです。

環境構築の際に行った工夫

以前、以下の記事でデータセンターからクラウドへ移行したことを書きました。

eatsmart.hatenablog.com

このとき、以前から利用していたDockerの適用範囲を広げ、Apache/Tomcat等もコンテナ化しました。 あわせて環境構築のためAnsibleの導入も行い、Nginxなどのミドルウェアの設定を環境ごとに定義して利用することにしました。 これにより、

  • 環境構築が簡単になった
  • 本番環境・検証環境・ステージ環境の構築手順を共通に出来た
  • 本番環境と同一の手順で設定の反映の検証が行えるようになった

など、メリットがありました。

今回は、環境構築の際に行った工夫を書きたいと思います。

Ansibleでの環境構築

まずはインフラ設計を元に、必要なサーバ/ミドルウェアを洗い出します。 次に、セットアップするために必要な手順をAnsibleのplaybookへ書きます。 この時、環境ごとに共通する箇所と異なる箇所を確認して、各環境ごとに異なる箇所の設定をホストへ定義しました。 こうすることで、各環境ごとのセットアップの手順が同一となり、手間と失敗を防ぐことが出来ました。

Ansibleを利用したことで、設定をコードにすることが出来たこと、バージョン管理が出来たので後から変更履歴を把握することが出来たので作業がスムーズに進みました。 移行後に行った変更も同じ手順で作業することで、サーバの追加やミドルウェアのインストールも同様に行うことが出来ます。

環境ごとに異なる箇所は、主に以下の3つです。

サーバの名前とIPアドレス

各環境でサーバの接続手順を統一するため、IPアドレスではなく名前を利用するようにしました。 データベースに接続するにはdb01と指定すれば、各環境のdb01へ接続出来ます。 これにより、各種設定ファイルの定義を環境に応じて切り替える必要が無くなりました。

名前解決のため、内部向けにBINDでDNSサーバをセットアップして利用しています。セットアップ時に環境ごとのIPアドレスを設定ファイルへ反映します。 このセットアップにもAnsibleを利用し、DNSの設定も同様にホストで管理しています。 各環境のローカルIPを共通にするという方法もありますが、障害時やメンテナンス時にサーバを切り替えることも想定し、名前を利用を選択しました。。

サービスに利用するホスト名

もぐナビなら、mognavi.jp/mognavi.dev/mognaiv.stgのように、環境ごとに異なるホスト名があります。 環境ごとに異なる設定は環境変数を参照する方針をとりました。これはサーバ/Dockerともに共通します。 例えばホスト名は、環境変数"HOST_MOGNAVI"を参照して利用するようにしています。 ミドルウェアやDockerコンテナでは環境変数から設定を行うようにすることで、各環境で同一の設定ファイルを利用することが出来ました。

ミドルウェアのパラメータ

各環境で異なるハードウェアのスペックや外部要因を吸収しています。 本番環境ほどリソースに余裕が無い場合や処理能力が不要な場合、ミドルウェアが利用するメモリの量を減らしています。 また、本番環境以外で不要な設定を無効にしたりもしています。

リバースプロキシのセットアップ

リバースプロキシとしてNginxを導入し、コンテンツのキャッシュとSSLの処理に利用しています。 各環境で利用するにあたり、バックエンドの指定をIPアドレスではなく名前で行うようにしました。 念の為パフォーマンスのしましたが、IPアドレスに比べ低下することはありませんでした。

SSLの処理で利用する証明書は、ホスト名ごとのリポジトリで管理しています。 ホスト名は環境変数から環境ごとのものを参照して利用します。

Apacheのセットアップ

URLのリライトやUAに応じたリダイレクト等、アプリケーション固有の処理に利用しています。 ApacheはDockerコンテナで稼働するため、起動時にDockerホストから環境変数を通してホスト名を指定します。 Apacheのconfiでは、以下のように環境変数を参照して利用することが可能です。

# ${HOST_MOGNAVI}の部分がmognavi.jpになる
ServerName ${HOST_MOGNAVI}

# ${HOST_MOGNAVI_SP}の部分がs.mognavi.jpになる
RewriteCond %{ENV:device_type} sp [NC]
RewriteRule ^/$ https://${HOST_MOGNAVI_SP}/ [R=301,NE,L]

設定の中の環境に依存する箇所を環境変数から参照するようにすることで、各環境で共通の設定ファイルを利用出来るようになりました。

まとめ

移行に伴い環境構築で行った工夫を書きました。 Ansibleを利用することで設定のコード化/バージョン管理が出来たので、全体の把握が行いやすくなりました。 以前は環境ごとに設定ファイルを作成しており環境ごとに差異が出来てしまうことがありました。環境変数を利用することで、設定ファイルの共通化とバージョン管理を行うことが出来ました。

tomcat上のwebアプリケーションをコンテナ化する

アプリケーションの稼働環境をdockerコンテナ化することは、環境の可搬性やネットワークの自由度などとても有用なので、弊社のサービスをコンテナ化しています。

先日、tomcatで動作しているwebアプリケーションをコンテナ化した際にやったことをブログに残したいと思います。

tomcatのwebアプリをコンテナに入れるにあたり、以下のことを行いました。

  1. mavenで構成管理する
  2. tomcat-maven-pluginでwebアプリを実行する
  3. tomcat-maven-pluginで実行可能jarを作成する
  4. 実行可能jarを入れたコンテナを作成する

mavenで構成管理する

もともとビルドのタスク実行はantを使用していたのですが、ライブラリの依存性管理や開発環境構築の利便性のために、mavenで構成管理をすることにしました。

ただ、単純にtomcatのWEB-INF/libフォルダ配下にjarファイルを置いて、全てをクラスパスに追加すれば良かった(もっともそのせいで、ライブラリ同士の依存関係が全く分からなくなっていたのですが…)のが、全てをpom.xmlに定義が必要となりました。

依存関係が管理されていなかったのでバージョンが不明だったり、全てのdependencyを記述するのが大変だったので、

<dependency>
    <groupId>jai_codec.jar</groupId>
    <artifactId>jai_codec.jar</artifactId>
    <version>1.0</version>
    <scope>system</scope>
    <systemPath>${basedir}/src/WEB-INF/lib/jai_codec.jar</systemPath>
</dependency>

のように、元々のWEB-INF/lib配下のjarにsystemスコープで定義しているところもあります。 これでは依存性の解決にはなっていませんが…。

tomcat-maven-pluginでwebアプリを実行する

mavenで構成管理を行うようにしたので、tomcat-maven-plugin(Apache Tomcat Maven Plugin - About Apache Tomcat Maven Plugin)を使ってtomcatmavenで起動するようにしました。

今回のアプリではDB接続にJNDIを使用しているのですが、JNDI定義はtomcatのcontextファイルに記述しています。 pluginの定義でも指定のcontextファイルを使用するように、configurationのcontextFileで指定します。

また、古いtomcatの頃の名残で、JSPスクリプトレット内でエスケープ無しにダブルクォートしていたり(Tomcat5.5.27、6.0.18からJSPのスクリプトレットなどの中でクォートを使用する際にエスケープが必要になった - 不会忘記的一天)するので、STRICT_QUOTE_ESCAPINGをfalseに設定します。

結果、pom.xmlに以下の定義を追加しました。 pluginのバージョンが2.1の理由は後述します。

<plugin>
    <groupId>org.apache.tomcat.maven</groupId>
    <artifactId>tomcat7-maven-plugin</artifactId>
    <version>2.1</version>
    <configuration>
        <contextFile>${project.basedir}/context.xml</contextFile>
        <systemProperties>
            <org.apache.jasper.compiler.Parser.STRICT_QUOTE_ESCAPING>false</org.apache.jasper.compiler.Parser.STRICT_QUOTE_ESCAPING>
        </systemProperties>
    </configuration>
</plugin>

これで、mvn tomcat7:runで起動するようになりました。

$ mvn tomcat7:run

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building XXXXXXXXXXXXXXXXXx
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] >>> tomcat7-maven-plugin:2.1:run (default-cli) > process-classes @ XXXXXXXX >>>
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ XXXXXXXX ---
・・・
・・・
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ XXXXXXXX ---
[INFO] Changes detected - recompiling the module!
・・・
・・・
[INFO] <<< tomcat7-maven-plugin:2.1:run (default-cli) < process-classes @ XXXXXXXX <<<
[INFO]
[INFO]
[INFO] --- tomcat7-maven-plugin:2.1:run (default-cli) @ XXXXXXXX ---
[INFO] Running war on http://localhost:8080/XXXXXXXX
[INFO] Creating Tomcat server configuration at /XXXXXXXX/XXXXXXXX/target/tomcat
[INFO] create webapp with contextPath: /XXXXXXXX
8 02, 2018 7:30:37 午後 org.apache.coyote.AbstractProtocol init
情報: Initializing ProtocolHandler ["http-bio-8080"]
8 02, 2018 7:30:37 午後 org.apache.catalina.core.StandardService startInternal
情報: Starting service Tomcat
8 02, 2018 7:30:37 午後 org.apache.catalina.core.StandardEngine startInternal
情報: Starting Servlet Engine: Apache Tomcat/7.0.37
8 02, 2018 7:30:41 午後 org.apache.catalina.core.ApplicationContext log

tomcat-maven-pluginで実行可能jarを作成する

tomcat-maven-pluginを使用して、実行可能jarを作る事ができます。コンテナ構築がjarファイルのコピーで済むと簡単で良いので、実行可能jarを作ってみます。

まず、pluginの定義にexec-war-onlyゴールを追加します。 また、実行するtomcatでJNDIを使うためにはenableNamingの設定が必要なようです(Apache Tomcat Maven Plugin :: Tomcat 7.x - tomcat7:standalone-war)。他にも色々と設定ができるようなので、参考にしてみて下さい。

ここで、以下のようにpluginのバージョン2.2、2.3にはexec-war-onlyが正常に動作しないとのことで、2.1を使用しました。

で、最終的に、pluginの設定は以下のようになりました。

<plugin>
    <groupId>org.apache.tomcat.maven</groupId>
    <artifactId>tomcat7-maven-plugin</artifactId>
    <version>2.1</version>
    <configuration>
        <contextFile>${project.basedir}/context.xml</contextFile>
    </configuration>
    <executions>
        <execution>
            <id>tomcat-run</id>
            <goals>
                <goal>exec-war-only</goal>
            </goals>
            <phase>package</phase>
            <configuration>
                <path>XXXXX[作成するJARの名前]</path>
                <enableNaming>true</enableNaming>
            </configuration>
        </execution>
    </executions>
</plugin>

ここでmvn packageコマンドを実行すると

$ mvn package

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building XXXXXXXX
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ XXXXXXXX ---
[INFO] Copying 1 resource
・・・
・・・
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ XXXXXXXX ---
[INFO] Changes detected - recompiling the module!
・・・
・・・
[INFO] --- maven-war-plugin:2.2:war (default-war) @ XXXXXXXX ---
[INFO] Packaging webapp
[INFO] Assembling webapp [XXXXXXXX] in [/XXXXXXXX/XXXXXXXX/target/XXXXXXXX]
[INFO] Processing war project
[INFO] Copying webapp resources [/XXXXXXXX/XXXXXXXX/src/main/webapp]
[INFO] Webapp assembled in [3288 msecs]
[INFO] Building war: /XXXXXXXX/XXXXXXXX/target/XXXXXXXX.war
[INFO] WEB-INF/web.xml already added, skipping
[INFO]
[INFO] --- tomcat7-maven-plugin:2.1:exec-war-only (tomcat-run) @ XXXXXXXX ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 15.023 s
[INFO] Finished at: 2018-08-02T19:58:47+09:00
[INFO] Final Memory: 49M/275M
[INFO] ------------------------------------------------------------------------

とjarの作成が成功しました。

さらにtargetフォルダの下にできたjarファイルを実行することで

$ java -jar ./target/XXXXXXXX-war-exec.jar

8 02, 2018 8:00:11 午後 org.apache.catalina.core.StandardContext setPath
8 02, 2018 8:00:11 午後 org.apache.coyote.AbstractProtocol init
情報: Initializing ProtocolHandler ["http-bio-8080"]
8 02, 2018 8:00:11 午後 org.apache.catalina.core.StandardService startInternal
情報: Starting service Tomcat
8 02, 2018 8:00:11 午後 org.apache.catalina.core.StandardEngine startInternal
情報: Starting Servlet Engine: Apache Tomcat/7.0.37
8 02, 2018 8:00:18 午後 org.apache.catalina.core.ApplicationContext log
情報: No Spring WebApplicationInitializer types detected on classpath

と、tomcatを使ってwebアプリを起動することができました。

実行可能jarを入れたコンテナを作成する

あとは、javaが動くコンテナ内にjarを入れれば良いのですが、今回は接続先DB等を外部化するために、起動時にパラメータでserver.xmlを指定するようにしてみました。

server.xmlのパスは、javaコマンドで起動時に-serverXmlPathで指定することができます(Apache Tomcat Maven Plugin - Executable War )。

#ビルド時にpom.xmlのserverXml項目で指定することもできます(Apache Tomcat Maven Plugin :: Tomcat 7.x - tomcat7:standalone-war)。

今回は、jarファイル、JNDIのDB定義をしたserver.xmlファイル、コンテナ起動時にtomcatを起動するスクリプトを、コンテナ内の同一フォルダに配置するようにしました。

起動スクリプトは、startup.shという名前で

#!/bin/sh
DIR=$1
cd $DIR
java -Xmx1024m -jar $DIR/XXXXXXXX-war-exec.jar -serverXmlPath $DIR/server.xml

というファイルを用意し、以下の内容のDockerfileを作りました。

・・・
・・・
 
RUN mkdir -p /usr/local/XXXXXXXX
ADD ./startup.sh /usr/local/XXXXXXXX/
ADD ./server.xml /usr/local/XXXXXXXX/
ADD ./XXXXXXXX-war-exec.jar /usr/local/XXXXXXXX/
 
EXPOSE 8080/tcp
 
CMD ["/usr/local/XXXXXXXX/startup.sh","/usr/local/XXXXXXXX"]

あとは、コンテナをbuildして、

$ docker build -t esma/XXXXXXXX ./

Sending build context to Docker daemon  39.85MB
・・・
・・・
Removing intermediate container 81eaa50676f6
 ---> aa9795082fde
Step 3/8 : RUN mkdir -p /usr/local/XXXXXXXX
 ---> Running in 0b34a06c54bd
Removing intermediate container 0b34a06c54bd
 ---> 2d4acc4a17a0
Step 4/8 : ADD ./startup.sh /usr/local/XXXXXXXX/
 ---> affd7178601c
Step 5/8 : ADD ./server.xml /usr/local/XXXXXXXX/
 ---> 836ca962776d
Step 6/8 : ADD ./XXXXXXXX-war-exec.jar /usr/local/XXXXXXXX/
 ---> b1477fde89e0
Step 7/8 : EXPOSE 8080/tcp
 ---> Running in 5c77492af7da
Removing intermediate container 5c77492af7da
 ---> 3e656de10f13
Step 8/8 : CMD ["/usr/local/XXXXXXXX/startup.sh","/usr/local/XXXXXXXX"]
 ---> Running in a61e2cf508ca
Removing intermediate container a61e2cf508ca
 ---> 2c7c01b9d549
Successfully built 2c7c01b9d549
Successfully tagged esma/XXXXXXXX:latest

そして、実行すると

$ docker run -d --name=XXXXXXXX -p 8080:8080 -it esma/XXXXXXXX

aef000541e0795693610849743dc0b97835d5b54b9d3de26e31e606eb9bdbc86

$ docker logs XXXXXXXX

Aug 02, 2018 3:16:32 PM org.apache.catalina.core.AprLifecycleListener init
INFO: The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: /usr/java/packages/lib/amd64:/usr/lib/x86_64-linux-gnu/jni:/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu:/usr/lib/jni:/lib:/usr/lib
Aug 02, 2018 3:16:33 PM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-bio-8080"]
Aug 02, 2018 3:16:33 PM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["ajp-bio-8009"]
Aug 02, 2018 3:16:33 PM org.apache.catalina.startup.Catalina load
INFO: Initialization processed in 1104 ms
・・・
・・・
Aug 02, 2018 3:16:37 PM org.apache.catalina.core.ApplicationContext log
INFO: Initializing Spring FrameworkServlet 'dispatcherServlet'
Aug 02, 2018 3:16:39 PM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-bio-8080"]
Aug 02, 2018 3:16:39 PM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["ajp-bio-8009"]
Aug 02, 2018 3:16:39 PM org.apache.catalina.startup.Catalina start
INFO: Server startup in 6363 ms
Aug 02, 2018 3:16:49 PM org.apache.naming.NamingContext lookup

と、無事コンテナ内でwebアプリを起動することができました。