桜技録

🐈🐈🐈🐈🐘

GradleからTomcat Manager Appを操作してwarをデプロイする

Gradleを使ってwarファイルをTomcatにデプロイしたいという場面に於いて、warファイルを$CATALINA_BASE/webappsにコピーする手法をよく見かけるが、それとは異なる手法としてTomcatにデフォルトで入っているManager AppをGradleから使う術を記しておく。

warファイルのコピーは単純だがGradleから見てコピー先がローカルなのかネットワーク上なのかDockerコンテナ内なのかを意識する必要がある。一方HTTPクライアントからManager Appを叩く分にはそのような環境差異を意識する必要はなく複数の異なる環境でもビルド・スクリプトを共有しやすい。

前準備

まずはManager AppをGUIレスで使えるようにする。

Tomcatをインストールした直後はManager Appを始めとしたプリインストールのアプリは$CATALINA_BASE/webapps.distにありデフォルトで無効になっている。これを$CATALINA_BASE/webappsから見えるようにする。

下はシンボリック・リンクを用いた例。

ln -s \
  $CATALINA_BASE/webapps.dist/manager \
  $CATALINA_BASE/webapps/

続いて$CATALINA_BASE/conf/tomcat-user.xmlを編集しmanager-scriptロールを持つユーザを作成する。

<tomcat-users xmlns="http://tomcat.apache.org/xml"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
              version="1.0">
  <role rolename="manager-script"/>
  <user username="USERNAME" password="PASSWORD" roles="manager-script"/>
</tomcat-users>

WebブラウザからGUIでManager Appを使う為のmanager-guiというロールもあるが同一ユーザにmanager-guiとmanager-scriptを一緒に持たせないことが推奨されている*1。(もしGUIも使うのであれば)GUI用とスクリプト用でユーザを分けるのが良いだろう。

最後にManager Appへの接続を許可するアクセス元アドレスの範囲を必要に応じて$CATALINA_BASE/conf/[enginename]/[hostname]/manager.xmlに設定する。

デフォルトの設定は$CATALINA_BASE/webapps.dist/manager/META-INF/context.xmlにありループバック・アドレスからのみを許可している。ネットワーク上のサーバであれば管理セグメントの特定ノードからは許可する等といった変更がいるだろう。

下は192.168.1.*からのアクセスを許可する例。

<Context privileged="true" antiResourceLocking="false">
   <Valve className="org.apache.catalina.valves.RemoteAddrValve" 
          allow="192\.168\.1\.\d+" />
</Context>

以上の設定を済ましてTomcatを再起動すれば作成したユーザでリモート・デプロイが可能になる。

Gradleでデプロイ

下例ではHTTPクライアントとして個人的にGradleで使いやすいと感じているgradle-http-pluginでdeployタスクを定義してみた。

USERNAMEPASSWORDは上で作成したmanager-scriptユーザのもの。外部定義して環境毎にURLとユーザを切り替えれば複数環境でタスクを共有出来る。

import groovyx.net.http.NativeHandlers
import io.github.httpbuilderng.http.HttpTask

plugins {
  id 'war'
  id 'io.github.http-builder-ng.http-plugin' version '0.1.1'
}

task deploy(type: HttpTask, dependsOn: 'war') {
  config {
    request.uri = 'http://example.com:8080'
    request.auth.basic('USERNAME', 'PASSWORD')
    request.encoder('application/octet-stream', NativeHandlers.Encoders.&handleRawUpload)
  }
  put {
    request.uri.path = '/manager/text/deploy'
    request.uri.query = [path: "/${war.archiveBaseName.get()}", update: true]
    request.contentType = 'application/octet-stream'
    request.body = war.archiveFile.get().asFile
    response.success { fs, body ->
      def message = body as String
      println(message)
      if (message.startsWith('FAIL')) throw new Exception(message)
    }
  }
}

ポイントとなるのはrequest.uri.queryに渡しているクエリパラメータ。

  1. path:対象アプリのコンテキストパスを/myappのようにスラッシュ始まりで指定する。GUIのManager Appやwar配置と違って省略することは出来ない。ROOTアプリケーションの場合は単に/を指定する。ここでは/ + warファイルのベース名を指定している。

  2. update:pathで指定したコンテキストパスにアプリがすでにあった場合の動作を制御する。false(デフォルト値)ならエラーとし、trueならば既存アプリをアンデプロイした後warをデプロイする。繰り返し使用する使途ではtrue固定で良いだろう。

その他に指定可能なパラメータやデプロイ以外に用意されているAPI(アンデプロイやアプリの一覧取得)は公式マニュアルが詳しい。

おまけ:curlでデプロイ

curlでも十分戦える。

curl --basic --user USERNAME:PASSWORD \
  --upload-file build/libs/myapp.war \
  'http://example.com:8080/manager/text/deploy?path=/myapp&update=true'