どうも、初めて書きますnju33(じゅん)という者です。
よろしくお願いします。
書く順番が回ってきて何を書こうか迷ったのですが、今回はPostCSSについて書こうと思います。
Scalaのforとは
最近リファクタリングとか微妙な不具合修正やってるほさかです。 Scala の for は Java のものとはだいぶ様子が違います。そこで仕様を読みながらもう一度forについて学びます。
ここではscala-lang.orgを 読みながら自分なりの説明を書いてますので、気になる方はscala-lang.orgを読みましょう。
for とは
2種類のforがあります。 for内包表記 と forループ です。
for ( enum ) e
forループは 列挙enum の各要素に対して 式e を実行します。
REPLの実行例
scala> for (x <- List(1, 2, 3)) print(x) 123
for ( enum ) yield e
for内包表記は 列挙enum の各要素に対して 式e を実行し、その結果を集めて返します。
REPLの実行例
scala> for (x <- List(1, 2, 3)) yield -x res0: List[Int] = List(-1, -2, -3)
列挙はまず ジェネレーター から始まります。その後には、さらに ジェネレーター、 値の定義 、ガード を書けます。
ジェネレーター p <- e
は式eのうちパターンpにマッチするものをバインドします。
マッチしない物は無視されます。
値の定義 はval
を省略する以外はいつもどおりです。
ガード if e
は列挙のバインドを e が真のものに限定します。
これらはそれぞれ map flatMap withFilter foreach で実装されています。 これらのメソッドを実装したものであればジェネレーターにできます。
変換ルール
for内包表記 for (p <- e) yield e′
は e.map { case p => e′ }
に変換されます。
forループ for (p <- e) e′
は e.foreach { case p => e′ }
に変換されます。
for内包表記 for (p <- e; p′ <- e′;…) yield e″
(…
は空かジェネレーターか値の定義かガード)
は e.flatMap { case p => for (p′ <- e′;…) yield e″ }
に変換されます。
forループ for (p <- e; p′ <- e′;…) e″
(…
は空かジェネレーターか値の定義かガード)
は e.foreach { case p => for (p′ <- e′;…) e″ }
に変換されます。
ガード if g
をともなう ジェネレーター p <- e
は 一つのジェネレーター p <- e.withFilter((x1,…,xn) => g)
(x1,…,xn
は p の変数) に変換されます。
値の定義 p′ = e′
をともなう ジェネレーター p <- e
は 値のペアを持つジェネレーターをともなって変換されます。
(p, p′) <- for (x@p <- e) yield { val x′@p′ = e′; (x, x′) }
おわり
forやScalaに限らず疑問があれば仕様を読めば良いと思います。
Scalaのspecializedアノテーションを使いこなすための基礎知識
こんにちは、アドテクエンジニアーのトデス子です。ふだんスカラを使っているのでスカラの話をします。
ScalaはJavaと同様、型パラメータを使用したコードは内部的にObject型を通して使用されます。
そのため、Int
やDouble
といったプリミティブ型を指定した場合は
boxing/unboxingのオーバーヘッドが発生します。
このオーバヘッドは多くの場合大した問題になりませんが、数値計算などの特定領域においては パフォーマンスのボトルネックになるケースがあります。
Scalaにおいては、@specialized
アノテーションを使用することでこのオーバヘッドを軽減する機構があります。
この記事では、この機構の詳細と使用時の注意点などについて紹介します。
Monocleのコミッターになりました
ご無沙汰しています。最近カレーがマイブームのプロダクトグループ所属エンジニアのあおいの(@AoiroAoino)です。 私事ですが、前回書いた記事にも登場したMonocleというライブラリのコミッターになりました。 で、早速なんか記事書いてと言われました()ので、今回はとりあえず代表的な(?) Lensについて、適当に書こうかなと思います。
Lens とは?
例えば、こんな感じのデータ構造と、その適当なインスタンスがあったとします。
// 適当なデータ共 case class Job(id: Int) case class Player(name: String, job: Job) case class Game(player: Player, stage: Int) val game1 = Game(Player("Aoino", Job(3)), 1)
このネストしたgame1
の一番深いところ、AoinoさんのJobのid
を書き換えようとした時にみなさんどうしますか?
copy
メソッドを使って
// copyメソッドで頑張る game1.copy( player = game1.player.copy( job = game1.player.job.copy( id = 20 ) ) )
とするか、case class Game(...)
の中に
// それっぽいメソッドを定義する def setJob(newId: Int): Game = this match { case Game(Player(name, Job(id)), stage) => Game(Player(name, Job(newId)), stage) }
ってメソッドを定義する感じですかね?
前者はcopy
メソッドのネストで辛いし、後者はcase class Game
内にPlayer
やJob
が持つフィールドごとにsetXXX
って一々定義するの、うーん、微妙ですね。
いっそmutableにでもしてしまった方が、直感的でわかりやすくかけたりしますよね。
// もしvarだったら... game1.player1.job.id = 5
とはいえ、もちろんimmutableのが良いし、でもって欲張ってmutableのような表現が欲しくなっちゃいますね。そこでLensの登場です。
とてもざっくり誤解を恐れずに言うと、Lensとはgetter/setterの性質を持った関数(みたいなもの)であり、取得したり変更したいフィールドに対する参照のようなものです。 故に、Functional Reference とかって名称で呼ばれていたりもするみたいです。
さて、ではひとまずJob
に対してLensを定義してみましょう。
※MonocleでLensを定義(生成)する方法は複数ありますが、次節で詳しく説明します。
// Job.idに対するLens val idLens: Lens[Job, Int] = Lens[Job, Int](_.id)(i => job => job.copy(id = i))
このidLens
がお待ちかねの「Jobのidに対するLens」です。
なので、この参照に対するget
メソッドに、Job
のインスタンスを渡すとid
の値が得られますし、set
やmodify
で値を書き換えることができます。
// Job.idのLensを使ってみる val job1 = Job(1) val job2 = Job(2) // get idLens.get(job1) //=> 1 idLens.get(job2) //=> 2 // set idLens.set(100)(job1) //=> Job(100) // modify idLens.modify(_ + 10)(job2) //=> Job(12)
さてさて、Job
に関しては良さそうですが、元々の話ではネストしているgame1
のJob
のid
を書き換えたいっていう話でしたね?ひとまずはGame
からJob
に至るまでのLensを定義しておきましょう。
// GameからJobに至るまでのLens共を定義 // Player.job val jobLens: Lens[Player, Job] = Lens[Player, Job](_.job)(j => player => player.copy(job = j)) // Game.player val playerLens: Lens[Game, Player] = Lens[Game, Player](_.player)(p => game => game.copy(player = p))
単純にget
するだけであれば、playerLens.get
してjobLens.get
してidLens.get
すればいいですね。
// ネストしたデータに対するget idLens.get( jobLens.get( playerLens.get(game1) )) //=> 3
で、setは.....こんな感じ.....かな.....
playerLens.set(jobLens.set(idLens.set(100)(jobLens.get(playerLens.get(game1))))(playerLens.get(game1)))(game1) //=> Game(Player(Aoino,Job(100)),1)
んー、しかしながら、このままだとcopy
やヘルパーメソッドを定義した時と比べて大変辛い.....
あれ?でも特にget
の形、見覚えありませんか?はいそうです、関数合成の話でよく見かけるパターンですね。そう、Lensも同様に合成することができるんです。
※そもそも元になったHaskellの場合、Lens は(実装方法にもよりますが?)関数として表現され、合成関数として表されます。 しかし、残念ながらMonocleの場合、Lensはデータ構造として表現されているので、厳密には「関数の合成」ではないですけれど。
MonocleでLensを合成する場合はcomposeLens
メソッドを使用します。関数を合成する時と同様、順番には注意です。
// composeLens版 // get (playerLens composeLens jobLens composeLens idLens) get game1 //=> 3 // set (playerLens composeLens jobLens composeLens idLens).set(20)(game1) //=> Game(Player(Aoino,Job(20)),1) // modify (playerLens composeLens jobLens composeLens idLens).modify(_ + 7)(game1) //=> Game(Player(Aoino,Job(10)),1)
またはcomposeLens
のエイリアスメソッドである^|->
を使用するともう少し短くかけます。
// ^|-> 版 // get (playerLens ^|-> jobLens ^|-> idLens) get game1 //=> 3 // set (playerLens ^|-> jobLens ^|-> idLens).set(20)(game1) //=> Game(Player(Aoino,Job(20)),1) // modify (playerLens ^|-> jobLens ^|-> idLens).modify(_ + 7)(game1) //=> Game(Player(Aoino,Job(10)),1)
さてさて、ご覧の通りスッキリ書けましたね? Lensの合成は冒頭で述べた通り「取得したり変更したいフィールドに対する参照のようなもの」であり、その参照してるフィールドに対して、値を取得したり、書き換えたりができるというのがお分かり頂けたかと思います。
このように、Lens はネストしたデータに対するimmutableで簡潔な操作を提供してくれる素敵な概念です。
Monocleを試してみよう
さて、今度はMonocleでLensを定義する方法に焦点を当ててみようと思います。
前節ではgetter/setterを渡して自前で頑張っていましたが、Lensを定義する方法はv1.2.0-M1
現在、三種類あります。
自前で頑張る
前節で説明のためにgetter/setterを渡して自前で作った方法です。
// 自前で頑張る import monocle.Lens val idLens: Lens[Job, Int] = Lens[Job, Int](_.id)(i => job => job.copy(id = i))
カリー化されていますが、第一引数がgetter、第二引数がsetterです。一応setterの方は引数の順番に注意が必要です。 定義に忠実なので理解しやすいですが、いざ自分で定義しようとするといささか面倒な方法です。
GenLens
マクロを使う
恐らく、MonocleでLensを定義しようとした際に一番使われている生成方法です。 内部的には上記自前で頑張るコードをマクロで生成してるだけです。
// 基本的なGenLensの使い方 import monocle.Lens import monocle.macros.GenLens val idLens: Lens[Job, Int] = GenLens[Job, Int](_.id) val jobLens: Lens[Player, Job] = GenLens[Player, Job](_.job) val idLens: Lens[Game, Player] = GenLens[Game, Player](_.player)
また、以下のような使い方もできます。
// 分割して定義する import monocle.Lens import monocle.macros.GenLens val gen: GenLens[Game] = GenLens[Game] val playerLens: Lens[Game, Player] = gen(_.player) val stage: Lens[Game, Int] = gen(_.stage)
// ネストした case class に対するLensをいきなり作る import monocle.Lens import monocle.macros.GenLens val idLens: Lens[Game, Int] = GenLens[Game](_.player.job.id)
自前で定義するよりも楽で、次に紹介する@Lenses
よりも扱いやすいので、基本的にはGenLens
を使えばいいと思います。
@Lenses
マクロアノテーションを使う
case class
定義時に@Lenses
アノテーションをつけると、コンパニオンオブジェクト内に各プロパティに対するLensが定義されます。
macro paradise を使用した機能なので、(例えばsbtで開発を行う場合は)下記プラグインを追加する必要があります。
addCompilerPlugin("org.scalamacros" %% "paradise" % "2.1.0-M5" cross CrossVersion.full)
使い方は以下の通り。
// @Lensesマクロアノテーションの使い方 @Lenses case class Game(player: Player, stage: Int) // コンパニオンオブジェクト内に player, stage に対するLensが生成される Game.player.get(game1) //=> Player(Aoino,Job(3)) Game.stage.get(game1) //=> 1
大変強力な機能ですが、そもそも macro paradise が experimental な機能であり、また、case class
定義時にしか使用できない為、
以外と扱いにくいのが現状です。あくまで実験的な機能と捉えるのがいいでしょう。
おまけ
MonocleのREADMEに従って、build.sbtファイルを書けばすぐにでも使い始めることができますが、もっと簡単にMonocleを試せるようgiter8のテンプレートを作ってみました。
※giter8の導入方法に関しては、giter8のREADMEを参照してください。
使い方は簡単で、
# テンプレート生成
$ g8 aoiroaoino/monocle-template
をコマンドラインで実行し、名前やScala, Monocleのバージョンなどを指定してやるだけです。
何も指定しない場合、ScalaやMonocleのバージョンはデフォルトで最新を指定するようになっているので、とりあえずname
だけ指定してやればいいと思います。
まとめ
今回はLensについてのみざっくりと適当に書きましたが、ほかにもPrismやTraversalなどの概念があり、Optics(LensやらPrismやらの総称)は大変興味深く非常に面白い概念です。 これがHaskellだけでなく、Scalaでも使えるなんてとてもワクワクしますね!以上、あおいの(@AoiroAoino)がお送りしました。
(´-`).oO(あ、個人的に弊社では、MonocleやOpticsに興味のあるエンジニアさんを募集しています!
公式のDockerコンテナでRedis Clusterを構築する
はじめまして、@chaltosです。
今回はDockerコンテナでRedisクラスタを構築した際の手順について書きます。
「set
, get
などが問題なく実行できればよい」という状況での作業メモ的なものなので、レプリケーションやバックアップなどはまったく考慮してません。
環境
OS | Mac OS X 10.9.2 |
Docker | 1.8.2 |
DockerMachine | 0.4.1 |
やってみる
Redis Cluster Tutorialによると、配布されているソースコードにはredis-tribやcreate-clusterというユーティリティが同梱されていて、以下のようなコマンドでノードの追加やスロットの割当てができるらしいです。便利ですね。
$ ./redis-trib.rb create 127.0.0.1:7000 127.0.0.1:7001 ...
$ create-cluster start $ create-cluster create
ですが、公式のDockerコンテナではRedisのビルド後にソースコードを削除しているため、これらに頼ることはできません。
仕方ないので手動でやります。
設定ファイル
デフォルトの設定ではクラスタリングできないので、まずは設定ファイルを用意します。
cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000
イメージの作成・起動
--volume
オプションで設定ファイルのあるディレクトリをマウントします。
上記の設定ファイルでredis-server
を実行すると、/data/nodes.conf
が生成されます。
このファイルの所有者はredis:redis
です。
--volume $(pwd):/data
とすると/data
の所有者が1000:staff
になってしまってnodes.conf
を生成できないので、--volume $(pwd):/data/conf
としておきます。
$ docker create --name node_6379 -p 6379:6379 --volume $(pwd):/data/conf redis:3 redis-server /data/conf/redis.conf $ docker create --name node_6380 -p 6380:6379 --volume $(pwd):/data/conf redis:3 redis-server /data/conf/redis.conf $ docker create --name node_6381 -p 6381:6379 --volume $(pwd):/data/conf redis:3 redis-server /data/conf/redis.conf $ docker start node_6379 node_6380 node_6381
クラスタリング
スロットの割当
まず各ノードにスロットを割当てます。
$ docker exec node_6379 redis-cli cluster addslots {0..5500} OK $ docker exec node_6380 redis-cli cluster addslots {5501..11000} OK $ docker exec node_6381 redis-cli cluster addslots {11001..16383} OK
OKっぽいですね、確認しましょう。
$ docker exec node_6379 redis-cli cluster nodes cf2e950621381be04ea1031ae80cb945f9743b83 :6379 myself,master - 0 0 0 connected 0-5500 $ docker exec node_6380 redis-cli cluster nodes 0969adb771d38a49e70f235b39665156ded5a43a :6379 myself,master - 0 0 0 connected 5501-11000 $ docker exec node_6381 redis-cli cluster nodes d9c4125051222bbc36cd0d0172e406baa9d89579 :6379 myself,master - 0 0 0 connected 11001-16383
ちゃんと割当てできてるように見えます。
ノードの追加
クラスタにノードを追加していきます。
$ docker exec node_6380 redis-cli cluster meet `docker-machine ip $DOCKER_MACHINE_NAME` 6379 OK $ docker exec node_6381 redis-cli cluster meet `docker-machine ip $DOCKER_MACHINE_NAME` 6379 OK
OKっぽいですね。
$ docker exec node_6379 redis-cli cluster nodes cf2e950621381be04ea1031ae80cb945f9743b83 :6379 myself,master - 0 0 0 connected 0-5500 $ docker exec node_6380 redis-cli cluster nodes 0969adb771d38a49e70f235b39665156ded5a43a :6379 myself,master - 0 0 0 connected 5501-11000 $ docker exec node_6381 redis-cli cluster nodes d9c4125051222bbc36cd0d0172e406baa9d89579 :6379 myself,master - 0 0 0 connected 11001-16383
変わってません。
やりなおす
どうもNAT越しだとうまくいかない(?)ようです。
--net host
を追加して、ブリッジしないようにしてみます。
ついでに各ノードで異なるポートを使用するようにしておきます。
$ for n in `seq 6379 6381`; do mkdir $n && echo "port $n" > $n/redis.conf && cat redis.conf >> &n/redis.conf; done $ docker stop node_6379 node_6380 node_6381 $ docker rm node_6379 node_6380 node_6381 $ docker create --name node_6379 --volume $(pwd)/6379:/data/conf --net host redis:3 redis-server /data/conf/redis.conf $ docker create --name node_6380 --volume $(pwd)/6380:/data/conf --net host redis:3 redis-server /data/conf/redis.conf $ docker create --name node_6381 --volume $(pwd)/6381:/data/conf --net host redis:3 redis-server /data/conf/redis.conf $ docker start node_6379 node_6380 node_6381 $ docker exec node_6379 redis-cli -p 6379 cluster addslots {0..5500} $ docker exec node_6380 redis-cli -p 6380 cluster addslots {5501..11000} $ docker exec node_6381 redis-cli -p 6381 cluster addslots {11001..16383} $ docker exec node_6380 redis-cli -p 6380 cluster meet `docker-machine ip $DOCKER_MACHINE_NAME` 6379 $ docker exec node_6381 redis-cli -p 6381 cluster meet `docker-machine ip $DOCKER_MACHINE_NAME` 6379
$ docker exec node_6379 redis-cli -p 6379 cluster nodes ca0a4f11069103e902ea83de9023b34a7aa01d06 192.168.99.100:6379 myself,master - 0 0 0 connected 0-5500 465ab243a99efec4e7cfa00491304b8dcffd0701 192.168.99.100:6381 master - 0 1444015783288 2 connected 11001-16383 c185ffd045e2ce324dce43ae5374253df8da2af2 192.168.99.100:6380 master - 0 1444015784307 1 connected 5501-11000 $ docker exec node_6380 redis-cli -p 6380 cluster nodes 465ab243a99efec4e7cfa00491304b8dcffd0701 192.168.99.100:6381 master - 0 1444015806729 2 connected 11001-16383 ca0a4f11069103e902ea83de9023b34a7aa01d06 192.168.99.100:6379 master - 0 1444015805713 0 connected 0-5500 c185ffd045e2ce324dce43ae5374253df8da2af2 192.168.99.100:6380 myself,master - 0 0 1 connected 5501-11000 $ docker exec node_6381 redis-cli -p 6381 cluster nodes c185ffd045e2ce324dce43ae5374253df8da2af2 192.168.99.100:6380 master - 0 1444015823353 1 connected 5501-11000 465ab243a99efec4e7cfa00491304b8dcffd0701 192.168.99.100:6381 myself,master - 0 0 2 connected 11001-16383 ca0a4f11069103e902ea83de9023b34a7aa01d06 192.168.99.100:6379 master - 0 1444015824374 0 connected 0-5500
先ほどとは違い、どのノードも他のノードを認識できてるようです。
実際にset
とかget
してみる
-c
は必須です。忘れるとリダイレクトせずにエラーを返します。
$ docker exec -it node_6379 redis-cli -c -p 6379 127.0.0.1:6379> set hoge 1 OK 127.0.0.1:6379> set hoge2 2 -> Redirected to slot [11000] located at 192.168.99.100:6380 OK 192.168.99.100:6380> set hoge3 3 -> Redirected to slot [15065] located at 192.168.99.100:6381 OK 192.168.99.100:6381> get hoge -> Redirected to slot [1525] located at 192.168.99.100:6379 "1" 192.168.99.100:6379> get hoge2 -> Redirected to slot [11000] located at 192.168.99.100:6380 "2" 192.168.99.100:6380> get hoge3 -> Redirected to slot [15065] located at 192.168.99.100:6381 "3"
きちんと他のノードにリダイレクトしてくれてますね。
とある自社サービスのパッケージングの話
弊社ではDSPサービスSphereの他に Sphere Paper という紙広告をつくるWebサービスを運営しています。
Webの管理画面から画像をアップロードしたりテキストを入力すると紙広告の原稿ができあがるというちょっと面白いWebサービスです。
また配布時には独自のエリアターゲティング技術を活用しています。
このSphere Paperは開発コードネームFSSというScala製のいくつかのアプリケーションで構成されさくらのクラウド上で運用されています。
今回はこのプロダクトのシステム構成とパッケージング+deployについてご紹介します。
あと本番サービスでGentoo Linuxを使っている日本の会社の一員としてGentooの話を書きます!
サーバーについて
サーバーは基本となる構成を作ってさくらのクラウドの機能で複製します。
複製して起動するとすでに下記のような構成で動く状態になっています。
(サーバー構成についてはこちらの記事にも少し書いていますのであわせてご覧ください。)
- CPUアーキテクチャはamd64
- OSおよびディストリビューションはGentoo Linux
- LDAPによりSSH公開鍵とSUDO権限を制御
/home
はNFSでマウントしておりどのサーバーでも同じホームディレクトリが見える
仕事で扱うサーバーは物理/クラウド両方ですが、やっぱりクラウドサーバーは環境の複製が楽ですね!
さくらのクラウド快適です♪
アプリケーションとシステム構成について
FSSのシステム構成はこのようになっています。
またFSSを構成するアプリケーションは下記の3つです。
WebView
いわゆる管理画面のView側です。
Single Page Applicationとして作られており、広告の入稿や管理・紙広告原稿のプレビュー等が行えます。
弊社のフロントエンド開発環境としては標準的なSlim, Sass, CoffeeScript, AngularJSで実装されています。
deployに関連する点としてビルドにMiddlemanを使用するためRubyが必要です。
WebAPI
いわゆる管理画面のWebAPI側です。
Scalaで開発されており、WebViewからのリクエストを受けデータのCRUDを行い、結果をJSONや画像で返します。
Play FrameworkやSkinny-ORMを使用して実装されています。
パッケージング時にはsbtを使用します。実行には当然ながらJVMが必要です。
RenderingServer
入稿データから紙広告の原稿を生成します。
こちらもScalaで開発されており、WebAPIと連携してRGB→CMYK変換や組版処理(!)を行い結果を保存します。
WebAPIと同じくパッケージング時にsbtを使用します。
実行にはJVMはもちろんですがImageMagickも必要です。
使用ミドルウェア等
ミドルウェア/ツール/実行環境として下記を使います。
- nginx
- WebViewおよびWebAPIのフロントで使います。
- JDK
- WebAPI, RenderingServerともに1.8を使います。
- sbt
- WebAPI, RenderingServerともに0.13.xを使います。
- Ruby
- WebViewのビルドに使います。
- 以前は2.1以上を使っていました。
- あるタイミングから2.2以上を使うようにしました。
- ImageMagick
- RenderingServerで使います。
- インストール時のオプションでLittle CMSに対応するようにします。
- MariaDB
- 原稿データ以外の永続化に使います。
- MongoDB
- 原稿データの永続化に使います。
- レプリケーションおよびシャーディングしています。
パッケージングとdeployについて
各種ミドルウェアや実行環境の構築は大抵のLinuxディストリビューションであれば、パッケージ管理システムが整備されているおかげでそれほど苦になりません。
パッケージ管理システムの流儀に法って yum install
や apt-get install
や emerge
すればインストールできますし、 /etc
の中に設定ファイルが配置され大抵は少し編集すれば動作します。
各ディストリビューションのメンテナーの皆さまのおかげですね。
一方、自社アプリケーションのdeployはどうでしょうか?
deployを繰り返す中で「必要なJDK/JREのバージョンがインストールされてる?」「このPull Requestがマージされると必要なRubyのバージョンが2.1から2.2に上がるけど大丈夫?」「ImageMagickをインストールするときにLittle CMS対応忘れてた!」「今回から設定ファイル増えたんだけど」などなど...色々な問題が待ち受けています。
ディストリビューションで提供されるパッケージのように、作ったアプリケーションも
「インストール/アップデート時に依存関係を解決してくれればいいのに!」
そして 「デフォルト設定や雛形が自動的に配置されればいいのに!」
...そう思いませんか?
私はずっとそう思っていました。
そもそもディストリビューションで提供されるパッケージと自分たちが開発しているアプリケーションのインストール方法やアップデート方法が違うことに意義はあるのでしょうか?
そう、自分たちで開発しているアプリケーションも パッケージングしてしまえば良い ではないですか!
しかし1日にいくつものcommitが生み出される自社アプリケーションでCIが通ってmasterにブランチがマージされるたびにパッケージを作るには工夫が必要です。
FSSでは全サーバーがGentoo LinuxということもありPortageパッケージ管理システムを使ってアプリケーションパッケージを作成しています。
Portageのパッケージはebuildという形式で作成しますがその実態はインストール/アップデートの手順を書いたBashスクリプトです。Portageのコマンドであるemergeでパッケージをインストール/アップデートすると、ebuildに従ってソースコードが取得されコンパイルされ配置されます。
もし例えるならrpmやdebのようなバイナリパッケージが料理そのものだとしたら、ebuildはレシピ+材料の在り処といった感じでしょうか。すぐ食べることができるのはrpm, debですが、ebuildは調理の手間はかかるものの状況に合わせてアレルギーがある食材を抜いたり旬のものを追加したりできるというイメージです。
またebuildではGit(Hub)の特定のブランチや特定のcommitを参照しインストール/アップデートすることもできます。
パッケージの依存関係として"Little CMS対応のImageMagickがインストールされていること"という記述もできます。
理想的なパッケージングの仕組みに思えてきましたね!?
ebuildに興味を持っていただけたところで(?)弊社で作っているebuildをご紹介します。
もしPortageで自社アプリケーションをdeployされる際の参考になれば幸いです!!
なお抜粋ですので全行を載せているわけではありません。また各サーバーで自家製ebuildを取得するためにはパッケージリポジトリも必要です。
全行を載せたサンプルやパッケージリポジトリについては以前書いた記事およびこちらのパッケージリポジトリにありますのでぜひご参照ください。
前提
弊社で開発しているWebView, WebAPI, RenderingServer各アプリケーションのebuildをご紹介します。
3アプリケーションともにソースコードはすべてGitHub.comで管理していますので、ソースコードの取得元を定義するEGIT_REPO_URI
は弊社のGitHubリポジトリのGit URLを指定しています。
またebuildは前述のとおり拡張されたBashスクリプトでこちらのドキュメントに書かれているように決められたfunctionが決められた順番で呼び出されます。
WebViewのebuild
ではWebViewのebuildです。
DEPEND
でインストール時(ビルド時)の依存関係を記述します。RubyやBundler、Node.jsに依存していることを示し、さらにRubyは2.2以上が必要なことを、Node.jsはnpmが必要なことも記載しています。
このように細かい依存関係が指定できるのはebuildのメリットですね。
RDEPEND
(Runtime Dependencies)で実行時の依存関係を記述できますが、このWebViewアプリケーションは特に依存しているパッケージがありませんので記載していません。
「パッケージインストール時にコンパイルが必須なGentoo(Portage)でビルド時と実行時の依存関係分ける必要あるの?」と思われるかもしれませんが、じつはPortageにはバイナリパッケージを扱う仕組みがあります。今回は触れませんがバイナリパッケージを活用するとパッケージ構成の柔軟性を保ったままインストール/アップデートにかかる時間とコンピュータリソースを大幅に節約できます。
webapp-configというWeb(View)アプリケーションのインストーラを継承してそちらの機能に頼っていますので他のebuildよりシンプルになっています。
EAPI=5 inherit git-2 versionator webapp EGIT_REPO_URI="git@github.com:???/???.git" DEPEND=">=dev-lang/ruby-2.2 virtual/rubygems dev-ruby/bundler net-libs/nodejs[npm]" RDEPEND="" src_compile() { cd "${S}/sources/${APP_PROJECT}/default" || die bundle install --path=vendor/bundle || die } src_install() { webapp_src_preinst insinto "${MY_HTDOCSDIR#${EPREFIX}}/../default" doins -r "${S}/sources/${APP_PROJECT}/build/default" webapp_src_install }
WebAPIのebuild
つぎにWebAPIのebuildです。
DEPEND
でインストール時(ビルド時)の依存関係を記述しているのはWebViewと一緒ですが、Scalaアプリケーションなので依存がJDK 1.8以上とsbt 0.13以上な点が異なります。
またsbtパッケージはオフィシャルにも提供されているのですが、合わない部分があったので現在は自前のパッケージを使っています。
RDEPEND
はJDKではなくJREにしているのですが、実際にはOracle JDKをインストールしてしまっています。
今気づいたのですがWebAPIもImageMagickに依存していますね。ただLittle CMS対応は必須ではありません。
pkg_setup()
でアプリケーション用のシステムユーザーを作っています。もちろんAnsibleのような構成管理ツールでもユーザー作成はできるのですが、個人的にはアプリケーションで必要となるユーザーはアプリケーションパッケージ側で記述する方が見通しが良いと思っています。
src_compile()
ではsbt stage
を実行してScalaのソースコードをコンパイルしています。
src_install()
で必要なディレクトリを作成しsbt stage
で生成されたバイナリを配置しています。また起動スクリプトや設定ファイルの配置も行います。
EAPI=5 inherit user git-2 versionator EGIT_REPO_URI="git@github.com:???/???.git" DEPEND=">=dev-java/sbt-bin-0.13 >=virtual/jdk-1.8" RDEPEND=">=virtual/jre-1.8 media-gfx/imagemagick[jpeg,png]" APP_DIR="/var/lib/${PN}" pkg_setup() { enewgroup fss || die enewuser fss -1 -1 "${APP_DIR}" fss || die } src_compile() { rev=$(git rev-parse HEAD) || die cd "sources/${APP_PROJECT}" || die sbt clean stage || die } src_install() { keepdir "/var/log/${PN}" "/etc/${PN}" || die keepdir "${APP_DIR}/versions/${PN}.${PV}.${rev}" || die local stage="sources/${APP_PROJECT}/target/universal/stage" insinto "${APP_DIR}/versions/${PN}.${PV}.${rev}" doins -r "${stage}/bin" || die doins -r "${stage}/lib" || die newinitd "${FILESDIR}/${PN}.init2" "${PN}" || die newconfd "${FILESDIR}/${PN}.confd" "${PN}" || die cp "${FILESDIR}/application.conf.example" "${ED}/etc/${PN}/" || die "Cannot copy example application.conf" sed -e 's!<PLACEHOLDER_LOG_DIR>!/var/log/'${PN}'!' "${FILESDIR}/logger.xml" > "${ED}/etc/${PN}/logger.xml" || die if use symlink; then dosym "${APP_DIR}/versions/${PN}.${PV}.${rev}" "${APP_DIR}/versions/current" || ewarn 'Exist symlink!' fi fowners -R fss:fss "${APP_DIR}" "/var/log/${PN}" || die fperms 0755 "${APP_DIR}/versions/${PN}.${PV}.${rev}/bin/${DIST_NAME}" || die }
RenderingServerのebuild
最後にRenderingServerのebuildです。
DEPEND
はWebAPIと同じです。
RDEPEND
では組版のためにImageMagickとGhostscriptへの依存関係を追加しています。こちらのImageMagickはWebAPIと違いLittle CMS対応が必要です。
pkg_setup()
ではWebAPIと同じくアプリケーション用のシステムユーザーを作っています。
src_compile()
でsbtによるコンパイルを行うところはWebAPIと同じですが、こちらのアプリケーションはsbt assembly
を実行します。
src_install()
もWebAPIと同じく必要なディレクトリを作成しsbt assembly
で生成されたjarを配置しています。またこちらも起動スクリプトや設定ファイルの配置も行います。
EAPI=5 inherit user git-2 versionator EGIT_REPO_URI="git@github.com:???/???.git" DEPEND=">=dev-java/sbt-bin-0.13 >=virtual/jdk-1.8" RDEPEND=">=virtual/jre-1.8 media-gfx/imagemagick[jpeg,truetype,postscript,png,lcms] >=app-text/ghostscript-gpl-9.15" APP_DIR="/var/lib/${PN}" pkg_setup() { enewgroup fss || die enewuser fss -1 -1 "${APP_DIR}" fss || die } src_compile() { rev=$(git rev-parse HEAD) || die cd "sources/${APP_PROJECT}" && sbt clean assembly || die } src_install() { keepdir "/var/log/${PN}" "/etc/${PN}" || die keepdir "${APP_DIR}/versions/${PN}.${PV}.${rev}" || die local target="sources/${APP_PROJECT}/target/scala-2.11" insinto "${APP_DIR}/versions/${PN}.${PV}.${rev}" doins "${target}/${DIST_NAME}-assembly-${DIST_VERSION}.jar" || die newinitd "${FILESDIR}/${PN}.init2" "${PN}" || die newconfd "${FILESDIR}/${PN}.confd" "${PN}" || die if use symlink; then dosym "${APP_DIR}/versions/${PN}.${PV}.${rev}" "${APP_DIR}/versions/current" || ewarn 'Exist symlink!' fi fowners -R fss:fss "${APP_DIR}" "/var/log/${PN}" || die }
おわりに
以上、弊社サービスのパッケージングとdeployのご紹介でした!
ところで弊社では一緒にebuildを書いてくださるエンジニアさんを募集しています!!
ご興味ありましたらぜひTwitter, 勉強会等でお声がけください!!!