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

エンジニア修行の記録

LTに初めて申し込むときの不安要素とその取り除き方

この記事は、エンジニアの登壇を応援する会 Advent Calendar 2018 11日目です。

LT、それは... 聞くは易し、行うのは難し。
バリバリ登壇するエンジニアに憧れてはいたものの、人前で登壇するなんて無理無理... 今までの自分はそんな風に思っていました。

それが今年の夏ごろに「エンジニアの登壇を応援する会」に入って以降、既に2回登壇することに成功しました。

これは秋の夜長の自由研究大会での発表。ジオキャッシング楽しいのでやろう。

これはGopher道場での発表。エンジニアの登壇を応援する会のイベントではありませんが、思い切ってやってみました。

この記事では、演台=死刑台みたいに思っていた頃の私の意識と、それをどうやって乗り越えたかについて書こうと思います。なお、5分くらいの短いLTを想定して書いています。

いやー、しゃべるネタないし...

いきなりですが、この壁がとても高いです。私はここを越えるのに1年近くかかりました。

私の場合、追い込まれなければ行動に移さないところがあります。 なので、まず登壇申し込みをし、その後ネタを探すという手荒な方法を取ることにしました。 ちなみにこういう人は私だけではないらしく、エンジニアの登壇を応援する会のSlackでは、誰かが登壇申し込みをすると「すぐ登壇」というスタンプが押されます。

また、とあるエンジニアの方の話では、普段からLT用のネタをメモアプリなどにストックしているそうです。あまった分はブログなどに流用するとのこと。
これを逆にすれば、自分の過去のブログ記事をもとにLTを作ることもできそうですね。

構成まとめられるかな...

5分というのは意外と短いです。
登壇を応援する会のベテランの方によると、箇条書き中心のスライドだと10枚くらい、一言のみのスライドが中心だと30枚が限度とのこと。
そのため、LTであまり複雑な話はできません。今年行った2つのLTでも、発表におさめられたのはもともと考えていた内容の半分程度でした。

構成がうまく決まらない...と悩んでしまう場合、それは発表する内容が多すぎる可能性が高いです。思い切って削ってみましょう。

スライド作るの面倒...

自分にとって一番の関門がここでした。大きなイベントで発表されるような美しいスライドを見てしまうと「あんなの作れねえよ...」と思ってしまいます。
ですが、ビジュアルデザインのことは一旦忘れましょう。

今年のスライドを作るにあたり、私は以下のように決めました。

  • スライドソフトに標準で入っているテーマを使う
  • フォントは1種類のみ
  • 文字の色は2色まで
  • アニメーションは使わない

ビジュアルに凝りだすと、永遠に時間が飛んでいきます。凝らないと決めることで時間を節約できますし、シンプルになってむしろ見やすいです。

上がっちゃって話せない...

ぜひ事前にリハーサルをしましょう。誰かを呼んで聞いてもらうのがベストですが、誰もいない会議室で一人でやるだけでも構いません。 そのうえで、一通り話して何分かかったか時間を計るのがおすすめです。

私はふだんから早口になってしまう傾向があり、ジオキャッシングのスライドでは5分で話す内容のはずがリハーサルでは4分で終わってしまいました。
そこで、もう少し各スライドに時間をかけて話していいんだと気付き、本番では聞き取りやすい速度で話すことができました。

ちなみに、エンジニアの登壇を応援する会では、本番の1-2週間前にリハーサルイベントも開催しています。 参加すると、登壇慣れした方々からのアドバイスも受けられます。皆さん初登壇がどれだけ緊張するかよく理解されている方ばかりなので、マサカリは飛んできません。安心ですね。

来年も登壇します

来年は倍の4回を目標に登壇したいと思っています。
お手柔らかによろしくお願いいたします。

Redashでadmin権限のユーザーがデータを見られなくなった

Redash運用中にやらかしたことをシェア。バージョンは4.0.1。

Redashにはdefaultというグループがあらかじめ用意されており、ユーザーを登録すると一度全員がdefaultグループに振り分けられる。
しかしこのグループは権限設定がFull Accessになっているため、そのままにしておくと全員がクエリをいじることができてしまう。
そこで新しくいろいろな権限設定のグループを作ってユーザーを振り分け、defaultグループからはユーザーを消すようにするのがセオリーだと思う。

しかし、グループの振り分けが終わった直後、adminグループにいるユーザーが、ダッシュボードもクエリも見られなくなってしまっていた。
webコンソールを見ても原因がつかめなかったので、RedashのPoatgreSQLを直接のぞいてみることにした。

Redashを動かしているサーバにログインして、以下のようにコマンドを叩く。

sudo -u postgres psql redash
SELECT * FROM groups WHERE name='admin';

すると、以下のように返ってきた。

id | org_id |  type   |            name            |         permissions        |          created_at
--+-------+ ------+-------------------+---------------------+-------------------------------
 1 |      1 | builtin | admin                      | {admin,super_admin}      | 2018-08-23 03:25:08.218876+00

adminグループにいるユーザは全ての権限を使えると思ってグループを振り分けたのだが、実際にある権限はadminとsuper_adminのみだった。
adminグループはあくまで管理用の権限のみを制御するグループであり、逆にユーザーがadminグループだけに所属していると、ダッシュボードやクエリの権限は何もなくなってしまうわけだ。
今回はadminグループのユーザーを別のグループにも所属させることで解決した。わかれば単純なことだが、グループ分けのときに勘違いしないようにしたい。

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 <設定したい権限名(カンマ区切り)> <DB上のグループID>

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

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が表示されるようになった。