astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

schemaspy+jenkinsで最新のER図をブラウザで閲覧できるようにする

こんにちは、アプリチームのchotaroです。

いくつかの新規サービスの開発を控え、アプリチームにも人の入れ替わりが発生してきています。

そんな中、新規参画者への情報共有が一つの課題になりました。

今回はそのためにやった一つのTIPSを紹介します。

解決する課題

  • 逐次DBの構造が変更されるのに対して、プロジェクト内のER図がメンテできておらず最新のER図が存在しないこと
  • 新規参入者が全体の構造を理解するのに、時間がかかってしまうこと

解決方法

schemaspyとjenkinsを組み合わせて、チームメンバーが最新のER図をWEBブラウザ上で確認できるようにする。

実践

schemaspy

schemaspyは、オープンソースで提供されている、ER図生成ツールです。(ライセンスはLGPLv3)

接続先やスキーマ名の情報を与えることで対象のDB・スキーマを読み取り静的なhtmlを一式生成します。

スキーマごとにディレクトリを生成し、スキーマ内でのカラムの検索やリレーションを表現した図を出力してくれます。

f:id:astamuse:20190123113640p:plain
スキーマごとの画面イメージ

html一式が出力されるため、例えばCIサーバ上に展開するようにすれば、チームメンバー全員が閲覧可能になります。

ER図の出力

基本的にはドキュメントに全部書いてあります。親切。

下準備

  • graphvizのインストール(ubuntuであれば apt-get install graphviz してあげてください。dot -V でinstallされているか確認できます。 )
  • java8のインストール

必要なライブラリの準備

  • ライブラリ本体SchemaSpy releases
  • 接続先に合わせたjdbc driver(postgresqlであれば、wget https://jdbc.postgresql.org/download/postgresql-42.2.5.jar )

上記を一つのdirectoryに入れ、同じ階層にschemaspy,propertiesを作成します。

# type of database. Run with -dbhelp for details
schemaspy.t={{接続先のDBの種類 postgresqlなら[pgsql] }}
# optional path to alternative jdbc drivers.
schemaspy.dp={{ 接続用のJDBC driver [/path/to/driver/postgresql-42.2.5.jar]}}
# database properties: host, port number, name user, password
schemaspy.host={{ 接続先host }}
schemaspy.port={{ 接続先port番号 }}
schemaspy.db={{ 接続先DB名 }}
schemaspy.u={{ user name }}
schemaspy.p={{ password }}
# output dir to save generated files
schemaspy.o={{ output先のdirectory }}
# db scheme for which generate diagrams
# schemaspy.s= {{ 対象のスキーマ名 (今回は全スキーマを対象としたいのでコメントアウトしています。) }}

実行

ここまでで準備してきたdirectoryで

java -jar [jar名] -all

とコマンドを実行します。(-allは全スキーマを分析対象とするためのオプションです。)

ciサーバ上で出力が確認できたらnginxなどWEBサーバーの設定を仕込み、まずページを見られるか確認します。

こんなページが閲覧可能になればOKです

f:id:astamuse:20190123103929p:plain
schemaspy index.html

okならばjenkinsにjobを作っていきます。

Jenkins jobでER図の最新化を自動化する

シェル実行で以下のようなスクリプトを設定。

rm -rf /path/to/output/*
cd ~/schemaspy
# jarの実行。
java -jar schemaspy-6.0.0.jar -all
ls -al /path/to/output/

このとき、書き込み先のディレクトリのpermissionには注意してください。jenkinsユーザーの書き込み権限が出力先のdirectoryにあることが前提になります。権限がない場合、処理がfailしてしまいます。

また一部のみ出力ではなく全量を出力するため、ゴミが残らないよう毎回クリーンアップするようにしています。

このjob自体をwebアプリのデプロイjobのあとに起動するように設定して完了です。

こうすることで実際に動いているwebアプリが、どんな構造のDBを元にしているのかがすぐ確認できるようになりました。

効果

これから先に備えた変更なので、この先どうなるか、というとこです。今後に期待。

ただし、ローカルで利用してみた所感では、やはり無いのと有るのでは自分自身の実装に対する確信が違います。

ひと目でわかること、は大事。

課題

リレーションを明示的に貼っていないとリレーション図を閲覧できません。(当たり前ですが……

なのでリレーションを明示的に貼ってはいないものの同一なもの(USER.user_idとACCESS_LOG.user_idのような関係)を読み取ることが難しいです。

docker上にDBを立ててそこにddlを流し、別途alter文を流して擬似的に見せることはできますが……そうするとそのalter文までメンテナンスしなくてはならないため諸々コストがかかります。

悩ましいところですが、新規チームメンバーの参画のタイミングで継続的に改善していくしかないかなというところです。

以上、主にschemaspyの紹介でした。

アプリチームでは今後もこのような改善活動を継続しつつ、サービス開発に勤しんでいきます。

もし興味持たれた方おられましたら、下記バナーから開発環境や採用情報など見れますので、是非見てみてください。

それでは失礼します。

strapi+nuxt.jsでHeadless CMSの夜明けを感じる

デザイン部でフロントエンドエンジニアをしているkitoです。
近年のCMS界隈には、いわゆるHeadless化の波がきています。CMSのHeadless化とは、CMS(Content Management System)からクライアントサイドを切り離し、クライアントサイドのフレームワークでUIを構築するアーキテクチャです。

デカップルド・アーキテクチャと呼ばれることがあるようですが、サーバサイドとクライアントサイドがAPIを通じてコンテンツをやりとりすることで、Webアプリだけではなくてネイティブアプリからの要求にも適切に答えられるようになります。スケーリングに関しても、Headless CMSの方が取り回しが良いのではないでしょうか。

WordpressがWP REST APIを開発し、Headless化へと道筋をつけたことは特筆すべきです。WP REST APIの公式サイトでは、冒頭に「WordPress はアプリケーションフレームワークへと生まれ変わろうとしています。」と気負って書かれています。
とはいえ、Headless CMSの主役が、WordPressになるかどうかは未知数です。WordPressのエコシステムは巨大で、蓄積されたリソースは非常に豊かであると思いますが、クライアントサイドのフレームワークとの相性は、おそらく現時点ではReact.jsやVue.jsの周辺から派生してきたHeadless CMSの方が優っているのではないかと思います。

今回は、まだalpha版ですが使ってみて好感触だったstrapiというHeadless CMSと、クライアントサイドはVue.jsのフレームワークでSSR(サーバーサイドレンダリング)に対応しているNuxt.jsをあわせて使ってみたいと思います。

strapi

まずHeadless CMSのstrapiをインストールします。(Node.js 10.x以上、NPM 6.x以上が必要になります)

npm install strapi@alpha -g

インストールできているか確認します。

strapi -v

インストールされていれば、「3.0.0-alpha.x」のようなバージョン番号が表示されます。

データベースを任意に作成してください。MongoDBやPostgreSQLも使えますが、今回はMySQLを使います。

mysql -u root -p
mysql> create database strapi_db;

下記コマンドでアプリを作成していきます。 my-project以下にstrapiとnuxt.jsをそれぞれインストールします。

mkdir my-project
cd my-project
strapi new strapi

使用するデータベースを聞かれるので、MySQLを選択します。

Lets configurate the connection to your database:
  Choose your main database:
  MongoDB
  Postgres
❯ MySQL

データベースの名前を入力してください。

Database name: strapi_db 

続いて、Hostやport、UsernameとPasswordを聞かれるのでそれぞれ入力してください。
これでmy-project以下にstrapiがインストールされました。下記でstrapiを起動してみましょう。

cd strapi
strapi start

ブラウザが自動で起動するのでユーザ名、パスワード、メールアドレスを入力し「Ready to start」をクリックしてください。
f:id:astamuse:20190116110820p:plain 下記のようなadmin画面にログインできます。これでstrapiのインストールは完了です。続いてデータを入力します。 f:id:astamuse:20190116110812p:plain

コンテンツタイプ作成から「コンテンツタイプを追加」をクリックします。今回は名前をpostにします。次に「新しいフィールドを追加」を選択し、Stringをクリックして名前を「title」と入力します。続いて、textを選択して「content」、メディアを選択して「cover」と入力します。(「保存」のクリックを忘れないようにしましょう) f:id:astamuse:20190116110800p:plain f:id:astamuse:20190116110753p:plain

次にロールと権限から、Public > Postの権限を変更します。今回はスクリーンショットのようにすべてOKにしてあります。 f:id:astamuse:20190116110750p:plain サイドカラムにあるCONTENT TYPESの箇所にPostがあると思います。そこから、実際のデータを適当に入力してください。
http://localhost:1337/postsにアクセスすると、下記のような先ほど入力したデータがjson形式で表示されるでしょう。これでstrapiの設定は完了です。次にNuxt.jsのインストールです。

[
  {
  "id": 1,
  "title": "タイトル1",
  "content": "テキスト1テキスト1",
  "created_at": "2019-01-15T05:14:40.000Z",
  "updated_at": "2019-01-15T05:14:40.000Z",
  "cover": {
  "id": 1,
  "name": "150x150.png",
  "hash": "cffd30f367994f9788ca31c4ab0018af",
  "sha256": "Lny0dZHead23KZOVGlnYl7e0FTu-FQqzmeokqqZJZxE",
  "ext": ".png",
  "mime": "image/png",
  "size": "0.86",
  "url": "/uploads/cffd30f367994f9788ca31c4ab0018af.png",
  "provider": "local",
  "public_id": null,
  "created_at": "2019-01-15T05:14:40.000Z",
  "updated_at": "2019-01-15T05:14:40.000Z"
  }
  },
  {
  "id": 2,
  "title": "タイトル2",
  "content": "テキスト2テキスト2",
  "created_at": "2019-01-15T05:15:14.000Z",
  "updated_at": "2019-01-15T05:15:14.000Z",
  "cover": {
  "id": 2,
  "name": "150x150.png",
  "hash": "4646c71aaeee40f687cd263cf6afd9f2",
  "sha256": "Lny0dZHead23KZOVGlnYl7e0FTu-FQqzmeokqqZJZxE",
  "ext": ".png",
  "mime": "image/png",
  "size": "0.86",
  "url": "/uploads/4646c71aaeee40f687cd263cf6afd9f2.png",
  "provider": "local",
  "public_id": null,
  "created_at": "2019-01-15T05:15:14.000Z",
  "updated_at": "2019-01-15T05:15:14.000Z"
  }
  },
  {
  "id": 3,
  "title": "タイトル3",
  "content": "テキスト3テキスト3",
  "created_at": "2019-01-15T05:15:31.000Z",
  "updated_at": "2019-01-15T05:15:31.000Z",
  "cover": {
  "id": 3,
  "name": "150x150.png",
  "hash": "436a2f656ea843f8bb1eda6f2b404794",
  "sha256": "Lny0dZHead23KZOVGlnYl7e0FTu-FQqzmeokqqZJZxE",
  "ext": ".png",
  "mime": "image/png",
  "size": "0.86",
  "url": "/uploads/436a2f656ea843f8bb1eda6f2b404794.png",
  "provider": "local",
  "public_id": null,
  "created_at": "2019-01-15T05:15:31.000Z",
  "updated_at": "2019-01-15T05:15:31.000Z"
  }
  }
]

Nuxt.js

先ほど設定したstrapiのjsonデータを、Nuxt.jsで取得して表示させます。 まずmy-projectにNuxt.jsをインストールします。yarnもしくはnpxが必要です。 my-project直下で下記コマンドを実行してください。Use axios moduleはyesにして、それ以外はdefaultでOKです。

yarn create nuxt-app nuxtjs
cd nuxtjs
npm run dev

http://localhost:3000/ にブラウザでアクセスすると下記のように表示されると思います。これでNuxt.jsのインストールは完了です。 f:id:astamuse:20190116110746p:plain

次に、/my-project/nuxtjs/pages にposts.vueファイルを作成して、下記コードを記述してください。 Nuxt.jsは、pages以下のvueファイルとurlが連動するようにルーティングされる規約なので、http://localhost:3000/posts のファイルを作成していることになります。

<template>
  <section class="container">
     <ul>
       <li v-for="post in posts" :key="post.id">{{post.id}}:{{post.title}}{{post.content}}、<img :src="`http://localhost:1337/${post.cover.url}`"></li>
     </ul>
  </section>
</template>

<script>
import axios from 'axios'

export default{

  async asyncData({app}){
    let res = await axios.get('http://localhost:1337/posts')
    return {posts : res.data}
  }

}
</script>

asyncDataは、Nuxt.jsのメソッドでページコンポーネントがロードされる前に呼び出され、ここでgetしたpostsデータはSSR(サーバサイドレンダリング)されます。strapiとnuxt.jsを起動させた状態で、 http://localhost:3000/postsにアクセスすると、先ほど入力したデータが表示されていると思います。(ブラウザでソースを見て頂ければわかりますが、SSRされています)
これだと流石に見た目がアレなので、Bootstrapで少しスタイルを修正します。
/my-project/nuxtjs/nuxt.config.js のlinkにbootstrapのcssを追加します。

  head: {
    title: pkg.name,
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: pkg.description }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      { rel: 'stylesheet', href: 'https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css' }
    ]
  },

/my-project/nuxtjs/layouts/default.vueのtemplateにnavを追加

<template>
    <div>
        <nav class="navbar navbar-light bg-dark">
            <a class="navbar-brand text-light" href="#">
                strapi + Nuxt.js
            </a>
        </nav>
        <nuxt/>
    </div>
</template>

/my-project/nuxtjs/pages/posts.vueを下記のように変更します。

<template>
    <div class="container">
        <div class="starter-template">
            <div class="card-deck">
                <div class="card" v-for="post in posts" :key="post.id">
                    <img class="card-img-top" :src="`http://localhost:1337/${post.cover.url}`">
                    <div class="card-body">
                        <h5 class="card-title">{{post.title}}</h5>
                        <p class="card-text">{{post.content}}</p>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    import axios from 'axios'

    export default {
        async asyncData({app}) {
            let res = await axios.get('http://localhost:1337/posts')
            return {posts: res.data}
        }
    }
</script>

<style>
    .starter-template {
        padding: 3rem 1.5rem;
        text-align: center;
    }
</style>

strapiはcmsなので、もちろん画像やテキストを先ほどのadmin画面から変更できます。

f:id:astamuse:20190116110739p:plain 手順としては多少長いですが、思いのほか簡単にインストールできたのではないでしょうか。
個人的には、strapiとNuxt.jsで旧来のモノリシックなCMSでは得られない風通しの良さを感じました。CMSとクライアントサイドが疎結合になることで、必要であればCMS側だけを変更したり、クライアントサイドのフレームワークを変更できるようになります。また、Nuxt.jsのようなクライアントサイドのフレームワークを導入することでGoogle的に質の高いサイトを低コストで構築可能で、メリットは大きいでしょう。

アスタミューゼでは、エンジニア・デザイナーを募集中です。ご興味のある方は遠慮なく採用サイトからご応募ください。お待ちしています。

手間なし、負担なし、しがらみなし 〜とある社内活性化施策〜

f:id:astamuse:20190107143637p:plain
謹賀新年

ご挨拶

新年、明けましておめでとうございます。本年もよろしくお願いいたします。

scalaでバックエンドを開発しているaxtstar(@axtstart)です。

さて、今回は昨年開催された社内イベントから出てきた あるもの について書きたいと思います。

よくある内容かもしれないけど

昨年の9月頃社内のイベントで、全社員が、 チームに分かれてアイデアを出し合い、どうすれば社内が活性化するのかというのをディスカッションし、発表するというものがありました。

また、出されたアイデアに対して、上位3チームには実際に予算がついて実施できる(しなければ?)というものでした。

我がBチームはランダムランチのような「AI飯」というのを提案しました。 これは、食事代を会社側で提供し、あまり交流のない部署のメンバ同士でランチに行くことで、社員同士をつなぎ、社内活性化を狙ったものでした。

コンセプトは、

  • 手間なし
  • 負担なし
  • しがらみなし

上記の3つを実現するため、

手間なしは、マシンによるお店、人の選定を行うことで、手間が無いばかりでなく恣意的にならないということ、

負担なしは、会社からお金が出るということ(つまりタダ飯)

しがらみなしは、今回は部長職以上は参加に加えないこと

というような内容でした。

その結果見事3位になって、予算10万円を獲得しました。

考えることは結構ある

上記のAI飯を実現するため、

  • お店をどう決定するか
  • どうやってメンバを決めるか
  • お金をどう渡すのか

という課題がありました。

お店に関しては、まず何らかのサービスから、弊社所在地を中心に1Kmといった範囲の店舗を抽出し、Bチームの誇るグルメ通である、Nさん、Tさんにより、狭かったり、常に混んでいたりとグループでの食事には不適な店舗を除外してもらいました。

f:id:astamuse:20190109094826p:plain
範囲

メンバの選定に関しては、部署を考慮して5人を決めた後、上記のお店と実施日を記載したメッセージを送信して、参加可否を確認しました。参加否の場合、新たにメンバを選定し同じ手順で参加可否を確認しました。

メンバが期限までに決まれば、その時に(役職などは無関係に)リーダを決定して参加者全員にメンバを通知することにしました*1

実施日にリマインドを送信し、お金に関してはKさん提案の事前申請による人数×5人分といった金額をリーダに渡して実施することになりました。

f:id:astamuse:20190109101300p:plain
メンバ選定と金銭授受

実装とか細かいところ

実装はチームのメンバの中に開発部のメンバが私を含めて二人いたことから、その二人で主導することになりました、私は種にslack部分などのインタラクションを担当しました。他にFさんが、メンバ選定のロジックを組むことになりました。

slackインタラクションの作り方

https://api.slack.com/apps

↑こちらで、新しいbotやslash commandが作れます。

f:id:astamuse:20190107143923p:plain
slack appの作り方

インタラクションはbot作成とよく似ていますが少し違うのは、

UserとSlack両方とやり取りがある点です。

なのでかどうかわかりませんが、「Add features and functionality」で下記を指定します。

  • Bots
  • Interactive Components

Interactive Componentsでは後述しますが、URLエンドポイントを指定する必要があります。

f:id:astamuse:20190107183118p:plain
request url

APIサーバ(URLエンドポイント)

slackに対してメッセージを送信するのはどこからでもできる*2のですが、インタラクションに関しては別で、先ほど設定した「Interactive Components」のInteractivityのRequest URLだけが返答を返すことができますので、こちらをAPIサーバとして実装する必要があります*3。またこちらはSSL対応の必要もあります。

SSL対応は我らがインフラエンジニアTさんにLet's encryptにて設定をお願いしました。

今回Fさんと協議を重ねた結果APIサーバはpythonでいこうという話になりました。

さらに、webサーバにはFlask、データベースはmongodbを利用しました。*4

ユーザ選定

ユーザ選定はslackのユーザリストから作成します

下記を利用します。

https://api.slack.com/methods/users.list/test

その後、先述した部長職以上の方を対象外とする処置を施しDBに入れます。

お店選定

お店の情報はyelpのapiを利用することにしました。

www.yelp.com

Nさん、Tさんによるフィルタリング済み店舗リストをDBに入れておきます。

yelpの情報を最大限利用する形でデータを格納しました。(そのままという)

{
    "id": "**",
    "alias": "**",
    "name": "**",
    "image_url": "**",
    "is_closed": false,
    "url": "**",
    "review_count": 4,
    "categories": [
        {
            "alias": "****",
            "title": "****"
        }
    ],
    "rating": 3.5,
    "coordinates": {
        "latitude": 0,
        "longitude": 0
    },
    "transactions": [],
    "price": "",
    "location": {
        "address1": "*",
        "address2": "",
        "address3": "",
        "city": "",
        "zip_code": "*",
        "country": "JP",
        "state": "13",
        "display_address": [
            "",
            ""
        ]
    },
    "phone": "+81",
    "display_phone": "03-",
    "distance": 0
}

こちらから1店舗を実施時に抽出します。

ユーザへのメッセージ送信のところ

メッセージの作成にはslackのInteractive messageを利用し、ユーザからの返信を受け取れるよう*5にしました。

下記jsonに対して、プログラムからtitle、image_url等を設定してslackに送信します。

{
    "fallback": "none",
    "color": "#258ab5",
    "attachment_type": "default",
    "title": "",
    "title_url": "",
    "text": "",
    "image_url": "",
    "fields": [
        {
            "title": "rating",
            "value": "",
            "short": false
        }
    ],
    "callback_id": "attendance_confirm",
    "actions": [
        {
            "name": "admit",
            "text": "参加します",
            "value": "admit",
            "type": "button"
        },
        {
            "name": "absent",
            "text": "欠席します",
            "value": "absent",
            "type": "button"
        }
    ]
}

slack上での表示は以下のようになります。

f:id:astamuse:20190107174410p:plain
お誘いメッセージ

上記の「参加します」、「欠席します」ボタンが押されたとき、エンドポイントにcallback_idが伝わります。

このcallback_idや送信ユーザを元に、参加可否により、「参加ありがとうございます」「欠席残念です」といったメッセージをslackに返します。*6

実施日当日

実施当日、下記メッセージでリマインドを行い、実施ちょっと前にも似たようなメッセージを送ります。

f:id:astamuse:20190108194430p:plain

そしてリーダにお金を渡して、食事会に行ってもらいます。

アンケート

開催の2時間後にTさん作成のアンケートへのリンクを記載したメッセージを送信し、社員の満足度などを聞きました。

12月からトライアルという形でスタートしており、

参加した社員からはかなり好意的な反応や、店の選定のちょっとした要望などがが返ってきており、これはすごく社内活性化になっているなと感じています。

さらに参加率を上げるため、例えばGoogleカレンダーと連携してオファーの時間に予定が埋まっている人は省くなど、新たに考えていく必要もあるのかなと思いました。

統計情報

実施回数 参加人数(のべ) アンケート回答 継続希望
6 27人 23人 22人

まとめ

思ったよりは良い反応がもらえて、業務の合間を縫って作った甲斐がありました。

また、作っている時に、他の部署の人と仲良くなれた気がします。自分的には大成功でした。

いかがでしたでしょうか?

アスタミューゼでは現在、エンジニア・デザイナーを絶賛大募集中です。 興味のある方はぜひ下記バナーからご応募ください。

*1:不測の事態があった場合はリーダに判断してもらうということも

*2:tokenがあれば

*3:通常のSlackBotは常駐プロセスのアプリ

*4:この辺のこまかいところは、別途書きたいと思います。

*5:受け取るのが先ほどのエンドポイント

*6:ちなみにこの処理は3秒以内に返すというslackのルールがあります。

Copyright © astamuse company, ltd. all rights reserved.