Heroku を使って Java の Web アプリを作ってみる。
#基本的な話
##Heroku とは
PaaS の1つ。
Web アプリケーションを動かすための土台(プラットフォーム)を提供してくれるクラウドサービス。
Google でいうと Google App Engine、 Amazon でいうと AWS Elastic Beanstalk とかが同じようなサービスに当たる。
最初は Ruby (Ruby on Rails)をサポートした PaaS だったけど、現在は様々な言語によるプラットフォームをサポートしている。
Java もサポートされている。
データベースは、 PostgreSQL や MySQL などを使える。
ただし、無償利用の場合はデータ量などに制限がある。
- アプリケーションのソースコードは Git を使って管理する。
- Heroku 上の Git リポジトリに Push すると、アプリケーションをビルドし、必要なファイルをかき集めて Slug と呼ばれるパッケージが作成される。
- Slug は Dyno と呼ばれる仮想の Unix コンテナにデプロイされ実行される。
- 実行された Web アプリには、インターネット越しにブラウザなどからアクセスできるようになる。
##アプリケーション
Heroku 上でプログラムを動かすために必要なソースコードや設定ファイルなど一式のこと。
- ソースコード
- 依存ファイル(pom.xml, build.gradle, Gemfile などなど)
- Procfile(Heroku 上で実行するコマンドを定義したファイル。詳細後述)
Heroku では、これらをまとめたものを「アプリケーション」と定義している。
##Slug
アプリケーションを Heroku 上で実行するために、ビルド、パッケージングして圧縮したもの。
- ソースコードをビルドしてできた実行ファイル。
- 依存するライブラリ一式。
- 言語の実行環境。
これらをまとめたものを、 Slug と呼ぶ。
##Buildpacks
Slug を構築する方法を定義したスクリプト。
Heroku は標準で Ruby, Node.js, Clojure, Java など様々な Buidlpacks を用意している。
これらは全てオープンソースで、 GitHub 上で開発されている(Buildpacks | Heroku Dev Center)。
標準でサポートされていない言語やフレームワークでも、 Buildpacks を作成すれば Heroku 上で動かすことができる。
##Dyno
アプリケーションを実行するための実行環境。
仮想の Unix コンテナのようなもの。
Heroku のサービスでは、この Dyno を単位にして性能などが説明されている。
たとえば、 Dyno 1つだとメモリは 512 MBだとか、1つの Dyno が処理できるスレッドは 256 個まで、などなど。
デフォルトでは、1アプリケーションにつき1つの Dyno が割り当てられる。
複数の Dyno を割り当てることで性能を向上させることができるが、有償になる。
##無償利用する場合の制限
こちら に様々な制限事項が記載されている。
主要そうなのをいくつかピックアップする。
###Dyno の制限
####メモリ
1Dyno のメモリは 512MB。
####稼働時間
1アプリケーションあたり、1Dyno 月 750 時間までは無料で稼働させられる。
1ヶ月 = 31日とした場合、月合計は 744 時間なので、普通に使う限りでは無料で使用することができる。
しかし、1つのアプリケーションに 2 つ以上の Dyno を割り当てたり、バッチ処理などのプロセスを別途稼働させた場合、それらの時間も合計されることになり、 750 時間の制限をオーバーすることになる(有償になる)。
###Slug のサイズ制限
300MB が上限。
Heroku に git push
すると、リポジトリの内容がパッケージング・圧縮され、 Slug と呼ばれるファイルが作成される。
このサイズの上限が 300MB。
※翻訳が古いためか、最大サイズが 200MB となっているが、 2014/11/22 現在は 300MB が上限の模様。
Your slug size is displayed at the end of a successful compile. The maximum slug size is 300MB; most apps should be far below this limit.
###データベースの制限
PostgreSQL を無償で利用する場合は Hobby Tier
というプランになり、以下のような制限がある。
- 登録できるデータは、全部で 10,000 行。
- 同時に開けるコネクションの数は、 20 まで。
#Hello World
Java の Getting Started を見ながら、 Java で Web アプリを動かすところまでやってみる。
##開発マシンの環境
###OS
Windows7 64bit SP1
###Git
Git for Windows 1.9.4
###SSH
Cygwin でインストール。
%CYGWIN_HOME%\bin
にパスを通して、 ssh
や ssh-keygen
が使える状態にしておく。
####Git から使用する SSH を指定する
環境変数 GIT_SSH
に Cygwin と一緒にインストールした ssh.exe のパスを設定しておく。
###Maven
Maven 3.2.3
※Maven3 が必要みたいです。
##Heroku のアカウントを作成する
公式サイト から Sign Up
して、アカウントを作成する。
必要なのはメールアドレスだけで、無償で作成可能。
##Heroku の CLI ツールをインストールする
こちら の Download Heroku Toolbelt for Windows
をクリックしてインストーラをダウンロード。
フルインストールすると Git と SSH もインストールされるみたいだが、自分の環境にはすでに Git と SSH はインストールしているので、 Custom Installation
を選択して Heroku Client
と Foreman
だけをインスト―ルした。
インストールが完了したら、コマンドラインを開いて以下コマンドを実行。
> heroku version
heroku/toolbelt/3.12.1 (i386-mingw32) ruby/1.9.3
##Heroku Toolbelt で Heroku にログインする
> heroku login
Enter your Heroku credentials.
Email: <Heroku アカウントのメールアドレス>
Password (typing will be hidden):<パスワード>
メールアドレスとパスワードを聞かれるので、先ほど作成したアカウントの情報を入力する。
続いて SSH の鍵を作成するか尋ねられる。
Windows 環境だと、当然 ~/.ssh/id_rsa.pub
なんてパスは無効なので、 Y
を選択してもエラーになって終了する。
なので、別途 ssh-keygen
で鍵を作る。
##SSH の鍵を作成して登録する
任意のフォルダで以下のコマンドを実行。
> ssh-keygen.exe -t rsa
質問は全て「未入力 Enter」でOK。
<Cygwin インストールフォルダ>/home/<ユーザー名>/.ssh
の下に、秘密鍵(id_rsa
)と公開鍵(id_rsa.pub
)が生成される。
> heroku keys:add <先ほど作成した id_rsa.pub のパス>
Uploading SSH public key id_rsa.pub... done
これで鍵が登録される。
登録されているかどうかは heroku keys
コマンドで確認できる。
##アプリケーションを作成する
> heroku create
Creating shielded-retreat-8659... done, stack is cedar-14
https://shielded-retreat-8659.herokuapp.com/ | git@heroku.com:shielded-retreat-8659.git
上記コマンドでアプリケーションが新規に作成される。
作成が完了すると、アプリケーションの URL と Git リポジトリの URL が表示される。
試しにアプリケーションの URL にアクセスしてみる。
###アプリケーション作成時に名前を指定する
> heroku create <アプリケーション名>
-
create
時に任意の名前を指定することができる。 - 未指定の場合はランダムな文字列が使用される。
###アプリケーションの名前を変更する
> heroku apps:rename <変更後の名前> --app <変更前の名前>
変更が完了すると、変更後の Web アプリと Git リポジトリの URL が出力される。
なお、変更後の名前が既に使われている場合は、 Name is already taken
というエラーメッセージが表示される。
##サンプルプロジェクトを取得する
Java のサンプルプロジェクトが Git リポジトリとして公開されているので、それをクローンしてくる。
> git clone https://github.com/heroku/java-getting-started.git
##プロジェクトを Heroku にアップする
先ほどクローンしてきた Git リポジトリのルートに移動して、リモートリポジトリを追加する。
> cd java-getting-started
> git remote add heroku <git リポジトリのパス>
heroku
という名前でアプリの Git リポジトリを登録したので、次はこのリポジトリに push する。
> git push heroku master
なにやら Maven の出力が大量に出力されたのち、 BUILD SUCCESS
と出て終了する。
さっそくブラウザから Web アプリにアクセスしてみる。
URL を直接ブラウザで指定してもいいけど、以下のコマンドを打つことで Web ブラウザを起動することもできる。
> heroku open
パスに /db
を追加すると、データベース(PostgreSQL)を使用したサンプルページが表示される。
アクセスするたびに時刻が記録され、結果が画面に表示される。
##各ファイルの意味とか
サンプルプロジェクトは、以下のようなファイル構成になっている。
│ README.md
│ LICENSE
│ .gitignore
│ Procfile
│ system.properties
│ pom.xml
└─src
└─main
└─java
Main.java
ファイルの種類 | ファイル名 | 説明 |
---|---|---|
Heroku 関係 | Procfile | foreman の設定ファイル。 |
system.properties | Heroku で動かす時の環境設定などを記載したファイル。 | |
Java プログラム関係 | src/main/java/Main.java | ソースコード。組み込みの Jetty を起動させている。 |
pom.xml | 依存関係を定義した普通の pom.xml。 | |
GitHub 関係 | .gitignore | |
LICENSE | ||
README.md |
###Procfile
foreman という Ruby のプログラム用の設定ファイル。
Heroku 上で実行するコマンドをここに記載する。
web: java -cp target/classes:target/dependency/* Main
Procfile は <プロセスタイプ>: <実行するコマンド>
という書式で記述する。
コマンドは複数記述することができ、その場合 foreman は各コマンドを別プロセスで起動する。
サンプルアプリでは、 web
というコマンドが定義され、 Maven のビルド結果を使用して java のコマンドを実行している。
Heroku では、このように web
という名前で定義されたコマンドが存在するとアプリケーションサーバーが起動するようになっている。
###system.properties
java.runtime.version=1.7
内容を見れば何となく想像できるが、 Java の実行環境のバージョンを指定している。
Heroku は、デフォルトでは Java 1.6 (1.6.0_27)が使用される。
このサンプルでは、 Java 1.7 (1.7.0_55)を使用するように指定している。
ちなみに、 Java 8 (1.8.0_20)もサポートされている。
※マイナーバージョンは 2014/11/13 現在ドキュメントに記載されている もの。
#サンプルをローカルで動かす
##データベースを用意する
###PostgreSQL をインストールする。
とりあえずローカルに PostgreSQL をインストールする。
手順は割愛(インストーラ落として叩くだけ)。
###データベース・ユーザーの作成
test という名前でデータベースを作成し、 test という名前のユーザーで書き込みできるようにしておく。
pgAdmin で GUI で簡単に作れるので方法は割愛。
###環境変数 DATABASE_URL の設定
環境変数 DATABASE_URL
に以下のようにデータベースの情報を設定する。
> set DATABASE_URL=postgres://test:test@localhost:5432/test
何故この環境変数を設定するのかというと、サンプルアプリの実装を見ると分かる。
private Connection getConnection() throws URISyntaxException, SQLException {
URI dbUri = new URI(System.getenv("DATABASE_URL"));
String username = dbUri.getUserInfo().split(":")[0];
String password = dbUri.getUserInfo().split(":")[1];
String dbUrl = "jdbc:postgresql://" + dbUri.getHost() + dbUri.getPath();
return DriverManager.getConnection(dbUrl, username, password);
}
System.getenv("DATABASE_URL")
でデータベースの接続情報を取得してコネクションを生成している。
Heroku 上で動かすと、 Heroku 上の PostgreSQL の URL 情報をこの DATABASE_URL
環境変数から取得できる。
URL のフォーマットは postgres://<username>:<password>@<hostname>:<port>/<path>
なので、それに合わせてローカルの設定をしている。
##Procfile を修正する
あたりまえだけど、 Procfile は Heroku(Linux) 上で動かすことが前提の記述になっており、 Windows 上で動かそうとするとエラーになる。
- web: java -cp target/classes:target/dependency/* Main
+ web: java -cp target\classes;target\dependency\* Main
スラッシュをバックスラッシュに、コロンをセミコロンに変更する。
##ビルドする
> mvn install
##起動する
> foreman start web
##確認する
Web ブラウザを開いて http://localhost:5000/
にアクセスする。
#ログの出力を確認する
ローカルで以下のコマンドを打つことで、 Heroku 上の Web アプリが出力しているログを確認することができる。
> heroku logs
また、 --tail
オプションを追加することで監視も可能。
> heroku logs --tail
#SSH で接続する
Heroku 上のアプリケーションが動いているサーバーに SSH で接続できる。
> heroku run bash
...
~ $ pwd
pwd
/app
~ $ ls
ls
Procfile build build.gradle gradle gradlew gradlew.bat src
#Gradle を使用する
Heroku では、 Gradle も使えるらしい。
前述の Maven でのサンプルを Gradle に切り替えて Heroku で動かしてみる。
##フォルダ構成
|-.gitignore
|-build.gradle
|-gradle/
|-gradlew
|-gradlew.bat
|-Procfile
|-system.properties
`-src/main/java
`-Main.java
##変更点
###build.gradle
apply plugin: 'application'
sourceCompatibility = '1.7'
targetCompatibility = '1.7'
mainClassName = 'Main'
applicationName = 'app'
repositories {
mavenCentral()
}
dependencies {
compile 'org.eclipse.jetty:jetty-servlet:7.6.0.v20120127'
compile 'javax.servlet:servlet-api:2.5'
compile 'postgresql:postgresql:9.0-801.jdbc4'
}
task stage(dependsOn: ['clean', 'installApp'])
task wrapper(type: Wrapper) {
gradleVersion = '2.1'
}
- 依存関係は
pom.xml
にあったのをそのまま持ってきた。 - application プラグインを有効にして、ビルド後の起動ファイルの名前などを設定している。
- Gradle Wrapper を有効にしている。
-
stage
という名前のタスクを用意している。-
stage
というタスクは、 Heroku の git リポジトリに push したときに自動的に実行される特別なタスク。 - このタスクで、プロジェクトをビルドし、コマンドラインから起動ができる状態にしておく必要がある。
-
###Procfile
- web: java -cp target/classes:target/dependency/* Main
+ web: ./build/install/app/bin/app
-
installApp
タスクで生成される起動スクリプトを実行するように修正。
###.gitignore
- target
+ /build/
+ /.gradle/
- Maven ではなく、 Gradle 用に無視するファイルを修正。
##Heroku にデプロイする
以下コマンドを、プロジェクトのルートで実行する。
> git init
> heroku create
> heroku addons:add heroku-postgresql
> git push heroku master
フォルダを Git リポジトリ化した後、 Heroku に新規アプリケーションを作成、 PostgreSQL のアドオンを有効にし、 Heroku の Git にプッシュしている。
※heroku
という名前の remote は、 heroku create
した時に自動で登録されている。
「Gradle はまだベータ版です」みたいなメッセージが出るけど、一応ビルドは正常に完了する。
##動作確認
> heroku open
ブラウザが立ち上がり、 Maven の時と同じようにアプリが動作するのが確認できる。
#タイムゾーンを変更する
> heroku config:add TZ=Asia/Tokyo
#アドオンを使う
Heroku では、アドオンという形で追加機能を利用することができる。
アドオンには、条件によっては無料で使えるものもある。
ただし、無料であっても使用するにはクレジットカード番号の登録が必要になる。
##アドオンの追加方法
こちら にアドオンの一覧がある。
追加したいアドオンのページを開き、プランと対象のアプリケーションを選択して追加する。
追加されたアドオンは、ダッシュボードのアプリごとの画面で確認することができる。
##無料で使えるアドオンの例
###ロギング
アドオンなしの場合、 heroku logs
でしかログを見ることができない。
この場合、参照できるログが最新 1500 行分までなど制約が大きい。
アドオンを入れることで、それより以前のログを参照したり、アーカイブ化したログをダウンロードしたりできるようになる。
###PostgreSQL のバックアップ
PostgreSQL のバックアップを1日1回取得することができる。
####コマンド
- 以下のコマンドは、アプリケーションを作成した(
heroku create
した)ディレクトリで実行する。 - そうでないディレクトリで実行する場合は、
--app <アプリケーション名>
オプションを末尾につけて、対象のアプリを指定する必要がある。
#####バックアップを確認する
> heroku pgbackups
ID Backup Time Status Size Database
---- ------------------------- ------------------------------------ ----- ------------
a001 2014/11/21 07:56.55 +0000 Finished @ 2014/11/21 07:56.56 +0000 2.0KB DATABASE_URL
#####手動でバックアップを取得する
> heroku pgbackups:capture
※無料版の場合、手動バックアップは2個まで作成可能。
#####バックアップファイルをダウンロードする
> heroku pgbackups:url <ID>
"https://https://s3.amazonaws.com/hkpgbackups/app31504551@heroku.com/a001.dump?(後略)"
-
ID
には、ダウンロードしたいバックアップの ID を取得する。- バックアップの一覧表示をしたときに、一番左に表示される項目(
a001
など)。
- バックアップの一覧表示をしたときに、一番左に表示される項目(
- URL が出力されるので、ブラウザなどを使ってアクセスすると dump ファイルをダウンロードできる。
#####リストアする
> heroku pgbackups:restore DATABSE_URL <ID>
- 指定した ID のバックアップをリストアする。
- 試してないけど、 ID の代わりに URL を指定できるらしい。つまり、手動で取得したバックアップを何処かネットワーク上の見える場所に置いて、 URL を指定すれば任意のバックアップからリストアできる?
#####バックアップを削除する
> heroku pgbackups:destroy <ID>
- 指定した ID のバックアップを削除する。
###メール送信
SendGrid
月200通までなら無料で使用できるメール送信用アドオン。
####Java でメール送信を実装
|-build.gradle
|-system.properties
|-Procfile
|-gradlew
|-gradlew.bat
|-gradle/
`-src/main/java/sample/heroku/mail/
`-Main.java
apply plugin: 'application'
sourceCompatibility = '1.7'
targetCompatibility = '1.7'
mainClassName = 'sample.heroku.mail.Main'
applicationName = 'mail'
repositories {
mavenCentral()
}
dependencies {
compile 'javax.activation:activation:1.1'
compile 'javax.mail:mail:1.4'
compile 'org.eclipse.jetty:jetty-servlet:7.6.0.v20120127'
compile 'javax.servlet:servlet-api:2.5'
}
task stage(dependsOn: ['clean', 'installApp'])
task wrapper(type: Wrapper) {
gradleVersion = '2.1'
}
web: ./build/install/mail/bin/mail
java.runtime.version=1.7
package sample.heroku.mail;
import java.io.IOException;
import java.util.Properties;
import javax.mail.Authenticator;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.Multipart;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
public class Main extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final String SMTP_HOST_NAME = "smtp.sendgrid.net";
private static final String SMTP_AUTH_USER = System.getenv("SENDGRID_USERNAME");
private static final String SMTP_AUTH_PWD = System.getenv("SENDGRID_PASSWORD");
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (req.getRequestURI().endsWith("/mail")) {
try {
sendMail(req,resp);
} catch (Exception e) {
e.printStackTrace();
}
} else {
showHome(req,resp);
}
}
private void sendMail(HttpServletRequest req, HttpServletResponse resp) throws Exception {
Properties props = new Properties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.host", SMTP_HOST_NAME);
props.put("mail.smtp.port", 587);
props.put("mail.smtp.auth", "true");
Authenticator auth = new SMTPAuthenticator();
Session mailSession = Session.getDefaultInstance(props, auth);
Transport transport = mailSession.getTransport();
MimeMessage message = new MimeMessage(mailSession);
Multipart multipart = new MimeMultipart("alternative");
BodyPart part1 = new MimeBodyPart();
part1.setText("This is multipart mail and u read part1......");
BodyPart part2 = new MimeBodyPart();
part2.setContent("<b>This is multipart mail and u read part2......</b>", "text/html");
multipart.addBodyPart(part1);
multipart.addBodyPart(part2);
message.setContent(multipart);
message.setFrom(new InternetAddress("me@myhost.com"));
message.setSubject("This is the subject");
message.addRecipient(Message.RecipientType.TO, new InternetAddress("to@myhost.com"));
transport.connect();
transport.sendMessage(message, message.getRecipients(Message.RecipientType.TO));
transport.close();
}
private class SMTPAuthenticator extends javax.mail.Authenticator {
public PasswordAuthentication getPasswordAuthentication() {
String username = SMTP_AUTH_USER;
String password = SMTP_AUTH_PWD;
return new PasswordAuthentication(username, password);
}
}
private void showHome(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().print("Hello from Java!");
}
public static void main(String[] args) throws Exception{
Server server = new Server(Integer.valueOf(System.getenv("PORT")));
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
server.setHandler(context);
context.addServlet(new ServletHolder(new Main()),"/*");
server.start();
server.join();
}
}
メール送信の実装は こちら を丸コピ。
Heroku にデプロイして、 /mail
パスにアクセスすれば、メールが送信される。
#参考