りんごとバナナとエンジニア

エンジニア修行の記録

Redashで閲覧権限のみのユーザでもダッシュボードを見られるようにする

redash.io

業務でRedash(v4.0.1)を使い始めたが、今のところなかなか便利なうえ運用も楽なので気に入っている。
ただしwebコンソールでできることがまだまだ限られており、かゆいところに手が届きにくいときもしばしば。

特に権限の設定については、webコンソールでできるのはData Resourceを Full AccessView Only にするかの二択のみ。View Onlyにすると、クエリの作成・編集・実行ができなくなる。
しかし先日、開発チーム以外のユーザをView Onlyのグループに振り分けたところ、なんとダッシュボードを見られなくなってしまっていた。当然だがダッシュボードの裏ではクエリが実行されるので、その権限がないView Onlyユーザは情報を取ってくることができないわけだ。
これではせっかくのデータ活用ができない。かといってFull Access権限を与えるのは不安が残る。

実はRedashには、webコンソールからは操作できない細かな権限設定が用意されている。
CLIで以下のようなコマンドを叩くと、権限を変更することができる。

cd /opt/redash/current
sudo -u redash bin/run ./manage.py groups change_permissions --permissions 設定したい権限名(カンマ区切り)

グループを作った時点では、権限設定はデフォルトのものとなっている。コードを探すとデフォルトは以下のようになっていた。

DEFAULT_PERMISSIONS = ['create_dashboard', 'create_query', 'edit_dashboard', 'edit_query', 'view_query', 'view_source', 'execute_query', 'list_users', 'schedule_query', 'list_dashboards', 'list_alerts', 'list_data_sources']

View Onlyユーザでもダッシュボードを使えるようにするには、そのグループの権限からcreate_dashboard,create_query,edit_dashboard,edit_queryを外したものを指定すればよい。こうすれば、クエリやダッシュボードの内容を変えることはできないが、execute_query権限は残してあるためクエリの実行はできる。
もし権限の変更を間違えてしまっても、--permissionsオプションなしでコマンドを実行すればデフォルトの権限設定に戻すことができる。

ちなみに、この方法で権限設定を直した翌日に、Redash Meetup 4.0.0に参加したのだが、ほとんど同じ内容が発表されていた。
権限設定で悩んでいる人は意外と多いようなので、CLIに触れてみると少しだけ幸せになれるかもしれない。

builderscon 2018という楽しいお祭りに参加しました

とある方からチケットを譲渡いただけるという幸運にあずかり、土曜日に参加してきました。

buildersconに参加したのは今年が初めてなのですが、他のカンファレンスと違い、お祭りだなあという雰囲気を強く感じました。
特定の技術に絞ったわけではなく、また「エンジニアとして成長するぞ!オー!」という感じでもなく、純粋に面白い話をシェアすることが一番大切にされている楽しい空間でした。
最近なかなか仕事以外の活動ができていなかったのですが、力をつけるために勉強するというよりも、作りたいものを楽しく作って結果として力もつくというのが一番幸せですね。エンジニアとしての初心にかえることができました。

忘れないうちに今日聞いたセッションのまとめを。

あなたの知らないデータベースのロギングの世界

メインテーマは、情報流出インシデントをきっかけにしたDB監査ログの強化だったのですが、個人的にはProxySqlを入れたときのハマりどころの話に身をつまされました。

普段は目の前のコードを書けば終わりなのですが、アーキテクチャを変えるとなると、ちゃんとこういう観点から影響範囲を考えなければいけないですね。まだまだ自分の視野は狭いです。

ブログサービスのHTTPS化を支えたAWSで作るピタゴラスイッチ

speakerdeck.com

Let's EncryptとAWS Step Functions、AWS Lambda、さらにはDynamoDBのTTLと、個人的に面白そうな技術がいろいろ登場しました。 最終的な証明書自動発行の仕組みはもっとじっくり理解したいです。

Webアプリケーションエンジニアが知るべきDNSの基本

speakerdeck.com

つい最近業務でRoute53を使い始めたばかりなので、とても刺激になりました。インフラ系は理解しきっていないまま触るのがめちゃくちゃ怖いんですよね...
これも情報量が多いので、要復習です。

LT大会

面白系のLTが多かったのですが、アイスブレイクではなく発表内容自体で笑いが取れるというのは本当にすごいことです。 それだけその技術に習熟していて、なおかつ尖ったことをやっているという証だと思います。こんなLTがしたい。

最後に反省点

参加するセッションを選ぶとき、ちょっと「自分にとっての実用性」という軸に偏りすぎたかなと思っています。 次回参加するときは、もっと何の役に立つか知らないようなセッションにも行こうと心に決めました。きっとそれこそがbuildersconの醍醐味です。

Jenkinsでシェルスクリプトを-eオプションなしで実行したい

最近Jenkinsを使い始めたが、Jenkins上でシェルスクリプトを書くと、特に何も指定していなくても #!/bin/sh -xe として実行されてしまうようだ。
普通であれば、スクリプトの途中であってもエラーが出れば即ビルド失敗となるので助かる仕様だろう。
しかし今回は、ビルドの過程で以下のようなことをやりたかった。

nc -z 0.0.0.0 80
if [ $? -ne 0 ]; then
  docker-compose down
  exit 1
else
  ...
fi

dockerで起動させたwebアプリケーションを、seleniumで自動テストしたい。そこで正常に起動したかどうかを判定するために、 nc -z 0.0.0.0:8000 を叩く。通じなければdockerコンテナを停止させて終了する、という流れだ。
しかしJenkinsでは #!/bin/sh -xe として実行されるので、ncコマンドが失敗した時点で即終了となる。else句の中にも入らないのでdockerも立ち上がりっぱなしだ。
dockerを落とすのを忘れたくなかったので、なんとか回避する方法がないか調べてみた。

#!/bin/sh をつけてみる

軽く調べて出てきたのがこれ。Jenkins内のシェルスクリプトの冒頭に #!/bin/sh をつけ、設定を上書きするというもの。
しかしこれでは解消しなかった。今回はwebアプリケーションがpython環境だったため、プラグインとしてshiningpandaを使っており、ビルド手順の項目でもshiningpandaの下でシェルスクリプトを実行するようにしていた。
コードを確認したわけではないが、shiningpandaの仕様として常に-xeがつけられてしまう可能性もある。

シェルスクリプトでexit codeを受け渡す

そこでさらに調べた結果、シェルスクリプトの書き方で対応することができた。

exc=0
nc -z 0.0.0.0 80 || exc=1
if [ $exc -ne 0 ]; then
  docker-compose down
  exit 1
else
  ...
fi

|| は、前の処理の結果が偽だった場合のみ、後ろの処理を行うというもの。このため、ncコマンドが失敗した場合、 ||によってexc変数に1が入る。これをつけたことで、 ncコマンドの成否に関わらず nc -z 0.0.0.0 80 || exc=1 の行は最終的に処理され、次の行に進むことができる。 そしてif句でexc変数の値を判定し、dockerを落とすことができる。

シェルスクリプトの経験値が浅いのでこれは考えつかなかったが、 || でつなぐのはいろいろなシーンで使えそうだ。

DjangoのView呼び出しの仕組み ~getメソッドはどう呼ばれるのか

最近仕事でDjangoを使う機会が多くなった。
プログラミングを勉強し始めた頃にDjango Girls Japanのチュートリアルをやったことがあるからまあ大丈夫だろうと思っていたが、すぐにそれだけでは全く歯が立たないと気づいた。業務でDjangoを扱う際は、デフォルトの内部メソッドをオーバーライドしたり、デバッグの際に内部メソッドがどのように呼ばれているかを手がかりにしたりすることが多く、内部の処理を理解していないと実装の方針を立てられないのだ。
そこで今回は、Djangoアーキテクチャのうち、Viewの部分の処理内容を追ってみる。全ての汎用Viewの基本になる、Viewクラスのgetメソッドが呼ばれるまでを見てみたい。
今回は以下のサンプルコードで考える。Djangoのバージョンは2.1とする。

# urls.py
from django.urls import path
from myapp.views import IndexView

urlpatterns = [
    path('', IndexView.as_view(), name='index'),
]
# views.py
from django.shortcuts import render
from django.views.generic import View

class IndexView(View):
    def get(self, request, *args, **kwargs):
        return render(request, 'myapp/index.html')

ViewクラスはDjangoのviews/generic/base.pyの中に定義されている。長くなるので全部は掲載しないが、ここの実装を読んでいくと流れを追える。

まず、リクエストに応じて、urls.pyに書いたurlpatternsの中でマッチングが起こり、該当するURLのViewクラスが特定される。このプロセスをオーバーライドすることは少ないので省略する。

URLでViewクラスをimportした時点で__init__が呼ばれる。が、ここはキーワードを属性に入れているだけなのであまり重要ではない。

次に、クラスベースでViewを書いている場合は、明示的にas_view()を呼び出す必要がある。

# base.py
 
    @classonlymethod
    def as_view(cls, **initkwargs):
        """Main entry point for a request-response process."""
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError("You tried to pass in the %s method name as a "
                                "keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get
            self.request = request
            self.args = args
            self.kwargs = kwargs
            return self.dispatch(request, *args, **kwargs)
        view.view_class = cls
        view.view_initkwargs = initkwargs

        # take name and docstring from class
        update_wrapper(view, cls, updated=())

        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
        update_wrapper(view, cls.dispatch, assigned=())
        return view

これがやっていることは、クロージャであるview(request, *args, **kwargs)関数を返すこと。
ちなみに、実装を書くときによく使うself.requestはこの段階で定義されている。

このview(request, *args, **kwargs)関数は、self.dispatch(self.request, *args, **kwargs)メソッドを呼ぶのだが、この実装が面白い。

# base.py

    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
    
    #(中略)
    
    def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        if request.method.lower() in self.http_method_names:
            # リクエストメソッド名を値として持っておく
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed) 
        else:
            handler = self.http_method_not_allowed
        # ここでリクエストメソッド名に対応したメソッドを呼ぶ
        return handler(request, *args, **kwargs) 

あらかじめ定義してあるhttp_method_namesに、リクエストメソッドと同じものがあれば、その名前のメソッドを呼ぶようになっている。
例えばgetリクエストなら、handlerにgetが入り、get(request, *args, **kwargs)メソッドが呼ばれる。postリクエストならpost(request, *args, **kwargs)となる。
そしてこれらのメソッドは、Viewクラス内に定義はない。views.pyの中に自分で定義する必要があるわけだ。

# views.py
from django.shortcuts import render
from django.views.generic import View

class IndexView(View):
    def get(self, request, *args, **kwargs):
        return render(request, 'myapp/index.html')

getメソッドにsuper()などの記述が不要なのは、もともと定義されていないためである。
こうしてviews.pyのgetメソッドが呼ばれ、templateが返される。

これ以外にも、場合によってはその他のメソッドが呼ばれることがあるが、この流れを抑えておくと、他の汎用ビューの仕組みも理解しやすくなる。

DjangoでTemplateDoesNotExistと言われたら

別のブログ記事用に、Djangoで簡単なアプリを作った。モデルがなく、単にView関数を呼び出してTemplateを出すだけ。

from django.shortcuts import render
from django.views.generic import View

# Create your views here.

class IndexView(View):
    def get(self, request, *args, **kwargs):
        return render(request, 'myapp/index.html')

するとエラーが出た。

django.template.exceptions.TemplateDoesNotExist: myapp/index.html

このとき何をチェックすべきだろうか。

Templateのパスを確認する

自分がよく間違えるのが、Templateのパスの書き方。
公式ドキュメントを読んで、DjangoのTemplateの探し方を理解しておく必要がある。

Templateの探し方は、プロジェクトのsettings.pyの中にあるTEMPLATESの項目で規定されている。
デフォルトでは、プロジェクトを作ったあとは以下のような状態になっているはず。(Django2.1の場合)

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

DIRSに何も入っておらず、APP_DIRSがTrueになっているので、Djangoプロジェクト内の各アプリケーションの配下にあるtemplatesという名前のディレクトリ内を探索する。
そのため、単にアプリケーション配下にTemplateファイルを置いてもだめ。templatesというディレクトリを作る必要がある。
各アプリケーションのtemplatesディレクトリが探索の起点となるので、myapp/templates/myapp/index.html に置いたTemplateファイルをViewの中で指定するには、単にmyapp/index.html と書けばよい。

なお、探索の際はどのアプリケーションかを考慮せずに、最初にパスがマッチしたら探索終了となる仕様になっている。そのため、templates内にさらにディレクトリを切るなどして、名前空間を分けることが推奨されている。

ただ、今回の場合はmyapp/templates/myapp/index.htmlとなるようにファイルを作っていたので、パスの問題ではなかった。

INSTALLED_APPSを確認する

上記のTemplate探索にはもうひとつ前提がある。「各アプリケーションの配下にあるtemplatesディレクトリ」を探索するということは、アプリケーションと認識されていなければ探索されないということだ。
今回はそもそもここに原因があった。settings.pyのINSTALLED_APPSにmyappを登録するのを忘れていた。

INSTALLED_APPSには、そのプロジェクト内の全てのアプリケーションを登録しなければならない。
アプリケーションがあるパッケージ名をそのまま書いても動くことは動くが、推奨されているのは各アプリケーションのConfigurationクラスを登録することだ。
今回の場合、myapp.apps.MyappConfigを登録することで、無事にTemplateが表示されるようになった。

ファイル内の重複行を楽に探すunixコマンド

先日、大量のデータから完全に重複した行を探す処理をする機会があった。そのときはどうすればいいか手間取ってしまったが、コマンドを工夫すればすぐにできる内容だった。
以下の単純なサンプルログtest.txtを例に取る。

{"location_name": "title", "timestamp_micros": 1531209434523235}
{"location_name": "product1", "timestamp_micros": 1531209437623412}
{"location_name": "product1", "timestamp_micros": 1531209437623412}
{"location_name": "title", "timestamp_micros": 1531209456903712}

たった4行なのですぐわかるが、2行目と3行目が重複している。これをコマンドで抽出したい。 このとき、以下のようなコマンドを使うと、重複行の内容とその頻度がすぐわかる。

$ uniq -c test.txt | sort -n -k 1 -r  
   2 {"location_name": "product1", "timestamp_micros": 1531209437623412}
   1 {"location_name": "title", "timestamp_micros": 1531209456903712}
   1 {"location_name": "title", "timestamp_micros": 1531209434523235}

コマンドを分解してひとつずつ見てみる。

uniqコマンド

uniqコマンドは、ファイル内から重複する行を削除して標準出力する。

$ uniq test.txt
{"location_name": "title", "timestamp_micros": 1531209434523235}
{"location_name": "product1", "timestamp_micros": 1531209437623412}
{"location_name": "title", "timestamp_micros": 1531209456903712}

重複行が消えて3行になって出力された。

注意すべきなのが、重複行が隣り合っていないと検知できないこと。使う前に行のソートが必要になることが多い。
ただし今回はアクセスログだったのでtimestamp_micro順に並んでおり、事前のソートは必要なかった。

また、uniqコマンドの便利なオプションが-c
重複行が消えずに、重複した内容とその行数も表示してくれる。

$ uniq -c test.txt
   1 {"location_name": "title", "timestamp_micros": 1531209434523235}
   2 {"location_name": "product1", "timestamp_micros": 1531209437623412}
   1 {"location_name": "title", "timestamp_micros": 1531209456903712}

sortコマンド

行をソートできるコマンド。
-nは数値として並べ替えをするオプション。これをつけないと文字コード上の順になるため、10が2より前になったりしてしまう。
-kは空白区切りやカンマ区切りのデータのとき、並べ替えの基準となるキーを指定するもの。 -rはソート結果を降順に出力するもの。

ポイントになるのが-kの使い方。
上のuniq -cコマンドの出力を見ると、実データの前に重複した数が表示されている。しかもこの数と実データの間にはスペースが空いている。
つまりこの出力をパイプで受け渡し、-k 1とすれば、1番目のキーとして重複した数をソートに使うことができる。これがuniq -c test.txt | sort -n -k 1 -rの意味だ。

もっとも今回はソートのキーが一番最初のものなので、-nさえつけていれば-kはつけなくても良いのだが、このオプションを知っておくと便利なことが多いだろう。
ちなみに-kの数値が2〜4の場合は、データが数値ではないので並べ替えは起こらないが、5にするとtimestamp_microsの値でソートされる。5というのはデータをスペース区切りとみなしたときに5番目ということ。

調べているうちに、コマンドの本を昔読んだ記憶がよみがえってきた。やっぱり使わないと覚えられない。

クリックすると一瞬だけ光るボタンを作る

Vue.jsの勉強としてFizzbuzzゲームを作っている。数字を出して答えを4択で選ばせるのだが、選んだときに正解なら緑、不正解なら赤に一瞬だけ光らせたい。
Vue.jsのtransition/animationの項目を読んだが、これは要素を表示/非表示にするときの話で、色を変えるときの話ではなかった。
CSS@keyframesを使おうともしたが、これは状態をAからBに変えるというものであり、今回のように色を変えてまた戻すという処理とは違う。

どう実装すればいいか悩んでいろいろ調べた結果、単純にCSSとJSで制御すればいいと気づいた。

実装

See the Pen Button Blink with CSS Transition by Udomomo (@Udomomo) on CodePen.

↑読み込まれないようなら下のリンクから見られる
Button Blink with CSS Transition

作り中のコードから引っ張ってきたのでVue.jsによる実装になっているが、この部分だけ見ればVue.jsならではの機能は全く使っていない。
まず、CSSでボタンのdiv要素全体のbackground-colorにtransitionを設定しておく。これで背景色が変わるときはアニメーションが発生するようになる。
そのうえで、JSでクリックイベントを検知し、背景色を変える処理をする。そして、一定時間が経ったらsetTimeoutを呼び出して元の色に戻す。
transitionの所要時間を十分短くし、かつそれより少しだけ長い時間を指定してsetTimeoutを発火させれば、一瞬だけ光って元に戻るように見える。

学んだこと

Vanilla JSは大事。
JSフレームワークの機能だけに囚われないようにしないと。