UUUM攻殻機動隊

UUUMのエンジニアによる技術ブログです

Google Apps Script で Kintone API のライブラリ作った

おはこんばんちは!! 尾藤 a.k.a. BTO です。

みなさん、Google Apps Script 書いてますか〜。 Cybozu の Kintone 使ってますか〜。

Cybozu の Kintone ってちょっとした業務アプリを手軽に作るのに便利なんですよね。 弊社でも社内の情報を管理するのに Kintone 使ってます。

一方で、 Kintone ってプログラミングは必要ないんだけど、やっぱりシステムの知識がある程度ないと使いこなすのは難しいですよね。 なので、情シスで対応できないところは Google Spreadsheet でデータの管理をしていたりします。

そうすると欲しくなりますよね。 Kintone とデータ連携するためのツールが!! というわけで、Google Apps Script で Kintone API 用のライブラリを作りました〜!!(パチパチパチパチ

Google Apps Script Kintone Library

せっかくなので、必要な方に使ってもらおうと思って、公開しました〜。

gas-kintone

使い方

詳しい使い方は README.mdをご覧ください。

それだけだと寂しいので、簡単なサンプルスクリプト貼っておきます。

var subdomain = 'subdomain';
var appId = 1;
var apiToken = 'xxxx';
var kintone = new Kintone(subdomain, appId, apiToken);

// レコード作成
var id = kintone.recordCreate({foo: 'bar'});
alert(id);

// レコード取得
var record = kintone.recordGet(id);
alert(record.foo);

// レコード更新
var revision = kintone.recordUpdate(id, {foo: 'baz'});
alert(revision);

// レコード削除
var result = kintone.recordDelete(id);
alert(result);

開発はローカルでできるようにする

この Kintone ライブラリを開発するにあたっていくつか工夫した点を書いておきます。

まず Google Apps Script は ES5(Javascript) です。 なので、Nodejs との親和性が高く、開発環境の構築は Nodejs のツールでやるのが良いです。

Google Apps Script はデフォルトで Google が Script Editor を用意してくれています。 これ自体は悪くはないですが、やはりローカルで自分の好きな環境で開発したいですよね。 そこで、ローカルで開発して、Google Apps Script にディプロイして動作確認をするやり方をとりました。

ローカルからディプロイ

ローカルで開発するにあたって、最も重要なのは、Google Apps Script のプロジェクトにどうやってディプロイするかです。 Google Apps Script は Google Drive 上に存在しており、Google Drive API 経由で書き換えが可能です。 この仕組みを使って Google Apps Script をディプロイするツールがいくつか公開されています。 gas-kintone では、Google の公式ツールである node-google-apps-scriptを使いました。

node-google-apps-script の使い方は割愛しますが、Google Drive API 経由でアクセスするため、Google Drip API の権限を持った認証キーを使えば、ローカルで編集したファイルをコマンドラインからディプロイできるようになります。

ユニットテスト

ユニットテストは GasTというライブラリを使いました。 Google Apps Script のユニットテストのライブラリはいくつかあるのですが、継続的にメンテナンスされているものがなく、高機能なものよりもシンプルで安定的に使えそうなものを選びました。

こんな感じのユニットテストを実行するための関数を定義しておきます。

function testRunner() {
  var test = new GasTap();

  var functions = testRunner.functions;
  for (var i = 0; i < functions.length; i++) {
    functions[i](test);
  }

  test.finish();
}

testRunner.functions = [];

そして他のファイルから、この testRunner.functions に追加すれば勝手に実行されるテストとして登録される仕組みです。

testRunner.functions.push(function (test) {
  // ここにテストを書く
});

ディプロイした後に、Google Apps Script プロジェクトにアクセスして testRunner を実行すれば、登録したテストが実行されます。 テストの実行結果はログで確認できます。

f:id:masatobito:20170623172125p:plain

最後の行で、82のテストを実行して、失敗が0だったのがわかります。

ユニットテストの実行のところは、コマンドラインで完結してなくてブラウザにアクセスしないといけないのでちょっとかったるいです。 ですが、Google Apps Script は Google の環境でしか実行できないので、ある程度は仕方がないです。

ちなみに GasT には syslog にログを吐き出す機能も持っているので、syslog でログを受け取るサーバを立てれば、もう少し自動化できるんじゃないかと思います。

eslint でコードチェック

Google Apps Script は ES5 なので、Nodejs で使われてるコードチェックツールがそのまま使えます。 なので、 eslint でコードチェックをするようにしました。 特にこだわりはないので、 airbnb-es5 をノンカスタマイズで使っています。

{
  globals: {
    // Google Apps Script
    PropertiesService: true,
    UrlFetchApp: true,

    // Test Library
    GasTap: true,

    // Our classes
    Kintone: true,
    KintoneClient: true,
    KintoneError: true,
    KintoneResponse: true,

    // test function
    testRunner: true,
  },
  extends: 'airbnb-es5',
}

固有値は Script properties に突っ込む

Kintone API 用のライブラリなので、テストの実行には実際に Kintone の API を叩く必要があります。 Kintone API を叩くには、実際にそのアプリが存在していないといけないし、アクセスするための APIトークンが必要になってきます。 例えテストプログラムといえども、この固有値をプログラムの中には埋め込みたくないですよね。 そこで、固有値に関しては、Google Apps Script の Script properties に保存するようにしています。

Script properties の値は Google Apps Script からアクセスできるクラスが準備されているので簡単に取り出すことができます。

まとめ

UUUM の社員旅行(任意参加)では石垣島に行けます!!

f:id:masatobito:20170623173301j:plain

dockerあれこれ

Dockerを使う上で、少し解りずらいけど理解したらDockerと少し親しくなれるようなところをピックアップしてみようと思います。

Docker imageって何?

設定ファイルとファイルシステムのdiffから構成される

例えば下記のようなシンプルなDockerfileからビルドしたイメージを docker inspect [image] で中身を一部抜粋すると

FROM alpine
RUN apk --no-cache add  git
CMD ["/bin/echo", "hoge"]
[
    {
        "Id": "sha256:167ac9be494e05feadc5096832622cf9b5b70acaa62ac6e17f61de624e6eb2fb",
        "RepoTags": [
            "sample1:latest"
        ],
        "RepoDigests": [],
        "Parent": "sha256:efcaf1953f1fe27f5e9372fae521da68a506da7352ce8d17f3c94a09fdaa5ec4",
         ...
        "DockerVersion": "17.03.1-ce",
        "Author": "",
        "Config": {
            ...
            "Cmd": [
                "/bin/echo",
                "hoge"
            ],
            ...
        },
        ...
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:3fb66f713c9fa9debcdaa58bb9858bd04c17350d9614b7a250ec0ee527319e59",
                "sha256:bfd51f3d31f5ed500e02975f8207a2f1f604ecd85126da84614fbe93e8d7aed8"
            ]
        }
    }
]

こんな感じになります。

ちなみに alpine はとてもサイズが小さいLinux OSです。

alpinelinux.org

CMDで指定した内容が Config の Cmd にあって、ファイルシステムのdiff(RootFS Layers)に、alpineの分と、apk で gitをインストールした内容の2つ分入っていることが解ります。

イメージのレイヤーを確認する docker history [image] を実行すると

167ac9be494e        6 minutes ago       /bin/sh -c #(nop)  CMD ["/bin/echo" "hoge"]     0 B                 
efcaf1953f1f        23 hours ago        /bin/sh -c apk --no-cache add  git              21.5 MB             
a41a7446062d        11 days ago         /bin/sh -c #(nop)  CMD ["/bin/sh"]              0 B                 
<missing>           11 days ago         /bin/sh -c #(nop) ADD file:ce33aabbc5f370e...   3.97 MB

inspectの Parent のところが historyの1つ前のイメージ efcaf1953f1f と一致していますね。 これで何となくイメージの内容が解るようになりましたね。

Copy On Write

次はファイルシステムの diff はイメージ間で共有されるというところを見ていきます。 さっきのinspectの内容の

"Layers": [
   "sha256:3fb66f713c9fa9debcdaa58bb9858bd04c17350d9614b7a250ec0ee527319e59",
   "sha256:bfd51f3d31f5ed500e02975f8207a2f1f604ecd85126da84614fbe93e8d7aed8"
]

の部分です。

apk で git をインストールしていた箇所を、ADDでファイルをコンテナに追加する処理に書き換えて試してみます

FROM alpine
ADD hoge.txt huga.txt
CMD ["/bin/echo", "hoge"]

で build して inspect した結果

"Layers": [
  "sha256:3fb66f713c9fa9debcdaa58bb9858bd04c17350d9614b7a250ec0ee527319e59",
  "sha256:77b5680dd209091028af983ee403527c3ff6d45933599e6e6d7caecfca4c7bcd"
]

1つの3fb66f … が同じで、2つめの値は違うということが見ると解りますね。 ということで、1つめの diffのハッシュが alpine のもので、2つめのハッシュはそれぞれ、apk add git の内容と、 hoget.txt の内容ということが解ると思います。

これでファイルシステムの内容が共有されるということが解りましたね。

build時のキャッシュについて

親イメージから辿っていき、命令文が完全に一致するものがあれば、新しくイメージを作らずにそれを使う

build時に既存のイメージを流用できるようなケースは、新しいイメージを作らずに既存のイメージが流用されます。

例えばさっきの1つめのDockerfileを最初にビルドすると

Sending build context to Docker daemon 99.33 kB
Step 1/3 : FROM alpine
 ---> a41a7446062d
Step 2/3 : RUN apk --no-cache add  git
 ---> Running in 0b1a3015df35
fetch http://dl-cdn.alpinelinux.org/alpine/v3.6/main/x86_64/APKINDEX.tar.gz

...

OK: 25 MiB in 17 packages
 ---> efe320ae020a
Removing intermediate container 0b1a3015df35
Step 3/3 : CMD /bin/echo hoge
 ---> Running in f0829ddf04a5
 ---> 1313f4ef40dd
Removing intermediate container f0829ddf04a5
Successfully built 1313f4ef40dd

で、Dockerfileを変更せずに再度ビルドすると

Sending build context to Docker daemon 99.33 kB
Step 1/3 : FROM alpine
 ---> a41a7446062d
Step 2/3 : RUN apk --no-cache add  git
 ---> Using cache
 ---> efe320ae020a
Step 3/3 : CMD /bin/echo hoge
 ---> Using cache
 ---> 1313f4ef40dd
Successfully built 1313f4ef40dd

となり、Using cache と出ていて、イメージのハッシュ値も一致していることが解ります。 このようにハッシュが使用されるルールは下記です。

  1. 親イメージから子に向かって処理は進められ、対象レイヤーの親イメージが持つ子イメージの中で、コマンドの内容が完全に一致しているものがあればそれが使用されます。(関係ないスペースが入ってもキャッシュは利用されなくなります)

  2. ADD, COPY 命令の場合は、ファイルの中身のチェックサムが一致するとキャッシュが利用されます。

なので、ADD, COPY命令の場合は、Dockerfile上の命令は変わっていなくても、ファイルの内容が更新された場合はキャッシュは使用されずに新しくイメージを作ってくれます。

CMD, ENTRYPOINT について

CMDとENTRYPOINTって解りにくいと思うので説明したいと思います。

2つの形式

まず書式としては2つあります

  1. shell方式 echo hoge
  2. exec方式 ["/bin/echo", "hoge"]

これらは同じ、hoge と出力するものです。 これらの方式とCMD, ENTYPOINTの挙動の違いが少し複雑です

CMDについて

イメージ対してゆるいデフォルトコマンドを指定する

  • run時に実行される
  • 上書き可能
ENTRYPOINTについて

イメージ起動時のコマンドを指定する

  • 同じようにRUN時に実行される
  • CMDと違って上書きはできない

ちょっとわかりづらくて説明もよく解らないので、実際の動きを見てみると

FROM alpine
RUN apk --no-cache add  git
CMD ["/bin/echo", "bbb"]
ENTRYPOINT ["/bin/echo", "aaa"]

を build して run すると、

aaa /bin/echo bbb

となります。 この挙動を見ると、ENTRYPOINTがあると、そっちのほうが優先され、CMDのexec形式で書いたものは引数として渡されているのが解ると思います

run時に ccc を引数として渡すと

aaa ccc

となり、CMDで指定したものが上書きされます。

次にENTYPOINTの書式をshell形式にして試してみます。

FROM alpine
RUN apk --no-cache add  git
CMD ["/bin/echo", "bbb"]
ENTRYPOINT echo aaa

これで run すると、引数有り無しにかかわらず、aaa が表示されるだけになり、CMDの内容も run の引数も影響がなくなってしまいます。 若干解りづらいですが、このあたりの挙動を理解しておくと、Dockerfileを書くときのストレスが減少できると思います。

UUUMのシステム開発ではDockerの恩恵をうけた快適な開発スタイルを実践しています^^

参考にさせて頂いたサイト

危険な危険なSQLインジェクション

エンジニアのspin13です。

今回はSQLインジェクションについてです。

SQLインジェクションとは

入力パラメータの検証不備等の原因で、予期せぬSQL文が発行されてしまう 予想していないパラメータが注入(inject)される

SQLインジェクションで何が起こるのか

  • データの窃盗
  • データの改変 SQL文で書けるようなことはなんでも起こり得る

<?php
if(!empty($_POST)){
    $db =  new PDO('mysql:dbname=test_db;host=localhost', 'user', 'password');
    $user = $_POST["user"];
    $pass = $_POST["pass"];
    echo "発行クエリ<br>";
    $query = "SELECT * FROM users WHERE name='{$user}' AND pass='{$pass}'";
    echo $query;
    echo "<br>";
    $result = $db -> query($query) -> fetchAll();
    echo "<br>";
    if($result){
        echo "成功";
    }else{
        echo "失敗";
    }
}
?>

上記はPOSTでユーザとパスワードを受け取ってDBに該当ユーザのレコードが存在するかどうかを確認するSQL文を発行します。

ユーザに user1
パスワードに password
と入力してsubmitしてみます。 出来上がるSQL文は、
SELECT * FROM users WHERE name='user1' AND pass='password' です。

[test_db]> select * from users;
+------+-------+-----------+
| id   | name  | pass      |
+------+-------+-----------+
|    1 | user1 | password  |
|    2 | user2 | changed   |
|    3 | user3 | password3 |
+------+-------+-----------+

このDBに対してSQLを実行します。

[test_db]> SELECT * FROM users WHERE name='user1' AND pass='password';
+------+-------+----------+
| id   | name  | pass     |
+------+-------+----------+
|    1 | user1 | password |
+------+-------+----------+
1 row in set (0.01 sec)

データが存在するのでログインに成功します。

[test_db]> SELECT * FROM users WHERE name='hoge' AND pass='fuga';
Empty set (0.00 sec)

存在しないユーザとパスワードの組み合わせではもちろんレコードが存在しないので失敗します。

次にSQLインジェクションで強制的にログインしてみます。

ユーザに user1 ' AND 1=1 -- '
パスワードに hoge
と入力してsubmitしてみます。 出来上がるSQL文は
SELECT * FROM users WHERE name='user1 ' AND 1=1 -- '' AND pass='hoge'

さてDBで叩いてみましょう。

[test_db]> SELECT * FROM users WHERE name='user1 ' AND 1=1 -- '' AND pass='hoge';
    -> ;
+------+-------+----------+
| id   | name  | pass     |
+------+-------+----------+
|    1 | user1 | password |
+------+-------+----------+

user1のレコードが返ってきてしまうんです!

なぜこのようなことが起きてしまうのでしょう。

解説

受け取ったパラメータをPHPのプログラム側で $query = "SELECT * FROM users WHERE name='{$user}' AND pass='{$pass}'";

として展開しています。

userに user1 ' AND 1=1 -- ' と入れると、
name = ' user1 ' AND 1=1 -- '
SQLでは -- 以降はコメントとして扱われるので、 AND pass='{$pass}'" が無視されてしまうのでパスワードに何が入っていても関係なくなってしまうんですね。

対策

プレースホルダを使いましょう

$state = $db-> prepare("SELECT * FROM users WHERE name=? AND pass=?");
$state-> execute(array($user, $pass));

このようにすればSQLの本体部分とパラメータ部で切り離されて、プレースホルダで与える部分は値としてしか解釈されないのでSQLインジェクションで不正なパラメータを注入するのが困難になります。

不正な値を受け取って実行できる状態でなくするのが大切です。 最近ではライブラリ側が自動でエスケープ処理等をしてくれるので意識することは少なくなっていますが、原理を知っておくときっと何か役に立ちます。

おまけ

user: aaaaaa' ;UPDATE users SET pass=‘’ – ‘ pass: aaaa

SELECT * FROM users WHERE name='aaaaaa' ;UPDATE users SET pass='' -- '' AND pass='aaa'
[test_db]> select * from users;
+------+-------+------+
| id   | name  | pass |
+------+-------+------+
|    1 | user1 |      |
|    2 | user2 |      |
|    3 | user3 |      |
+------+-------+------+
3 rows in set (0.00 sec)

▂▅▇█▓▒░(’ω’)░▒▓█▇▅▂

Datadog の導入がめっちゃ楽だった話

こんにちは、新人エンジニアのハトネコエです。
気付いたらプログラミングを始めて2年が経ちました。

みなさまはゴールデンウィークをいかがお過ごしでしたでしょうか?
私は人生初めてVPSを契約しました。

Ansible でサーバー構築のレシピを四苦八苦しながら作り、アプリケーションを動かせるまで至ると、
今度はサーバーの監視をしたくなってきます。

(↓なお、Ansibleについては以前、弊社のナカハシがこちらの記事で取り上げました)

サーバー監視のクラウドサービスと言えば Mackerel がおなじみです。
見やすいグラフが印象的で、弊社でも一部利用させていただいております。

Mackerel が便利なのは知っているので、
今回は異なるクラウドサービスを使おうと考えました。 Datadog です。

f:id:nekonenene:20170508180215p:plain

続きを読む

Railsアプリ開発中に思い出す、StrategyとRackミドルウェア

こんにちは、エンジニアのナカハシです。

最近は、しばらくRailsでのWebアプリ開発に勤しんでいる毎日です。

開発中にStrategyとRackミドルウェアを復習したので、軽くまとめてみました。

続きを読む

Rails5で手早くCMS開発をするのに便利なgem7選

エンジニアのタナカです。

4月から新しくサイト開発に携わる方も多いのではないでしょうか?
サイト開発では殆どの場合、CMS機能を用意する必要がありますが、このCMS機能をどう実現するのかというのが最初に検討することだと思います。

CMSといえばWordPressが有名ですが、WordPressも一長一短なので、 様々な理由によりCMSのスクラッチ開発を行う場合も多いと思います。
とはいえスクラッチ開発となると開発コスト等が問題になる場合もあります。 けれども、Railsを以ってすればWordPressと同等以上のCMSを効率よく開発する事も可能です。

ということで、Rails5でのCMS開発に役立つgemを紹介したいと思いますが、 その前にまずはCMSに必要な機能を定義してみます。
CMSと言っても様々なタイプがありますが、今回はニュース・メディア系のCMSを想定しています。

ニュース・メディア系CMSでよくある機能

  • ニュース(ブログ)記事投稿・編集
  • 固定ページ投稿・編集
  • Slug(カスタムURL)の設定
  • 画像投稿
  • タグ付け
  • 予約投稿
  • 履歴管理
  • ユーザー管理

これらの機能を意識しながら、使えそうなgemを紹介いきたいと思います。
これから紹介するgemはいずれも定番のものなので、ぜひ抑えておきたいところです。

ActsAsList

github.com

記事等の並び順を管理することができ、順番の入れ替えが簡単に行えるようになります。
元々Railsに組み込まれていた機能がgemとして切りだされたものですが、Rails5でももちろん使用可能です。

FriendlyId

github.com

たとえばIDが含まれるような以下のようなURLをフレンドリーなURLにしてくれるものです。

http://example.com/states/4323454

このような人間に理解しやすいURLに置き換えることができます。

http://example.com/states/washington

WordPressではslugという機能で提供されています。

Paperclip

github.com

フォームから画像ファイルをアップロードを行う際に便利なgemで、リサイズやサムネイル作成も行ってくれます。
アップロードされた画像をS3に書き出すことも可能なので、画像まわりの処理をこのgemに任せることができます。

ActsAsTaggableOn

github.com

記事などにタグ付けを行えるgemです。 タグの追加削除が簡単に行え、タグでの検索も可能です。

予約投稿

Rails5で利用できるgemは無さそうですが、記事のモデルにpublished_atカラムを追加し、公開日時をセットする方法がよくとられています。
同モデルに以下のようなscopeを定義することで公開済みのものを簡単に取得できるようになります。

scope :published, -> { where("published_at IS NOT NULL").where("published_at <= ?", Time.now) }
scope :draft, -> { where("published_at IS NULL OR published_at > ?", Time.now) }

PaperTrail

github.com

記事の履歴管理に利用できます。
記事を編集した際に、前の状態に戻すといった操作が行えるようになります。

devise

github.com

ユーザー登録やログイン認証といった機能を簡単に組み込むことができます。
アクセス権限を設定することが可能なので、adminユーザーの管理だけでなく、一般ユーザー向けにSNS的な機能を提供したい場合にも利用できます。

Storytime

github.com

これまでに上げたgemの機能を全て実現できるCMS機能が丸ごと入りのgemです。
これらの機能に加え、マルチサイトやナビゲーションの管理など、WordPressとほぼ同等の機能があり、管理画面も付いてきます。

CMSのgemはほかにもいくかリリースされていますが、Rails5対応で多機能なものはStorytime以外に無いと思います。
(固定ページの投稿、編集だけが必要であれば、ComfortableMexicanSofaもお勧めです。)

ただし残念なことに、最近はメンテナンスがあまりされていません。そのため、不具合があった場合は自分で対応することになる可能性が高いです。 (私は自分でメンテすることを前提に利用していますが、Storytimeのソースが読みやすいので問題が発生してもパッチの作成等で対応しています。)

まとめ

gemをうまく組み合わせることでCMSを効率よく開発することができます。(CMSに限った話ではないですが)
また、条件があえば、CMS全部入りのgemを利用することで、開発工数を大幅に削減することも可能です。
WordPressを独自プラグイン等で無理に拡張する前に、Railsでの開発を検討してみてはいかがでしょうか?

PHPなチームがRuby on Railsでの開発を行って得られたもの

3日連続 nazoです。

昨年から行っているチャレンジとして、Ruby on RailsによるWebサービスの開発があり、それが一段落しましたので、経緯と結果について報告させて頂きたいと思います。

続きを読む