蛇ノ目の記

技術のことも。そうでないことも。

2018年を振り返る

年の瀬なので今年を振り返ってみる。という去年とまったく同じ書き出し。

nao-y.hatenablog.com

仕事

2018年はPythonエンジニアとして働いた初めての一年だった。

技術

Pythonだけでなく、AWS(IAM, S3, RDS, Route53)やnginxを触ることができた。

AWSではIAMユーザーを作ることを覚え、個人で作っているDiscord botでDynamoDBを使うときに応用できた。nginxではバーチャルサーバの基本的な設定に加えて、Let's Encryptによるサーバ証明書、OpenSSLによるクライアント証明書の有効化を覚えた。

執筆

今年の初めに書いたブログで「書く実績を解除する」ことを目標の一つにしていた。

ブログ初めには遅すぎた - 蛇ノ目の記

Software Design9月号の記事を書くという形で達成できた。編集者の方やレビュアーのBPメンバーには本当に感謝。

nao-y.hatenablog.com

本についてはもう一点。「スラスラ読める Pythonふりがなプログラミング」の監修をやらせていただいた。世の中に数多くあるPython入門本の入り口となる一冊で、超入門書といえる。

book.impress.co.jp

親にこの本の監修をしたことを話したところ、これまでプログラミング未経験だった父親が1冊通して読んでプログラミングの便利さを理解できたと言ってくれた。

Pythonコミュニティ

みんなのPython勉強会

1月のイベントで登壇した。

startpython.connpass.com

「Pythonでスタートして、Pythonを仕事にするまでの話」
横山 直敬(ビープラウド)
みんなのPython勉強会がきっかけで勉強を再開し、がっつりPythonをやる会社に入社しました。
Pythonによるキャリア形成の話やビープラウドに入ってからの話をします。

Pythonを独学して自分が面白いと思うことをやっていくうちにPythonが本業になった話をした。LTを目標に勉強する「LT駆動開発」なんて言葉を使っていたら、思いもよらないところで他の人に影響を与えていて本当に驚いた。

GitPitch Presents: github/NaoY-2501/GitPitch-Slides

The Fastest Way from Idea to Presentation for everyone on GitHub, GitLab, and Bitbucket.

PyCon APAC 2018

初の海外カンファレンス参加。そしてシンガポールに初上陸。アレな英語力を総動員して必死にトークを聴き取ろうとがんばっていた。

PyCon APAC 2018 カテゴリーの記事一覧 - 蛇ノ目の記

ここで聴いたFlaskによる機械学習WebアプリのトークはPyQの問題作成に活かすことができた。

blog.pyq.jp

帰国から即みんなのPython勉強会に参加して最速でPyCon APAC 報告LTをやった。

docs.google.com

PyCon JP 2018

2017年に引き続きスタッフとして参加。今年はコンテンツチームとして活動した。

カンファレンスの肝になるコンテンツを自分たちで企画する面白さを実感しつつも、「特に重要なトークの採択を1チームだけでやっていいのかな」という思いもあった。スタッフ全体で決めるのがいいのかな。

nao-y.hatenablog.com

趣味

開発

詳しくはPythonタグをば。

Python カテゴリーの記事一覧 - 蛇ノ目の記

OpenStreetMapのデータを使った位置情報の可視化やQRコードに緊急時の情報を埋め込むWebアプリを作ったりしていた。あとはDockerを使ったPythonスクリプトの実行など。広く浅くといった感じ。

写真

Singapore 2018

Shinjuku Night Scene 2018

実は仕事でも写真を撮っていて、PyQオフィシャルブログで連載している「Pythonエンジニア列伝」のnobolisさん回の写真を担当した。

blog.pyq.jp

ポーカー

テキサスホールデムをやっている。

今年はポーカー仲間ができ、主に高円寺のバーで仲間たちとやっている。

上手い人が多くてよくボコられているけど、その割に自分がなかなか成長してない……。

最近、火曜日のまちこ会行けてなくてすいません(´・ω・`)

音楽

音楽自体はずっといろいろ聴いていたけど、音楽ネタのブログを書き始めた。

C95で買った同人音楽の紹介も近いうちに書きたいな。

今年始めたこと: アイドルを聴き始めた - 蛇ノ目の記

MOP of HEAD/空きっ腹に酒/DALLJUB STEP CLUBのライブでぶち上がった - 蛇ノ目の記


Pythonエンジニアとして過ごす初めての1年が経ち、仕事でPythonを書くことにも慣れてきたけど、「〜完全に理解した」とか言う間もなく「〜ぜんぜんわからない」になるくらいには苦戦しながらやっている。主にDjangoとnginx。Web系なのにWebに弱い(´・ω・`) 経験を積んでなんとかしていきたい。愚者なので経験で学ぶ。

なんか去年より振り返り方が雑な気もするけど、ここらで2018年の振り返りを終わりにする。

2018年はありがとうございました。

2019年もよろしくお願いします。

MOP of HEAD/空きっ腹に酒/DALLJUB STEP CLUBのライブでぶち上がった

12/8にDALLJUB STEP CLUBの新アルバムプレリリースライブに行ってきた。

"エモい"と"ぶち上がる"くらいしか音楽の感想を示す語彙を持ってないので、ライブのまとめ 。

MOP of HEADとDALLJUB STEP CLUBは撮影可なので自分でとった映像。

なお、やたら大きく聴こえる歓声はぶち上がってる自分のもの。

MOP of HEAD

2018/12/09 MOP of HEAD "S.A"

2018/12/08 MOP of HEAD "B.O.B"

ドラムの山下さんが中学の先輩だったりする。というのは少し前にライブハウスで話す機会があって知ったこと。世間って狭い。

空きっ腹に酒

www.youtube.com

名前は少し目にしたことがあったけど、聴くのはこのライブが初めてだった。

他の2バンドとは異なり、ラップを基盤としたロックという雰囲気。

DALLJUB STEP CLUB

2018.12.08 DALLJUB STEP CLUB "犬っぽい(Inuppoi)"

WOZNIAKでも活躍する星優太さんがいるバンド。MOP of HEADと同じくクラブミュージック寄りでMOPよりさらにそっちらしい雰囲気。

めっちゃかっこよかった。少し前にsora tob sanakaとツーマンライブやってたけどそれも行ってればよかった…。

DALLJUBの新アルバムのダイジェストはこちら。

www.youtube.com

あとWOZNIAKでお気に入りのHeptagon。

www.youtube.com

今年始めたこと: アイドルを聴き始めた

BeProud Advent Calender 2018 n日目の記事ではありません。

あとシリーズものっぽいタイトルだけど続きません。

アイドルといってもメジャーなのは聴いてないので、推し紹介みたいな趣き。

この間のBPLLでの自己紹介のお題が「今年始めたこと」だった。

bpstudy.connpass.com

そのときは「区立体育館の激安ジムで体を動かし始めた」と喋ったんだけど、アイドルを聴き始めたのも今年からだったことに、今日sora tob sakanaのライブに行ったことで気づいた。

sora tob sakana

sora tob sakanaは平均年齢17歳のアイドルグループ。通称オサカナ。ハイスイノナサsiraphで活動している照井順政さんがプロデュースしている。

siraphについては今年の1月にブログに書いてるので、もしよければそちらもどうぞ。雪山合宿に行ってきた話とライブに行ってきた話 - 蛇ノ目の記

オサカナはsiraphが出るイベントで2回くらい観たことがあって、そのときはあまり気に留めていなかった。けれど7月頭に行ったシーシャ屋さんの店内で流れているのを聴いて、唐突にハマることになった。

照井さんの作るポストロックの流れを汲む曲・オサカナのかわいい声が合わさって刺さりまくり、そのままハマりこんだ。ライブで観るとそこに振り付けのかわいさがプラスされるのでヤバい。「ああ無理尊い」と限界オタクと化す。

www.youtube.com

www.youtube.com

他に聴いてるアイドル

ヤなことそっとミュート

www.youtube.com

www.youtube.com

maison book girl

www.youtube.com

www.youtube.com

こうやって見るといわゆるアイドルって感じの曲より、ロックとかポストロック、現代音楽みたいな他のジャンルが濃く出ているのが好きなんだなと思う

Ubuntu 16.04でLet's Encryptを使ってサーバ証明書を取得する

BeProud Advent Calender 2018 15日目の記事です。

adventar.org

仕事でサーバーにSSL証明書を入れる機会があった。

できればワイルドカード証明書使いたい、という気持ちがあった。そこで、手軽に使えてワイルドカード証明書にも対応しているというLet's Encryptを採用。

Let's Encryptのクライアントならワイルドカード証明書が取得できる、そう思っていた時期がありました(´・ω・`)

環境

tl;dr

certbotではなくletsencrypt

Let's Encryptユーザーガイドを見ると、certbotコマンドを使っているが、Ubuntu 16.04ではletsencryptコマンドを使う。

インストール方法: Ubuntu 16.04 (xenial)に従って、

sudo apt-get install letsencrypt python-letsencrypt-apache

とするだけでいい。apacheプラグインが不要な場合は python-letsencrypt-apacheは省略する。

Ubuntu 16.04にインストールする方法はあるが、リポジトリを追加する必要がある。

www.digitalocean.com

オーケストレーションの際にリポジトリを追加する手間が増えるので、今回は採用しなかった。ワイルドカード証明書は必須の要件ではなかったし。

ワイルドカード証明書

ワイルド証明書とは、一枚の証明書で同じドメインに属しているサブドメインSSL化ができる証明書のこと。

証明書を取得する際に、*.example.comのようにドメイン名の左側にアスタリスクをつける。

参考: ワイルドカード証明書について | JPRS

ワイルドカード証明書を取得するには、Let's EncryptのパッケージがACME v2 APIに対応していなければいけない。ACMEとは証明書発行のプロトコルのこと。

参考: Let's Encrypt を支える ACME プロトコル - Block Rockin’ Codes

Let's Encryptでのワイルドカード証明書取得については以下が詳しい。

ACME v2 とワイルドカード証明書の技術情報 - Let's Encrypt 総合ポータル

ワイルドカード証明書と ACME v2 へ対応 - Let's Encrypt 総合ポータル

ACME v2 APIに対応したクライアントは、ACME Client Implementations - Let's Encrypt - Free SSL/TLS Certificatesで一覧できる。そしてここにletsencryptの名前はない

サーバ証明書の取得

Webサーバが動作している環境では、webrootプラグインを使う。

$ letsencrypt certonly --webroot {webroot-path} -d certificated.example.com

{webroot-path}はWebサーバのドキュメントルートディレクトリ。

Webアプリが/home/www/myapp/であれば、{webroot-path}/home/www/myapp/になる。

このコマンドを実行すると{webroot-path}/.well-known/acme-challengeにワンタイムトークンが作られる。Let's Encryptの認証サーバーがexample.com/.well-known/acme-challenge/{token}に対してHTTPリクエストすることで、ドメインの認証を行う仕組みになっている。

今回の案件ではドキュメントルートディレクトリではなく/home/www/letencrypt/を指定した。

この場合、HTTP通信(80番ポート)を通したcertificated.example.comへのリクエストに対して、/home/www/letencrypt/マッピングしておく。

# /etc/nginx/nginx.conf
server {
    listen 80;
    server_name certificated.example.com;

    location / {
        root /home/www/letsencrypt;
    }
}

コマンドを実行するとどうなる?

過去に証明書を取得したことがない場合は、鍵の紛失時の連絡や期限切れ通知に使うメールアドレスを入力するTUIが起動する。メールアドレスを入力すると利用規約への同意が求められる。これも初回のみ。

nginxにSSL通信の設定をする

サーバ証明書が取得できたので、nginxの設定をしていく。

サーバ証明書秘密鍵/etc/letsencrypt/live/{ドメイン名}に置かれている。

# /etc/nginx/nginx.conf
server {
    listen 443;
 server_name certificated.example.com
    ssl on;
    ssl_certificate /etc/letsencrypt/live/certificated.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/certificated.example.com/privkey.pem;

    location / {
    . . .
    }
}

server {
    listen 80;
    server_name certificated.example.com;

    location / {
        root /home/www/letsencrypt;
    }
}

Djangoのtemplateでdefaultdictを使えない件について

BeProud Advent Calender 2018 9日目の記事です。

adventar.org

案件でDjangoを使っていたときにdefaultdictがtemplateにレンダリングできないことに気づいたので検証してみた。

案件で使っているバージョンは1.9.3。

おしながき

ディレクトリ構成

Django 1.9.3, Django 2.1.4, jinja2で確かめるということで変則的なディレクトリ構成になっている。 mysite以下はDjangoプロジェクトの通常の構成。

advent_cal_2018
  ├ django_1.9.3
  │ ├ mysite
  │ └ env
  ├ django_2.1.4
  │ ├ mysite
  │ └ env
  └ jinja2
    ├ env
    └ defaultdict_survey.py

検証用のWebアプリ

localhost:8000 に繋ぐと辞書の中身を表示するだけのWebアプリをでっち上げた。

# mysite/myapp/views.py
from collections import defaultdict, OrderedDict

from django.shortcuts import render


def index(request):

    # dictionary
    normal_d = {
        'foo': 'foo',
        'bar': 'bar',
        'baz': 'baz',
    }

    # defaultdict(list)
    default_d_1 = defaultdict(list)
    s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]

    for k, v in s:
        default_d_1[k].append(v)

    # defaultdict(str)
    default_d_2 = defaultdict(int)
    keys = ['ham', 'spam', 'eggs', 'ham', 'spam', 'spam', 'eggs']

    for k in keys:
        default_d_2[k] += 1

    # OrderedDict
    ordered_d = OrderedDict()
    s = [(0, 'zero'), (1, 'one'), (2, 'two')]
    for k, v in s:
        ordered_d[k] = v

    context = {
        'normal': normal_d,
        'default_d_list': default_d_1,
        'defaukt_d_int': default_d_2,
        'ordered_d': ordered_d,
    }

    return render(request, 'myapp/index.html', context)
<!--mysite/myapp/temaplates/myapp/index.html-->
<html>
<head>
    <title>BP Advent Calendar 2018</title>
</head>
<body>
    <div class="normal">
        <h1>dictionary</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in normal.items %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>

    <div class="defaultdict-list">
        <h1>defaultdict(list)</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in default_d_list.items %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>

    <div class="defaultdict-int">
        <h1>defaultdict(int)</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in default_d_int.items %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>

    <div class="OrderedDict">
        <h1>OrderedDict</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in ordered_d.items %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>

</body>
</html>

Django 1.9.3で確認する

$ pwd
/Users/user/Python/works/advent_cal_2018/django_1.9.3
$ ls
env                     mysite                  requirements.txt
$ . env/bin/activate
(env) $ cd mysite/
(env) $ pip list
Package    Version
---------- -------
Django     1.9.3  
pip        10.0.1 
setuptools 39.0.1 
You are using pip version 10.0.1, however version 18.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
(env) $ python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
December 09, 2018 - 05:46:05
Django version 1.9.3, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

表示された画面 f:id:Nao_Y:20181209144824p:plain

確かにdefaultdictはレンダリングされていない。

Django 2.1.4で確認する

$ pwd
/Users/user/Python/works/advent_cal_2018/django_2.1.4
$ ls
env                     mysite                  requirements.txt
$ . env/bin/activate
(env) $ cd mysite/
(env) $ pip list
Package    Version
---------- -------
Django     2.1.4  
pip        10.0.1 
pytz       2018.7 
setuptools 39.0.1 
You are using pip version 10.0.1, however version 18.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
(env) nao-Mac:mysite nao$ python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
December 09, 2018 - 05:52:18
Django version 2.1.4, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

表示された画面 f:id:Nao_Y:20181209145345p:plain

2.1.4でもdefaultdictはレンダリングできないっぽい。

Jinja2で試してみる

Jinja2ではテンプレートにdefaultdictを埋め込んで、HTMLを出力することで確認する。

# jinja2/defaultdict_survey.py
from collections import defaultdict, OrderedDict

from jinja2 import Template

template = """
<html>
<head>
    <title>BP Advent Calendar 2018</title>
</head>
<body>
    <div class="normal">
        <h1>dictionary</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in normal.items() %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>

    <div class="defaultdict-list">
        <h1>defaultdict(list)</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in default_d_list.items() %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>
    
    <div class="defaultdict-int">
        <h1>defaultdict(int)</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in default_d_int.items() %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>

    <div class="OrderedDict">
        <h1>OrderedDict</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in ordered_d.items() %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>

</body>
</html>
"""

template = Template(template)

# dictionary
normal_d = {
    'foo': 'foo',
    'bar': 'bar',
    'baz': 'baz',
}

# defaultdict
default_d = defaultdict(list)
s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]

for k, v in s:
    default_d[k].append(v)

# defaultdict(list)
default_d_1 = defaultdict(list)
s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]

for k, v in s:
    default_d_1[k].append(v)

# defaultdict(str)
default_d_2 = defaultdict(int)
keys = ['ham', 'spam', 'eggs', 'ham', 'spam', 'spam', 'eggs']

for k in keys:
    default_d_2[k] += 1

# OrderedDict
ordered_d = OrderedDict()
s = [(0, 'zero'), (1, 'one'), (2, 'two')]
for k, v in s:
    ordered_d[k] = v

context = {
    'normal': normal_d,
    'default_d': default_d,
    'ordered_d': ordered_d,
}

rendered = template.render(
    normal=normal_d,
    default_d_list=default_d_1,
    default_d_int=default_d_2,
    ordered_d=ordered_d)

print(rendered)
$ cd 
$ pwd
/Users/user/Python/works/advent_cal_2018/jinja2
$  ls
defaultdict_survey.py   env                     requirements.txt
$ python defaultdict_survey.py > rendered.html
<!--jinja2/rendered.html-->
<html>
<head>
    <title>BP Advent Calendar 2018</title>
</head>
<body>
    <div class="normal">
        <h1>dictionary</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            
            <tr><td>foo</td><td>foo</td></tr>
            
            <tr><td>bar</td><td>bar</td></tr>
            
            <tr><td>baz</td><td>baz</td></tr>
            
        </table>
    </div>

    <div class="defaultdict-list">
        <h1>defaultdict(list)</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            
            <tr><td>yellow</td><td>[1, 3]</td></tr>
            
            <tr><td>blue</td><td>[2, 4]</td></tr>
            
            <tr><td>red</td><td>[1]</td></tr>
            
        </table>
    </div>
    
    <div class="defaultdict-int">
        <h1>defaultdict(int)</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            
            <tr><td>ham</td><td>2</td></tr>
            
            <tr><td>spam</td><td>3</td></tr>
            
            <tr><td>eggs</td><td>2</td></tr>
            
        </table>
    </div>

    <div class="OrderedDict">
        <h1>OrderedDict</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            
            <tr><td>0</td><td>zero</td></tr>
            
            <tr><td>1</td><td>one</td></tr>
            
            <tr><td>2</td><td>two</td></tr>
            
        </table>
    </div>

</body>
</html>

Jinja2では問題なくdefaultdictを使える。

Djangoでdefaultdictを使いたいとき

単純にdefaultdictをdictionaryに変換してあげるのが一つ。

テンプレートエンジンにJinja2を設定できるのでそれを利用する手もある。

https://docs.djangoproject.com/en/2.1/topics/templates/#django.template.backends.jinja2.Jinja2

ただ、すべてのテンプレートをJinja2に合わせる必要があるので、基本的にはdictionaryに変換することになる、のかなぁ。

なぜdefaultdictとOrderedDictで扱いが違うのか。

OrderedDict

通常の dict メソッドをサポートする、辞書のサブクラスのインスタンスを返します。

defaultdict

新しいディクショナリ様のオブジェクトを返します

collections --- コンテナデータ型 — Python 3.7.1 ドキュメント

このあたりに理由がありそう。

OrderedDictが 辞書のサブクラスであるのに対してdefaultdictは 新しいディクショナリ様のオブジェクト

字面だけ見るとdefaultdictは辞書のようで実際は辞書でない、と読める。Djangoのテンプレートエンジンは辞書とそのサブクラスには対応できるが、「ディクショナリ様のオブジェクト」には対応していない、のかな。

原因を把握するためにはdefaultdictとOrderedDictの実装、Djangoのテンプレートエンジンでの辞書の扱いを理解する必要がありそう。

プログラミングの面白さを感じながらITエンジニア目指してほしいよねっていうエモい話

ここ数年でプログラミングを学ぶハードルは格段に下がった。無料の動画コンテンツはたくさんあるし、少しお金を出せばUdemyなんかでさらに踏み込んだ内容を学ぶことだってできる。あとは実際に手を動かして学べるコンテンツも充実してきている。

「ITエンジニアになれば給料が上がる」とか「未経験でもITエンジニアになれる」という言葉も目にするようになってきた。

そんな中でおととい、Twitterのタイムラインでこのブログ記事を目にした。この1件だけでこういうことを言うのは憚れるけど、「未経験でもITエンジニアになれる」という向きには違和感を覚える。

30sman.com

プログラミングスクール上がりの新人の教育にとても苦労したというSEの体験談なのだけど、この新人が悪いとかプログラミングスクールが悪いとか言うつもりはなくて、ただ「仕事に就く」という目的だけで、それ以外のモチベーションが無いまま勉強していたがために起きた不幸なことなのかなと思った。

未経験の状態からでも十分な教育を受ければ実践で活躍できるようになれると信じているけど、なによりその前にプログラミングを面白いと感じられるかどうかが重要。プログラミングを教える側としては面白さに気づける仕組みやコンテンツを作りたい。一方、ITエンジニアを目指して勉強する側は「仕事に就く」ことだけにとらわれずに、プログラミングでできるいろいろな面白いことにも目を向けて、いざ仕事に就いたときに楽しくやれるようになってほしい。

周りが猛者ばかりだと生存者バイアスみたいなもので気づきにくいけど、プログラミングを面白いと感じられない人はいるはず。結構大変な話だけど、教える側はそこに気づいたら、フォローしたり、時にはITエンジニア以外の道を示すこともあっていいのかなと思う。そもそも面白く感じない人がなんでITエンジニア目指すんだよってなるけど、「未経験でも」っていう発想だとどうせ仕事だし、ってことでまあまあありそう。

ちなみに「未経験でもITエンジニアになれる」という言い方は仕事を下に見ているような感じがしてあまり好きじゃない。「未経験だけどプログラミングって面白そうだからITエンジニアになりたい」と思う人が増えたらいいなと願っている。あ、給料が上がるらしいっていうのはモチベーションがぶち上がっていいと思います。

Dockerコンテナ上でPythonスクリプトを動かしてみる

仕事でDjango製のWebアプリのテスト環境をDockerで作る、という試みをやったのでその入り口になる話を書いてみる。

Docker Hubを探せばPython3.7のイメージはあるけど、ここではUbuntu16.04のイメージを使う。

Ubuntu 16.04のイメージにPython3.7をインストールする

Dockerfile中にコメントを書いている通り、Ubuntu16.04にPython3.7をインストールする方法は以下の記事を参考にした。

medium.com

# Dockerfile
FROM ubuntu:16.04

# refs. Install Python3.7 in ubuntu 16.04
# https://medium.com/@manivannan_data/install-python3-7-in-ubuntu-16-04-dfd9b4f11e5c
RUN apt update && apt-get install -y \
    build-essential \
    checkinstall \
    libreadline-gplv2-dev \
    libncursesw5-dev \
    libssl-dev \
    libsqlite3-dev \
    tk-dev \
    libgdbm-dev \
    libc6-dev \
    libbz2-dev \
    zlib1g-dev \
    openssl \
    libffi-dev \
    python3-dev \
    python3-setuptools \
    wget \
    && mkdir /tmp/Python37
WORKDIR tmp/Python37
RUN wget https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tar.xz \
    && tar xvf Python-3.7.0.tar.xz
WORKDIR /tmp/Python37/Python-3.7.0
RUN ./configure --enable-optimizations \
    && make altinstall \
    && mkdir /usr/local/Python
WORKDIR /usr/local/Python

Linux力が全然無いので、Python3.7を入れるだけでこんなにパッケージが必要なのか(小並)となった。

docker-compose.ymlを書く

次にdokcer-compose.ymlを用意する。

Pythonスクリプトを置いたディレクトリをReadOnlyでマウントしてから、ファイルを実行する。

# docker-compose.yml
version: "2"
services:
  python:
    build: .
    container_name: run_python
    volumes:
      - ./src:/usr/local/Python/:ro 
    command: "python3.7 sample.py"

以下がそのスクリプト

# sample.py
print('spam ham eggs')

マウントしたスクリプトを実行する

docker-compose build でサービスをビルドしてから、docker-compose upでサービスを実行する。

スクリプトがファイルを出力するとき

docker-compose.ymlの中でマウントすると、コンテナ上で作成されたファイルはローカルにも作られる。

このとき、ReadOnlyを外しておかないと怒られる。

# sample.py
zen_en = 'zen_en.txt'
zen_ja = 'zen_ja.txt'

with open(zen_en, 'w', encoding='utf-8') as f:
    f.write('Beautiful is better than ugly.')

with open(zen_ja, 'w', encoding='utf-8') as f:
    f.write('醜いよりも美しい方が良い。')

これを書いて、サービスを実行するとローカルに2つのファイルが作られる。

ローカルに影響を与えたくないのであれば、DockerfileCOPYを追加する。

このとき、docker-compose.ymlからvolumesは消しておく。

# Dockerfile
.
.
.
WORKDIR /tmp/Python37/Python-3.7.0
RUN ./configure --enable-optimizations \
    && make altinstall \
    && mkdir /usr/local/Python
WORKDIR /usr/local/Python
COPY ./src /usr/local/Python/
# docker-compose.yml
version: "2"
services:
  python:
    build: .
    container_name: run_python
    command: "python3.7 sample.py"

スクリプトに変更があったときはサービスをビルドし直してから、実行する必要があってひと手間増えてしまう(´・ω・`)

sample.pyにコンテナ上にファイルが作られたか確認するためにファイルの中身を確認する処理を追記した。

# sample.py
zen_en = 'zen_en.txt'
zen_ja = 'zen_ja.txt'

with open(zen_en, 'w', encoding='utf-8') as f:
    f.write('Beautiful is better than ugly.')

with open(zen_ja, 'w', encoding='utf-8') as f:
    f.write('醜いよりも美しい方が良い。')

with open(zen_en, encoding='utf-8') as f:
    for line in f:
        print(line)

with open(zen_ja, encoding='utf-8') as f:
    for line in f:
        print(line)
$ docker-compose build
.
.
.
$ docker-compose up
Recreating run_python ... done
Attaching to run_python
run_python | Beautiful is better than ugly.
run_python | 醜いよりも美しい方が良い。
run_python exited with code 0

まとめ

「Dockerコンテナ上にスクリプトを置いて、コマンドを実行する」という流れを応用して、「Webアプリを置いてテストコマンドを実行」とすればDockerを使ったテスト環境を作れる。