新着記事

TORICO全社会イベント TORICO NIGHT 2019 でオペレータしました

Qiita の TORICO Advent Calendar 2019 の記事として書きました

TORICO NIGHT

TORICOでは、年2回のペースで全社員任意参加の食事会「TORICO NIGHT」を開催しています。

2017年から開始して、201911月の開催で5回を迎えました。

多くの会社は、12月に「忘年会」という形で食事会をされると思いますが、TORICO12月は商品が非常に多く売れる時期であり、物流チームも事務チームも気が抜けなくなるため、その時期を避けて秋口に食事会をします。

開催場所

TORICOでは、マンガの原画展やトークイベントを行うためのイベントスペースを自社運営しています。TORICO NIGHT を開催する際も自社イベントスペースを使うことが多くあります。

今回の TORICO NIGHT は、自社運営の池袋のイベントスペース「池袋虜」で行うこととしました。

運営スタッフ

TORICO NIGHT の運営スタッフ(幹事)は、毎回くじ引きなどで決めます。イベントの最後に、当会の幹事が決めた方法で次回幹事を決めることが多いです。2019年春のTORICO NIGHT での決め方は「役員によるくじ引き」で、私は自分自身を引き当ててしまったため2019 TORICO NIGHT の幹事となりました。

2019年秋の幹事は、部門がそれぞれ違う5名が選ばれました。その5名で、次のTORICO NIGHT の全てを決め、実施することになります。

実施6週間前

顔合わせ、役割分担決め

実施時期が近づいたので、幹事チームで集まっておおよその役割を決めます。

私は、製作物のデザインや製作、本番のPA、照明のオペレーションを担当することにしました。

その他のメンバーで、ケータリングの手配や参加者への案内、会場押さえなどをします。当日の進行は全員で作ります。

今後、週イチで一時間ほど使って幹事チームで集まることを合意し、Slackのプライベートチャンネルを作ってミーティングは終了しました。

実施5週間前

テーマ決め、ケータリング決め

会のテーマやおおよその進行を決めます。ケータリングは早めに業者を決めて、予約を取ります。

予算としては単価3000円強、ドリンク別 程度からあります。前回のケータリングの感想をメンバーで話し合いながら、良さそうな所を選定します。

今回の会のテーマとして、クイズ主体で行ってみようと決めたので、問題の提出を宿題としてMTGは終了しました。

実施4週間前

進行の詰め

全体のタイムテーブルを詰めます。

クイズ以外のゲームのテストプレイをしながら、全体の時間規模を見積もります。

この段階で必要な製作物が見えてきますし、会場設営の図面も見えてきます。

実施3週間前

必要なものの洗い出し。当日の司会進行の確認など。

私は音響と照明を担当するため、現地会場を観察しながら、機材設備の数や状態を確認します。

今回はクイズのコンテンツとして、社外の脱出イベントで見るような謎解きゲームを少し盛り込みたいと思っていたので、会場を観察してその題材も探します。

会場である池袋虜は、TORICOが運営する前はクラブとして使われていました。ワンマンオペレーションか出来るよう、ステージ上のDJ卓のまわりにPA・照明設備が配置されています。

池袋虜の図面

この形だと外音を作るのも大変ですし、画面や照明を見ながらのオペレーションができませんので、なんとか配置を直したいところです。

マルチケーブルを会場奥まで取り回して、メイン卓を移動させるのも考えましたか、1日の社内イベントのためだけにやるのは手間がかかりすぎるので低優先とし、他の方法を探します。

池袋虜のPA卓「PRESONUS StudioLive 16.0.2 USB」は、USBで接続したPCでリモートコントロールができます。調光卓も、すでにWindows PC の Surfaceでコントロールできるようになっています。そのため、15mUSBケーブルで、コントロール用のPCだけ客席に配置する方法で行くことにしました。

当日は客席の隅に私のオペレーションブースを作り、PC4台で演出をコントロールすることにします。4台の役割としては

Mac1 スライド (keynote)
Mac2 BGM (iTunes, vlc)
Mac3 PAミキサー
Win1 調光卓

となります。

1PCに複数のアプリを使って台数を減らせますが、オペレーターである私が混乱してミスをする可能性もあるため、1PCに1つのアプリでわかりやすくします。

ディスプレイは、今回は全部で3台使いました。HDMI分配器を使って全てのディスプレイに同じものを映します。

実施2週間前

クイズ問題の調整と確定

配点の計算、想定できるエラーのシミュレーション、集計と伝達方法など詰めます。

デザインクリエイティブもほぼ出来上がっているので、試し刷りをして使用感を確認します。

制作物

作ったものとしては、

  • ネームカード用紙
  • 謎解き用問題用紙
  • ドリンクメニュー
  • 会場地図
  • くじ引き用ネームカード

などを作りました。

ネームカード用紙

名刺カードにプリントしたのですが、フチなしプリントはA4に8面配置のクリアカット用紙がきれいに印刷できて使いやすく、よかったです。

四隅に色がついているのは、謎解きゲームで使うためです。

くじ引き用ネームカード

次回幹事決めのためのくじを作ります。これも名刺カードで作りました。くじ引きは電子化(アプリ化)もできますが、アプリにすると運営が操作しているように見える可能性もあるため、印刷物で作ります。

コストと使いやすさ、当選発表の時の見た目のバランスを考えて、名刺カードで刷ることにしました。自動印刷をしやすくするため、A4面付けの用紙ではなく既に名刺サイズになっている用紙を使います。

HTML + jQuery でページを作って印刷しました。

Github

HTML

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="business-card.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-3.4.1.min.js"
            integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
            crossorigin="anonymous"></script>
</head>
<body>
<script>
const nameSource = `
漫画 全館子
スキマ ネ子
ホーリン ラブ子
まんが 王子
マンガ テンコ
`;
    const names = nameSource.split('\n').filter(i => i);
    $(function () {
        const $body = $('body');
        for (const name of names ){
            const $card = $('<section>').addClass('sheet');
            const $name = $('<div>').addClass('name');
            $name.text(name);
            const $signature = $('<div>').addClass('signature');
            $signature.text('TORICO NIGHT 2019.11');
            $card.append($name);
            $card.append($signature);
            $body.append($card);
        }
    });
</script>
</body>
</html>

SCSS

* {
  box-sizingborder-box;
}
htmlbody {
  margin0;
  padding0;
}
@page {
  size91mm 54.5mm;
  margin0;
}
@media print {
  body {
    width91mm/* needed for Chrome */
  }
}
.sheet {
  width91mm;
  height54.5mm/* 0.5mm余裕をもたせる */
  // page-break-after: always;
  page-break-beforealways;
  positionrelative;
  margin0;
  padding5mm;
  &:nth-of-type(1) {
    page-break-beforeauto;
  }
  .name {
    width100%;
    margin-top15mm;
    height10mm;
    line-height10mm;
    //border: 1px solid black;
    font-size13mm;
    text-aligncenter;
  }
  .signature {
    width100%;
    margin-top10mm;
    height10mm;
    line-height10mm;
    font-size3mm;
    color#777;
    text-aligncenter;
  }
}
/* プレビュー用のスタイル */
@media screen {
  body {
    background#eee;
  }
  .sheet {
    backgroundwhite/* 背景を白く */
    box-shadow0 .5mm 2mm rgba(000.3); /* ドロップシャドウ */
    margin5mm;
  }
}

ドリンクメニュー

てきとうに時間をかけずにささっと。

(ダサい枠線の黒丸はクイズ中の謎と関連させるためです)

実施1週間前

準備は終わっているため、大きな作業は特にありませんでした。
当日の進行やクイズ問題の出題方法の確認を行います。

また、購入物が揃っているかの確認も行っておきます。

ちなみに、Keynote のスライドは、150枚ほどになりました。スライドをしっかり組むほど、当日はリラックスしてオペレーションできます。

当日

準備通りにイベントを進行します。小さなトラブルはいくつかありましたが、リカバリしつつ無事にイベントを終えることができました。

次回運営担当者に向けて

仕事のついでに行うものなので、できる範囲で、リラックスして設計してください。ただし、自社運営会場を使う場合、ケータリングやドリンクの提供方法は、早めに決めたほうが良いでしょう。

スライドを使うのであれば、やっぱり Keynote が使いやすく美しいと思います。

一生懸命がんばっても、参加者に冷たい態度を取られることはあるかもしれません。そういった方を見つけてしまっても、心を動かされずに練習どおり最後まで通し切るのが良いでしょう。それが、他の楽しんでいる方のためになります。

旧Redash(V2) から 新Redash(V8) にアップデートを行いました

Qiita の TORICO Advent Calendar 2019 の記事として書きました

社内での実績閲覧のツールとして Re:dash を使っています。

最初に導入した Re:dash v2 のまましばらくアップデートを行っていなかったので、v8 にアップデートを行いました。

アップデート方針

v8 は dockerコンテナで提供され、 docker-compose で起動させることができます。提供されている docker-compose.yml は、Postgres や redis コンテナの起動も自動でされますし、インストールスクリプトも良くできており、初期状態の ubuntu サーバであればコマンド一発で docker エンジンのインストールも含めアプリの起動まで行うことができます。

従来の EC2イメージの Redash から、スクリプトでアップデートできたら楽なのですが、少し試したところ Postgres の DB のスキーマ名が変わっていたり、Postgresのサーバのバージョンも 9.3 -> 9.6 への大きなアップデートとなることもあり、今回は「Redash v8 を新規EC2で起動し、旧サーバからDBのレコードをコピーする」という方針としました。

Redash V8 サーバの構築

https://github.com/getredash/setup/ こちらのスクリプトを使うことで、非常に簡単に起動させることができます。

1. EC2 で Ubuntu18 サーバを起動

2. ホームディレクトリで、

git clone git@github.com:getredash/setup.git

3.

cd setup
bash setup.sh

あとは、スクリプトの中で docker エンジンのインストール、postgres のデータファイル構築や ランダムなDBパスワードの作成、初期スキーマの構築などすべて行ってくれます。スクリプト終了時に docker-compose up -d まで行うため、ブラウザでアクセスすれば Redash のサインインページが表示されます。

postgres コンテナへの接続

インポートするために、Postgres の Docker コンテナに外部から接続できるようにします。

上記スクリプトで Redash をインストールした場合、/opt/redash/docker-compose.yml が作成されますので、この postgres サービスの箇所に

 postgres:
image: postgres:9.6-alpine
env_file: /opt/redash/env
volumes:
- /opt/redash/postgres-data:/var/lib/postgresql/data
restart: always
# 追加
ports:
- "5432:5432"

ports項目を追加し、ホストマシンの5432ポートをコンテナに接続します。
/opt/redash/env の POSTGRES_PASSWORD に作成されているパスワードと、ユーザー名 postgres でコンテナの postgres に接続できます。

旧サーバの暗号キー等の設定のコピー

旧サーバの、/opt/redash/.env の設定値を新サーバにコピーします。

REDASH_COOKIE_SECRET 

REDASH_COOKIE_SECRET は、DBの data_sources テーブルにある暗号化フィールドの復号化に使われますので、テーブル内容をそのままコピーする場合は一致させる必要があります。

これを一致させない場合、Redash 起動時の data source の選択の際にローディングスピナーがぐるぐる回り続けることになります。APIレスポンス自体はHTTP500が返ってきており、docker-compose のログには Pythonの例外が、暗号化内容を復号できない旨のメッセージと共に出力されます。肝心のトレースバック(スタックトレース)は保存し忘れました。

実際のDBの復号化には settings.DATASOURCE_SECRET_KEY が暗号キーとして使われ、settingsを読んでみると

COOKIE_SECRET = os.environ.get("REDASH_COOKIE_SECRET", "c292a0a3aa32397cdb050e233733900f")
SECRET_KEY = os.environ.get('REDASH_SECRET_KEY', COOKIE_SECRET)
DATASOURCE_SECRET_KEY = os.environ.get('REDASH_SECRET_KEY', SECRET_KEY)

このようになっており、DBの暗号キーとクッキーの暗号キーは個別に設定できるようですが、旧Redashの場合はそれぞれ同じ値を使っていると思いますので、新サーバの /opt/redash/env には 旧サーバの /opt/redash/.env から REDASH_COOKIE_SECRET の内容だけコピーしておきます。REDASH_SECRET_KEY ができていたら項目ごと消しておきます。

その他の設定

Google (Gsuite ) を使っている場合は、

REDASH_GOOGLE_APPS_DOMAIN
REDASH_GOOGLE_CLIENT_ID
REDASH_GOOGLE_CLIENT_SECRET

をコピーすることで新サーバでもGoogle 認証が行えるようになります。そのほか、REDASH_MAIL_* など、旧サーバの設定内容はそのままコピーします。

REDASH_GOOGLE_CLIENT_ID="1xxxxxxxx.apps.googleusercontent.com"

このように、ダブルクォーテーションで囲っているとV8ではうまく動作しませんので、envファイル内でダブルクオーテーションは書かないようにします。

テーブル内容のコピー

おそらく、慣れてる方であれば pg_dump を使うのが良いと思うのですが、私は postgres や pg_dump の経験があまり無いため、datagrip を使ってSELECT 結果から INSERT 文を生成し、それをコピペで流し込むことにしました。

旧Redash からの変更点として、データベーススキーマ名が redash から postgres に変更されたこと、また旧版の queries テーブルや dashboards にあった user_email フィールドが無くなっていることなどあり、pg_dump での対応が難しそうだと思ったためです。

データ量として 700クエリー、30ダッシュボード、60ユーザー程度の規模ですので、コピペで十分対応できました。なお、query_results テーブルに過去のSQL結果が入ってますが、2GB程度のストレージを使っており、不要だと思ったため今回は query_results は移行しませんでした。

旧サーバの postgres に接続し、以下のテーブル内容をコピーしていきます。

organizations
users
data_sources
groups
data_source_groups
queries
visualizations
dashboards
widgets

外部キー制約があるため、流し込む順番が違うと失敗しますが、上記の順番で INSERT できると思います。

queries , dashboards には user_email という不要フィールドがあり、queries にはさらに query_results への参照があるのですが、それらは不要なため下記のようなSELECT 文で datagrip 上で検索し、INSERT 文としてコピーして 新DBに INSERT します。

SELECT id, updated_at, created_at, org_id, data_source_id, name, description, query, query_hash, api_key, user_id, last_modified_by_id, is_archived, options, version, is_draft, schedule_failures, search_vector, tags, schedule
FROM public.queries
ORDER BY id;
SELECT id, updated_at, created_at, org_id, slug, name, user_id, layout, dashboard_filters_enabled, is_archived, version, is_draft, tags
FROM public.dashboards
ORDER BY id;

ついでに、MySQL データソースは RDS用のものが新たに出来ていたため、 data_sources の type フィールドは mysql から rds_mysql 変更しました。

シーケンスの更新

INSERT しただけではオートインクリメントPKに使われるシーケンス番号が1から始まり、新しいクエリを保存する時エラーになってしまうので、シーケンスも更新しておきます。

SELECT * FROM users_id_seq;
SELECT MAX(id) + 1 FROM users;
ALTER SEQUENCE users_id_seq RESTART WITH 61;

SELECT * FROM groups_id_seq;
SELECT MAX(id) + 1 FROM groups;
ALTER SEQUENCE groups_id_seq RESTART WITH 6;

SELECT * FROM data_sources_id_seq;
SELECT MAX(id) + 1 FROM data_sources;
ALTER SEQUENCE data_sources_id_seq RESTART WITH 24;

SELECT * FROM data_source_groups_id_seq;
SELECT MAX(id) + 1 FROM data_source_groups;
ALTER SEQUENCE data_source_groups_id_seq RESTART WITH 22;

SELECT * FROM dashboards_id_seq;
SELECT MAX(id) + 1 FROM dashboards;
ALTER SEQUENCE dashboards_id_seq RESTART WITH 37;

SELECT * FROM queries_id_seq;
SELECT MAX(id) + 1 FROM queries;
ALTER SEQUENCE queries_id_seq RESTART WITH 699;

SELECT * FROM visualizations_id_seq;
SELECT MAX(id) + 1 FROM visualizations;
ALTER SEQUENCE visualizations_id_seq RESTART WITH 1230;

SELECT * FROM widgets_id_seq;
SELECT MAX(id) + 1 FROM widgets;
ALTER SEQUENCE widgets_id_seq RESTART WITH 695;

このように、MAX(id) の結果をコピペしてALTER SEQUENCE していきました。1行で書けそうなのですが、経験が無く書き方がわからなかったため、愚直に書いてます。

これで、新サーバで旧サーバの状態が再現されていると思います。

nginx に SSL証明書を組み込む

自己認証局のSSL証明書がありますので、使います。

参考: dockerでredashのnginxをssl化したら思いの外ハマった

mkdir -p /home/ubuntu/nginx/certs

certs に、証明書 ( my-awesome.crt, my-awesome.key )を入れる 

/home/ubuntu/nginx/default.conf を作成

upstream redash {
server redash:5000;
}

server {
listen 80 default;
return 301 https://$host$request_uri;
}

server {
listen 443;
ssl on;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1.2;
ssl_ciphers "ECDHE+RSAGCM:ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:!EXPORT:!DES:!3DES:!MD5:!DSS";

gzip on;
gzip_types *;
gzip_proxied any;

ssl_certificate /etc/nginx/certs/my-awesome.crt;
ssl_certificate_key /etc/nginx/certs/my-awesome.key;

error_log /var/log/nginx/nginx_error.log;
access_log /var/log/nginx/nginx_access.log;

location / {
proxy_ssl_server_name on;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_pass http://redash;
}
}

/opt/redash/docker-compose.yml を修正

 nginx:
image: redash/nginx:latest
ports:
- "80:80"
- "443:443"
depends_on:
- server
links:
- server:redash
restart: always
volumes:
- /home/ubuntu/nginx/default.conf:/etc/nginx/conf.d/default.conf
- /home/ubuntu/nginx/certs:/etc/nginx/certs

最後に

繰り返しとなりますが、データソース選択ビューで、INSERTしたはずのデータソースが表示されず、ぐるぐるとローディングスピナーが回り続ける場合、旧サーバと settings.DATASOURCE_SECRET_KEY が一致していません。/opt/redash/env の REDASH_COOKIE_SECRET を旧サーバと同じにして、REDASH_SECRET_KEY 設定を消すとデータソースが見れるようになると思います。

CloudFront の Origin を、ボットかどうか(User-Agentによって)判定して切り替える

Webpack でビルドしたWebアプリを S3 にデプロイし、S3 + CloudFront でホストしていました。

ブラウザからのリクエストは問題ないのですが、検索エンジンやTwitterなどのボットからのアクセスの場合、JSを動かせないため内容のほぼ無いページが評価されてしまい、情報の価値がありません。

そのため、Lambda@Edge + CloudFront で、User-Agent を判定し、JSを動かせないようなUAの場合はオリジンをサーバサイドレンダリングするサーバに切り替えるようにしました。

Lambda@Edge とは

Lambda ファンクションを CloudFront のエッジサーバにデプロイし、動作させることができます。

CloudFront で動作させる箇所を4箇所から選ぶことができ、オリジンサーバへリクエストする際に Lambda ファンクションでリクエストヘッダを変更したり、オリジンサーバを変更したりできます。

AWSのページにわかりやすい図があります

ちなみに、Lambda@Edge について以前書いたブログ記事もあります

失敗例

AWSの図を見る限り、Origin Request で Lambda ファンクションを動かし、
リクエストヘッダの User-Agent ヘッダで Origin を変えれば良さそうです。

実際にそのようなコードは書けるのですが、Origin Request の際は User-Agent ヘッダは常に
「Amazon CloudFront」になるため、判定には使えません。

なお、CloudFront のビヘイビアの設定で、ユーザー端末の User-Agent ヘッダをオリジンまで通すことは可能です。
(ビヘイビア→ Cache Based on Selected Request Headers → Whitelist Headers で、User-sAgent を Add Custom)

ただし、これを行うと警告が出ます。

Some headers have a lot of possible values, and caching based on the values in these headers would cause CloudFront to forward more requests to your origin. We recommend that you do not cache based on the following headers:User-Agent

この警告にある通り、キャッシュの効率が悪くなるので、よほどの理由がない限りやらないほうが良いでしょう。

方針

このような方針にしました。

1. Viewer Request に入ってきた時点で、Lambda@Edge で User-Agent ヘッダを判定。
ホワイトリストで評価し、もしJSを扱えないようなUA(ボット等)の場合、
bot-user-request=1 のリクエストヘッダを付与する。

2. CloudFront のビヘイビアで、bot-user-request のHTTPヘッダは通す (Cache Based on Selected Request Headers にする)

3. Origin Request の時点で、Lambda@Edge で bot-user-request ヘッダがあれば、オリジンのドメインを変更

コード

Viewer Request

'use strict';

/*
Cloudfront の Viewer Request 用のファンクション。

Google Bot からのアクセスの場合、Httpヘッダの bot-user-agent = "1" を設定する。
この後、CloudFront のビヘイビアで bot-user-agent のヘッダーを通し、
さらに Origin Request のLambda で、bot-user-agent を判断し、
Originを切り替える
*/

const bots = [
'Twitterbot',
'facebookexternalhit',
'compatible; Google',
'Googlebot',
];


exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;

const isBot = bots.some(v => {
return request.headers['user-agent'][0].value.includes(v)
});
if (isBot) {
request.headers['bot-user-agent'] = [
{
'key': 'bot-user-agent',
'value': '1'
}
];
}

callback(null, request);
};

Viewer Response

'use strict';

/*
bot-user-agent の HTTPリクエストヘッダがあったら、
オリジンを書き換える。

*/
const newDomainName = 'new.example.com';

exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;

if ('bot-user-agent' in request.headers) {
if ('custom' in request.origin ) {
request.origin.custom.domainName = newDomainName;
request.headers['host'] = [{ key: 'host', value: newDomainName}];
// HTTPSでリクエストする場合
request.origin.custom.port = 443;
request.origin.custom.protocol = 'https';
}
}

callback(null, request);
};

AWS LambdaをCloudFrontエッジサーバで動かす (Lambda@Edge)

AWSの Lambda ファンクションを、CloudFrontのエッジサーバにデプロイして、リクエストやレスポンスをインターセプトして処理を行うことができます。Lambda@Edge と呼ばれます。

Webアプリケーションフレームワークにある「ミドルウェア」のような使用感で使えます。

例えば、S3 をオリジンに使っている時(サーバレス環境などで)、少しだけ動的に認証やリダイレクトなどのHTTPヘッダーを扱う処理をしたい時など、便利です。

ポイントとして、us-east-1 (バージニア北部) リージョンの Lambdaファンクションのみ選択できます。

AWSの公式ドキュメント

1. 概要

1-1. フックポイント(トリガー)

Amazonのページにわかりやすい絵があります

リクエスト:

クライアント → [Viewer Request]→ CloudFront → [Origin Request] → Origin

レスポンス:

クライアント ← [Viewer Response] ← CloudFront ← [Origin Response] ← Origin

Viewer Request, Origin Request, Origin Response, Viewer Response の4箇所で処理を差し込めます。

今回は、例として Origin Response にファンクションを差し込んでみます。

1-2. Lambdaファンクションのパターン

公式ドキュメントにサンプル集があります(便利)。

今回は、Origin Response で処理をさせるため、引数にはレスポンスが入っており、処理としてはレスポンスを返す( callback(null, response) を実行する) 必要があります。

リクエストとレスポンスを取得するファンクションの定番パターンは

'use strict';

exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const response = event.Records[0].cf.response;
// なにか処理...
callback(null, response);
}

このようになります。responseを加工して返すか、新しいレスポンスデータを作って返すことになるでしょう。

1-3. eventの中身をすべてJson で表示するファンクション

最初は、event の中身をすべて見れるようなファンクションを作ると動作を早く理解できます。このようになります。

'use strict';

exports.handler = (event, context, callback) => {
const response = event.Records[0].cf.response;
response.status = 200;
response.statusDescription = 'Ok';
response.headers['content-type'] = [{
"key": "Content-Type",
"value": "application/json",
}];
response.body = JSON.stringify(event, null, ' ');
callback(null, response);
}

2. 実際に作ってみる

2-1. CloudFront ディストリビューションの作成

2-1-1. CloudFront のページ  から、 Create Distribution

2-1-2. Web の Get Started

2-1-3. Lambda Function Associations は、ここでは設定しない。(あとで Lambdaのページから設定する)

オリジンは適当なS3にでもしておいてください。

キャッシュ時間は、すべて0にしてキャッシュさせないようにすると検証が楽です。

Create Distribution で作成。

2-2. Lambda 関数の作成

2-2-1. us-east リージョンの Lambdaページから「関数の作成

2-2-2. ロールは、「カスタムロールの作成」

2-2-3. ロール作成ページが表示されるので、ロール名をつけて許可

2-2-4. このロールに、EdgeLambdaの「信頼関係」(サービスプリンシパル) が必要なので、一旦 IAM のロールの「信頼関係」タブを見る

2-2-5. 信頼関係の編集 をクリック

2-2-6. Principal をリストにして、"edgelambda.amazonaws.com" を追加して、ポリシーの更新。

"Service": [
"edgelambda.amazonaws.com",
"lambda.amazonaws.com"
]

2-2-7. 信頼されたエンティティに edgelambda.amazonaws.com が追加されている。

2-2-8. Lambda のページに戻ると、「既存のロール」に先程のロールが設定されるので、「関数の作成」をクリック。

2-2-5. 関数作成のウインドウが出るので、関数を書いて、「保存」。

'use strict';

exports.handler = (event, context, callback) => {
const response = event.Records[0].cf.response;
response.status = 200;
response.statusDescription = 'Ok';
response.headers['content-type'] = [{
"key": "Content-Type",
"value": "application/json",
}];
response.body = JSON.stringify(event, null, ' ');
callback(null, response);
}

2-2-6 「テスト」ボタンをクリック

2-2-7. このテストページでは、ファンクションの第一引数を設定してテストできる。

今回のLamda関数は、第一引数 event が何であってもオウム返しするだけなのでどんなデータでもテストできるが、Amaon CloudFront Access Request In Response が実際の Origin Response の event に近いので、これを使ってテストイベントを作る。

2-2-8. 「テスト」をクリックしてテストを実行。

2-3. CloudFront にデプロイ

作ったファンクションを CloudFront にデプロイする。

2-3-1. Lambda の Designer から、CloudFront を選択

2-3-2. 「グローバル展開が保留中」と表示されるので、下にある トリガーの設定で、「Lambda@Edgeへのデプロイ」をクリック

2-3-4. デプロイページで、「オリジンレスポンス」を選択し、確認チェックボックスを入れて「デプロイ」

デプロイされる。

2-4. テストリクエスト

curlかなんかでリクエスト。eventの内容がわかる。

% curl -v 'http://xxxxxxxxxxxxxx.cloudfront.net/'

* Trying ****:****:****:****:****:****:****:****...
* TCP_NODELAY set
* Connected to xxxxxxxxxxxxxx.cloudfront.net (****:****:****:****:****:****:****:****) port 80 (#0)
> GET / HTTP/1.1
> Host: xxxxxxxxxxxxxx.cloudfront.net
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 2663
< Connection: keep-alive
< x-amz-bucket-region: ap-northeast-1
< Date: Sat, 19 Jan 2019 13:30:09 GMT
< Server: AmazonS3
< X-Cache: Error from cloudfront
< Via: 1.1 xxxxxxxxxxxxxx.cloudfront.net (CloudFront)
< X-Amz-Cf-Id: xxxxxxxxxxxxxx
<
{
"Records": [
{
"cf": {
"config": {
"distributionDomainName": "xxxxxxxxxxxxxx.cloudfront.net",
"distributionId": "xxxxxxxxxxxxxx",
"eventType": "origin-response"
},
"request": {
"clientIp": "****:****:****:****:****:****:****:****",
...
...

この出力は、今回の Lambda 関数の第一引数そのものなので、Lambda のテストデータとしてコピペしておくと今後のテストに便利です。

2-5. 修正後のデプロイ

アクションメニューに「Lambda@Edge へのデプロイ」項目が追加されるのでここからデプロイすると楽です。

AWS CloudFrontにACMの独自SSL証明書を適用する

CloudFront には、AWS ロードバランサー (ELB,ALB) と同じように ACM (Certificate Manager) で無料で作成したSSL証明書を適用することができます。

ポイントは、us-east-1 (バージニア北部) リージョンのACM で作った証明書のみ選択できるというところです。

1. ACMでSSL証明書を作る

1-1. us-east-1 の ACM へ行く https://console.aws.amazon.com/acm/home?region=us-east-1#/

1-2. 証明書のリクエスト をクリック

1-3. パブリック証明書のリクエスト → 証明書のリクエスト

1-4. ドメイン名を入力。「この証明書に別の名前を追加」すると、SSL証明書の SAN (Subject Alternative Name ) になる。

1-5. 検証方法はお好みで。DNSだと、使用後にレコードを消さないと後々レコードを見つけた時に混乱の原因になるので、削除するひと手間が多くかかる。Eメールの方が好み。

検証が終われば、SSL証明書が使えるようになります。

2. CloudFront に適用する

2-1. CloudFrontの、Distribution Settings を開く

2-2. Alternate Domain Names に、使うドメインを書く

2-3. SSL Certificate で Custom SSL Certificate (example.com): を選ぶと、us-east-1 (バージニア北部) リージョンのACMで作った証明書の一覧が出てくる。
先程作ったSSL証明書を選択。

これで 独自ドメインのSSL証明書が CloudFront で使えます。

あとは、DNSで、使うドメインのCNAME を CloudFront のドメイン (xxxxxxxxxxx.cloudfront.net) に向ければ、独自ドメインのHTTPリクエストがSSL通信でこの CloudFront を経由するようになります。

Bootstrapでスマートフォン/PC両対応ページを手軽にレイアウトする 第1会 スペーシングユーティリティ

社内勉強会で行った内容の復習です。

※ サンプルドキュメントの内容はダミーです

はじめに

TORICOでは、ウェブサービスのCSSフレームワークとしてBootstrap4を多く採用しています。

ユーティリティクラス(margin,padding,文字サイズなど)、プリセットデザイン(card,buttonなど)、グリッドレイアウトが必要十分なだけ収まっており、容量と使う機能のバランスがちょうどよいためです。

漫画全巻ドットコムなどのサービスの特集ページもBootstrap4が適用されており、
非デザイナーでも十分な自由度を保ったレイアウトを少ないコード量で書くことができます。
今回は、非デザイナー向けに、簡単にBootstrapの使い方を教えます。

エディタアプリの準備

テキストエディタを使います。好みのテキストエディタが特になければ、Microsoft の Visual Studio Code (以下VSCode) をインストールしてください。Windows でも Mac でも、無料で使えます。

Visual Studio Code

その他では、Atom(Github), ブラケッツ(Adobe), サクラエディタnotepad++  あたりが無料で使えて使いやすいと思います。

サンプルドキュメント

今回は、サンプルとしてこのような文章を使います。

新年会のお知らせ

◆ 幹事
漫画全巻子 @zenkanko

◆ 日時
1月20日

◆ 会費
4,000円

◆ 場所
新宿西口 ○○家

◆ 参加方法
Slackの #新年会2019 に参加し、参加の意思がある旨発言してください。

◆ キャンセル規定
3日前以降のキャンセルは会費をもらいます。注意してください。

ドキュメントの構造を把握する

このドキュメントの構造としては、
「新年会のお知らせ」がドキュメント全体のタイトル、「幹事」「日時」がその子セクションの見出しとなっています。

ドキュメントの構造をマークアップする

このドキュメントの構造を、コンピュータがわかる形で意味付けしていく作業を「マークアップ」と呼びます。マークアップのために「HTML」という言語が使われます。
それでは、HTMLを使ってこのドキュメントをマークアップします。

ドキュメント全体のタイトルを h1 タグ、小セクションの見出しを h2 タグ、その中の文章を p タグでマークアップすると

<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h1>新年会のお知らせ</h1>

<h2>幹事</h2>
<p>漫画全巻子 @zenkanko</p>

<h2>日時</h2>
<p>1月20日</p>

<h2>会費</h2>
<p>4,000円</p>

<h2>場所</h2>
<p>新宿西口 ○○家</p>

<h2>参加方法</h2>
<p>Slackの #新年会2019 に参加し、参加の意思がある旨発言してください。</p>

<h2>キャンセル規定</h2>
<p>3日前以降のキャンセルは会費をもらいます。注意してください。</p>
</body>
</html>

こうなります。

本来は、h1,h2,pタグの他に、sectionタグなんかもほしい所ですが、今回は省略します。

試しに、この文章をテキストエディタ(VSCode等)で書いて new-year-party.html というファイル名で保存してください。文字コードは UTF-8 で保存してください。

文字コードはVSCode は右下に表示されています。デフォルトで UTF-8 になってると思います。改行コードは、Windowsの場合 CRLF となってると思いますが、LFにしておいたほうが無難です。

保存した new-year-party.html をダブルクリックするか、もしくはブラウザのアプリアイコンにドラッグアンドドロップして開くと、ページが表示されます。

もし、「このファイルを開く方法を選んでください」というウインドウが出たら、Google Chrome がインストールされていたら Google Chrome で、なければ Microsoft Edge を選んでください。

ブラウザは、マークアップの内容を把握し、h1 を大きな文字で、h2 を少し大きな文字で、
p を小さな文字でページに表示してくれます。

Bootstrap の導入

ドキュメントのマークアップが終わったので、見た目をきれいにするため Bootstrap を導入します。

Bootstrapの導入ページ を見ると、CDN(ファイル配信サーバー)を使う方法が書いてありますので、その通りにします。

<head>〜</head> の中に、

<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">

これをコピペします。これで終わりです。

1行目の meta name="viewport" タグは、スマートフォンでHTMLを見た時に文字サイズが小さくならないようにする指定です。(bootstrapと直接関係はありません)

2行目のlink タグで bootstrap を読み込んでいます。

ファイルを保存して、ブラウザをリロードすると、表示が少し変化します。
例えばフォントが変更され、大見出しの「新年会のお知らせ」が、ブラウザの左上に余白がほとんどなく配置されます。

レイアウト調整

次に、Bootstrapの スペーシングユーティリティを使って余白を整えていきます。
このスペーシングユーティリティは美しい見た目を作る上で一番重要といえます。

ドキュメントはここです。

例えば、各HTMLタグに、class="my-4" とか class="px-3" とか class="ml-2" のように class 属性を書くことで、余白量を調整することができます。

my-4 は、margin Y(天地)方向 サイズ4を設定
px-3 は、padding X(左右)方向 サイズ3を設定
ml-2 は、margin L(左)方向 サイズ2を指定

という指定になります。詳しくは、ドキュメントを徹底的に読んでください。

外枠をつける

将来的にスマートフォンでもPCでも美しく表示できるよう、ドキュメント全体を1つの div タグで囲って、スペーシングユーティリティで余白を作っておきます。

ついでに、カラーユーティリティを使って簡単に色をつけます。

全体的なコードはこうなります。

<html>

<head>
<meta charset="utf-8">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
crossorigin="anonymous">
</head>

<body class="bg-secondary p-2">
<div class="bg-white p-2 shadow-sm">
<h1>新年会のお知らせ</h1>

<h2>幹事</h2>
<p>漫画全巻子 @zenkanko</p>

<h2>日時</h2>
<p>1月20日</p>

<h2>会費</h2>
<p>4,000円</p>

<h2>場所</h2>
<p>新宿西口 ○○家</p>

<h2>参加方法</h2>
<p>Slackの #新年会2019 に参加し、参加の意思がある旨発言してください。</p>

<h2>キャンセル規定</h2>
<p>3日前以降のキャンセルは会費をもらいます。注意してください。</p>
</div>
</body>

</html>

※ VSCode では、Shift+Alt+F でコードの自動整形ができます。何かを書いたらこのショートカットキーで整形すると見やすくなります。

スマートフォンサイズで見るとこのような形になります。

各要素の余白を調整したいので、スペーシングユーティリティで調整します。
ついでに、小見出しが少し大きすぎるのと思ったので、タイポグラフィのクラスを使って調整します。

h1 に、class="my-5" をつけ、
h2 に、class="h4 mt-4" をつけました。

このようになります。

<html>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
crossorigin="anonymous">
</head>

<body class="bg-secondary p-2">
<div class="bg-white p-2 shadow-sm">
<h1 class="my-5">新年会のお知らせ</h1>

<h2 class="h4 mt-4">幹事</h2>
<p>漫画全巻子 @zenkanko</p>

<h2 class="h4 mt-4">日時</h2>
<p>1月20日</p>

<h2 class="h4 mt-4">会費</h2>
<p>4,000円</p>

<h2 class="h4 mt-4">場所</h2>
<p>新宿西口 ○○家</p>

<h2 class="h4 mt-4">参加方法</h2>
<p>Slackの #新年会2019 に参加し、参加の意思がある旨発言してください。</p>

<h2 class="h4 mt-4">キャンセル規定</h2>
<p>3日前以降のキャンセルは会費をもらいます。注意してください。</p>
</div>
</body>

</html>

スマートフォンサイズで見るとこのようになります。

これで読みやすくなりましたが、このHTMLをPCサイズで見ると、
もっと左右の余白がほしいところです。

レスポンシブブレイクポイント

続いて、Bootstrapの持つレスポンシブデザイン機能を使い、PCサイズで見たときにもっと多くの余白を取るようにします。

レスポンシブデザインの際、PCかスマートフォンかの判断は、ブラウザの横幅で判断します。というより、実際はPCとスマートフォンを区別しているわけではありません。横幅の大きなブラウザでも小さなブラウザでも破綻なく要素がきれいに表示されるようにレイアウトを行います。

ブラウザの幅を変えていったとき、レイアウトが大きく切り替わるポイントがあります。それを「レスポンシブブレイクポイント」(以下ブレイクポイント)と呼びます。

Bootstrap4 のドキュメントを見てみると、Bootstrap4では 576px, 768px, 992px, 1200px にブレイクポイントが設定されていることがわかります。

これらすべてのポイントについて細かく考慮する必要はありません。必要に応じて、「あるブレイクポイントを超えるブラウザサイズの場合、レイアウトを変更する」ようにHTMLタグのクラスをつけていきます。

今回は、幅768px を超えるブラウザ(PCでの閲覧を想定) の際に、左右の余白を多くとることにします。

幅768px を超えるサイズのことを、Bootstrap では medium デバイス (略して md )と呼んでいます。

先程のスペーシングユーティリティの説明を読むと、デバイスサイズに応じて余白の量を変えられることがわかりますので、

<body class="bg-secondary p-2">
<div class="bg-white p-2 shadow-sm">

この箇所について、スマートフォンでは 2のサイズのパディングをとっていましたが、medium以上のサイズのデバイス(つまりタブレットやPC)では、5サイズで取るように修正します。

p-md-5 を追記します。

<html>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
crossorigin="anonymous">
</head>

<body class="bg-secondary p-2 p-md-5">
<div class="bg-white p-2 p-md-5 shadow-sm">
<h1 class="my-5">新年会のお知らせ</h1>

<h2 class="h4 mt-4">幹事</h2>
<p>漫画全巻子 @zenkanko</p>

<h2 class="h4 mt-4">日時</h2>
<p>1月20日</p>

<h2 class="h4 mt-4">会費</h2>
<p>4,000円</p>

<h2 class="h4 mt-4">場所</h2>
<p>新宿西口 ○○家</p>

<h2 class="h4 mt-4">参加方法</h2>
<p>Slackの #新年会2019 に参加し、参加の意思がある旨発言してください。</p>

<h2 class="h4 mt-4">キャンセル規定</h2>
<p>3日前以降のキャンセルは会費をもらいます。注意してください。</p>
</div>
</body>

</html>

この指定で、PCサイズのブラウザで見た時にも十分な余白がとれるようになります。

margin と padding

余白について、padding(p) と margin(m) という2つの指定方法がありました。

padding (p) は指定要素の内側margin(m) は指定要素の外側の余白を指定します。

marginの相殺

HTMLのスタイルの仕様で、注意しなければいけない仕様に「天地方向のマージンの相殺」というものがあります。

例えば

<h2 class="my-4">小見出し</h2>
<p class="my-3">本文</p>

というHTMLがあった場合、小見出しと本文の間の余白は 4+3 のサイズとはなりません。大きい方の4のサイズとなります。

これがマージンの相殺という仕様で、HTMLをレイアウトする際には避けて通れない、重要な仕様です。必ず覚えてください。マージンの相殺は天地方向のみ発生し、左右方向では発生しません。

相殺したくない場合はいくつか回避方法があります。

  • overflow: hidden を使う
  • padding を使う
  • border を使う
  • hr を挟む
  • etc

検証コード

<html>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
crossorigin="anonymous">
</head>

<body class="bg-secondary p-3">
<div class="bg-white p-2 my-4 shadow-sm">
<h2 class="mb-5">見出し</h2>
<div>
<p class="mt-5">↑この間のmarginが相殺されている</p>
</div>
</div>
<div class="bg-white p-2 my-4 shadow-sm">
<h2 class="mb-5">見出し</h2>
<div style="overflow: hidden;">
<p class="mt-5">overflow:hidden で margin 相殺を打ち消し</p>
</div>
</div>
<div class="bg-white p-2 my-4 shadow-sm">
<h2 class="mb-5">見出し</h2>
<div style="border-top:1px solid transparent">
<p class="mt-5">border で margin 相殺を打ち消し</p>
</div>
</div>
<div class="bg-white p-2 my-4 shadow-sm">
<h2 class="mb-5">見出し</h2>
<div class="pt-1">
<p class="mt-5">padding で margin相殺を打ち消し</p>
</div>
</div>
<div class="bg-white p-2 my-4 shadow-sm">
<h2 class="mb-5">見出し</h2>
<hr class="m-0">
<p class="mt-5">hr で margin 相殺を打ち消し</p>
</div>

</body>

</html>

詳細については、ブラウザのデバッグツール (Windowsでは Shift + Ctrl + I) のインスペクタを使って、動作を確認してください。

余計な要素を追加せずに行えるのが overflow: hidden を指定する方法です。

今回のように、見出しや本文を上から下に配置していくレイアウトの場合、枠の左右の余白を(親要素の)padding で作り、その中の見出しや本文の天地余白をmargin で作っていくとうまくまとまります。

個人的には、「要素の上だけにmargin を指定する」「要素の下だけにmaginを指定する」といった指定方法より、「天地にmarginを指定して積極的に相殺させる」ほうが、前後の関係を気にする必要がなく早く書いていけるように思います。

おさらい

  • Bootstrapを使うと、スマートフォンやPCなどデバイスのブラウザサイズが変わっても破綻なくレイアウトをすることができる(レスポンシブデザイン)。
  • レイアウトの切り替わるブラウザ幅を「レスポンシブブレイクポイント」と呼ぶ
  • Bootstrapの導入は1行の link タグのコピペだけで行える。簡単。
  • スペーシングユーティリティ重要。
  • 天地方向のマージンは相殺される。

次回は、レスポンシブグリッドを使ったレイアウトの段組みを行います。

仕事をする上でいつも思っている言葉

弊社TORICO にインターンとして大木さんというエンジニアが参加されています。ブログをお任せしたところ、職場や働き方についても書かれていました。今回はそれに刺激を受け、私も職に対しての考えを書こうと考えました。今回は、私が仕事をする上でいつも思っている言葉、そして私を変えた言葉をいくつか紹介したいと思います。

TORICOインターンブログ

時間とは、可能性だ

Time is ナントカ。パターンは多くあると思うのですが、私は「可能性」だと思っています。Time is possibility. 自分で思いついた言葉ですが、世界中で同じことを考えている方は多くいるんじゃないかと思っています。

人、特に日本で生まれた人は、手厚い人権に守られているため自由に学校や仕事を選ぶことができます。「何にでもなれる」と言っても大げさではないでしょう。ただし、何にでもなれる可能性は時間が経つごとに減っていきます。ポジションには限りがあり、競争があるためです。

人が時間を使うということは、何かを達成する可能性を取捨選択するということです。時間には、すべての人に平等な限界があります。それを何に使うかで、やりたいことをできる可能性は変わってきます。

日本で生まれて、やりたいことを選ぶ自由がある以上、好きなことをやればいいと本当に思います。そして、何かを選択すれば、それは何かができる可能性として自分の中に蓄積していきます。逆に選ばなかった可能性は捨てることになり、永遠に失われます。時間は巻き戻せないためです。時間は、自分のやりたいことを実現する可能性を高めるために使うものである、と私は考えます。

文句を言う前に頭を使え

これも私が思いついた言葉ですが、いつも忘れずに行動しています。ちなみに言葉で人に伝えたことはありません。パワハラだと言われそうなので。

何か複雑な問題があった時、それが原因でストレスを負った時、文句・愚痴を言ったり言い訳をしたくなることがあります。その言い訳の先にあるのは自分のストレスの解消や虚栄心の確保です。聞かされる人にとってはただただ時間の無駄で、人によってはそれがストレスの原因になりえます。家に帰ったら人形にでも向かって心が穏やかになるまで愚痴を言うのがいいでしょう。

求められることは、課題を効果的に解決できるまで知恵を絞り続けることです。場合によっては「解決できない理由を説明する」だったり「課題を凍結する(先送りにする)」選択もありえると思いますが、文句を言うことはゴールに近づくことにはならず、ただの時間の無駄です。社会人であれば、頭を使って問題に立ち向かいましょう。

人のえらさは平等

社会人になってすぐ、先輩から教わったことです。えらさ、はいろんな意味にとれる言葉ですが、ここでは「人より権利が優れている」という意味で、王国でいう王様のようなえらさです。

日本人である限り、他の日本人よりえらいということは決してありません。すべての人が法のもと平等であると憲法で定められています。

では、会社などの組織に所属した時のヒエラルキーは何なのかというと、ただの雇用契約上の役割や権限の違いでしかありません。アルバイトでも社長でもお客様でも、総理大臣でも、お互いに成人であればえらさの違いはなく、雇用契約であったり売買契約であったり、契約上の権限や役割の違いがあるだけです。組織内での権限の違いを組織内での「えらさ」と表現するのもわからなくはありませんが、「えらさ」という言葉にすると人権的に差があるようにも感じられるかもしれないため、私は言わないようにしています。

会社組織でいうなら、上司は部下に対して人権に優位性があるわけではなく、「評価して賃金を決めることができる権利を組織内で与えられている」程度の違いしかありません。(持ってる株式数に大きな違いが無い場合)。なので、同じ社会人でありプロフェッショナル同士である同僚・部下とは礼儀を持って接するのが大切なことです。彼らが隣の席にいるのは、「自分と同じ会社と雇用契約を結んだ」ただそれだけの理由です。

これからどうするか

これは、アドラー心理学の解説書「幸せになる勇気」から学んだ言葉です。

この本は、カウンセリングをする時、多くは「悪いあいつ」の批判や、「かわいそうなわたし」を訴えるだけで終わってしまう。それは本質の解決にはつながらず、聞き流すだけの内容だと、伝えます。

たしかに愚痴や悪口にはなんの意味も価値もありません。家に帰ったら人形にでも向かって言ってればいいでしょう。大事な話題は「これからどうするか」であり、人と会話をする上で見失ってはいけません。

嫌われる勇気幸せになる勇気

両方人気のある本ですが、内容は面白く、私の行動を形作る要因となっています。おすすめします。

そのほか、この場を使って読んで良かった思う良本を紹介しておきます。

あなたの知らない脳

哲学的な何か、あと科学とか

「これは自分の仕事ではない」と言わない

何かの本か記事で読んだのですが、出展を覚えていません。おそらく Amazon とか AWS の組織について書かれた本だったと思うのですが…。

「これは自分の仕事ではない」と言う前にすべきことは、知恵と勇気をふりしぼって、そしてなりふりかまわず課題の解決に向き合うことです。

知恵をうまく使うことで、具体的には既存のツールや手順をうまく組み合わせることで課題の解決は大きく効率化させることができます。

逆に知恵の範囲が狭ければ、人海戦術と根気での解決策しか考えつかないかもしれません。もしその時、「面倒だ」「もっと効率的にできる方法がありそうだ」と感じれば、効率化する方法は間違いなくあります。知恵と勇気をふりしぼって、対応することで、個人や組織の成長につながるでしょう。

ダメじゃなければOK

私の考えた言葉です。デザインワークをする際は、これを思いながら作業をします。

私は自分で行ったデザインを見直す際、ダメな箇所があるかどうかを注意して確認します。良くできた箇所はまったく気にしません。無視します。ダメな所だけを探します。

ダメな箇所に気づいたら、無くなるまで直します。「○○さんに見せたらここ何か言われるかなー」と感じたら、それはダメな箇所であり、直す必要があります。先鋭的な表現とかメッセージ性とかはあってもなくてもどっちでもいいです。ただし、そもそものコンセプトとして「なければダメ」なら無きゃダメです。ダメな所がみつからなければ、そのデザインはひとまずは「OK」です。

納期までにでっちあげる

マンガ「げんしけん」で、コミケの出版物の納期がせまっておりもう無理だと半ばあきらめかけているチームに向かって登場人物が言った言葉です。

この「でっちあげる」という表現が好きで、納期を最優先とし、あきらめずに手段を選ばず達成する意志を感じます。

本来であれば、クオリティを十分に保って、かつ納期(約束)を守るのが一番良いのですが、両立が難しい場合でもあきらめずに、何としてでも約束は守らなければならないと、この作品は伝えています。

げんしけん、新装版出てた

しっかり練習してこい

スーパーストリートファイター4で、ケンが勝利時にいう言葉。つまり、ケンに負けた時に言われる言葉。その言葉はビデオゲームの範囲を超えて、プレイヤーにも向けられているようです。

ビデオゲームに限らず、どんなゲーム(スポーツ)であれ、勝利するために挑戦するわけですが、勝つ可能性を上げるためには練習と研究が重要です。あとは事前準備とか体調管理とかでしょうか。当たり前のことなのですが、たまに忘れかけてしまうこともあります。

例えば、たまたま1ゲームだけうまくいった時、次も同じようにやればうまくいくと思ってしまうことがありますが、そんなことはありません。それは自己評価が高すぎます。勝ったのはたまたまであり、慢心すると足元を掬われるということを「勝って兜の緒を締めよ」ということわざが思い出させてくれます。

「勝った理由は運が良かったから、負けた理由は実力不足」という言葉もあります。もし、勝ちたくてやることであれば、「しっかり練習してこい」と言われないよう練習しましょう。時間を練習に使えば、勝つ可能性を上げることができます。

逆に、練習に時間を使えないのであれば、そのゲームでは勝つことをあきらめているということになります。

Django に Celery タスクキューを導入し、遅い処理を利用者に体感させないようにする

Django でウェブアプリを作る際、遅い処理をタスクキューにするには、celery が便利です。今回、社内勉強会で Django + celery のチュートリアルを行ったので、celery で簡単なタスクを動かすまでを書いておきます。

内容としては Celery ドキュメントの First steps with Django をなぞっています。

環境

  • MacOS
  • Python 3.6.5
  • Django 2.1
  • Celery 4.2.1
  • Redis

Redisは、キューのブローカーとして使います。Redis以外にも、RabbitMQ やAmazon SQS が使えます。

Redis サーバの起動方法は書いていませんので、適宜起動してください。

プロジェクトフォルダの作成

$ mkdir celery_handson
$ cd celery_handson

venv (仮想環境) の作成

$ python3.6 -m venv venv
$ . venv/bin/activate

ライブラリのインストール

$ pip install django
$ pip install celery django-celery-results redis django-redis
$ pip install ipython

django-admin.py を認識させるため、仮想環境に入り直しておきます。

$ deactivate
$ . venv/bin/activate

Djangoプロジェクトの作成

(今、celery_handson ディレクトリにいますが、さらにその中に celery_handson という名前の Django プロジェクトを作ります)

$ django-admin.py startproject celery_handson
$ cd celery_handson

Django_celery_results の導入

settings.py を編集し、INSTALLED_APPS に django_celery_results を追加します。

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_celery_results', # ←追加
]

DBの作成

$ ./manage.py migrate

これで、Djangoの初期設定は完了しました。

試しに、

$ ./manage.py runserver

を起動し、http://127.0.0.1:8000/ にアクセスしてみると、テストページが表示されます。

Celery 設定の追加

settings.py に追加します。

CELERY_BROKER_URL = "redis://<Redisサーバのホスト>:6379/1"
CELERY_RESULT_BACKEND = "django-db"

Celery ファイルの作成

celery_handson/celery.py を作成

urls.py の並びに、celery.py を作ります。

# celery_handson/celery_handson/celery.py
import os
from celery import Celery

# set the default Django settings module for the 'celery' program.
settings = os.getenv(
"DJANGO_SETTINGS_MODULE", "celery_handson.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings)

app = Celery('celery_handson')

# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')

# Load task modules from all registered Django app configs.
app.autodiscover_tasks(['celery_handson'])

公式ドキュメントにあるファイルほぼそのままです。

タスクスクリプトの作成

urls.py, celery.py の並びに、tasks ディレクトリを作り、その中に __init__.py を作ります。

# celery_handson/celery_handson/tasks/__init__.py
from ..celery import app


@app.task()
def add_numbers(a, b):
print('Request: {}'.format(a + b))
return a + b

celery ワーカーの起動

./manage.py があるディレクトリで行います。

celery -A celery_handson worker --concurrency=1

--concurrency は並列性のオプションで、省略しても問題ありませんが開発中は1が扱いやすいように思います。

ターミナル内で実行し続けますので、そのターミナルはそのままにして、次の「タスクの実行」からは新しいターミナルを起動してください。

(ちなみに celery 実行バイナリは、venv/bin/celery にいます。)

(iTerm2 を使っている場合、ターミナルに画像が表示されます)

タスクの実行

同期処理

$ ./manage.py shell
>>> from celery_handson.tasks import add_numbers
>>> add_numbers(3, 4)
Request: 7
7

普通に同プロセス内で add_numbers をコールしただけです。これは celery は関係ありません。

非同期処理

>>> add_numbers.delay(5, 6)
<AsyncResult: deeddee2-74a6-48ec-9f22-987417135592>

@app.task() デコレータがついた関数は Celery のタスクとして登録され、.delay メソッドが使えるようになっています。.delay をコールすることで、非同期実行のタスクキューとしてブローカーに登録され、ワーカーはそれを拾って処理を行います。

.delay メソッドの戻り値である AsyncResult は、result プロパティの中に結果が入っているかもしれません。

また、django_celery_results を使っていれば処理結果は DB (モデル) に記録され、task_id の値を元に取得することができます。

>>> tr = TaskResult.objects.get(task_id='2781413b-282e-4de8-b134-b338b592ad02')
>>> tr.status
'SUCCESS'
>>> tr.result
'11'

ビュー内で行う重い処理や、マネジメントコマンドから実行するタスクで並列したいものは、このタスクの delay メソッドを使うことで簡単に非同期化でき、使用者のストレスを下げることができます。

PyCharm の設定

タスクの開発は、他のスクリプトと同様にIDEでブレイクポイントを設定すると効率的に開発できます。

Django の環境設定

一番上の celery_handson ディレクトリを PyCharm で開きます。

Project Interpreter

⌘+, で設定を開き、検索窓に interpreter と入力。

Project Interpreter を、先程作った venv 内のPython を指定します。最近の PyCharm は自動認識します。

Django Support

検索窓に django と入力し、Languages & Frameworks ➝ Django の設定をします。Enable Django Support にチェックを入れ、Settings に celery_handson/settings.py を指定。

Project Structure

検索窓に structure と入力し、./manage.py が入っている celery_handson ディレクトリを Sources に追加しておきます。

Run/Debug Configurations の登録

エディタ右上の、Edit Configurations... を選択し、+ ボタンから Django Server を選択 して設定を登録します。

Enironment variables (環境変数) に、DJANGO_SETTINGS_MODULE celery_handson.settings を登録しておきます。(無くても動くかもしれません。PyCharm の他の設定によります。設定しとくと無難です)

この設定を実行すると、Django テストサーバが起動します。

Celery用の設定

Run/Debug Configurations の + をクリック ➝ Python で、

Script path に venv/bin/celery、parameters に -A celery_handson worker --concurrency=1 を、念の為 Environment variables: に DJANGO_SETTINGS_MODULE celery_handson.settings を登録しておきます。

この環境をデバッグ起動すれば、ワーカープロセス内でブレイクポイントが使えるようになり、開発がしやすくなります。

Celery ワーカーの状態取得

from celery.task import control
inspect = control.inspect()
inspect.stats()  # ステータスを取得
inspect.ping()  # 疎通確認
inspect.active()  # 現在実行中のタスク

Supervisor の設定サンプル

[program:celery-handson]
autostart = true
autorestart = true
user = ubuntu
environment = DJANGO_SETTINGS_MODULE=celery_handson.settings
command = /var/django/celery_handson/venv/bin/celery -A celery_handson worker --concurrency=8
directory = /var/django/celery_handson/celery_handson
stopasgroup = true

; logs
stdout_logfile = /var/log/supervisor/celery_handson.log
stdout_logfile_maxbytes = 1MB
stdout_logfile_backups = 5
stdout_capture_maxbytes = 1MB
redirect_stderr = true


; プロセス再起動:
; sudo supervisorctl restart celery-handson


メモ: django_celery_beat でDBで設定したタスクを実行させるには

command = /var/django/celery_handson/venv/bin/celery -A celery_handson beat --scheduler=django_celery_beat.schedulers:DatabaseScheduler

Google Hangouts Chat にプログラムからメッセージを送信する

Google ハングアウトの後継のチャット(インスタントメッセージング)サービス、Chat では、Webhook エンドポイントを使うことでとても簡単にチャットルームへのメッセージの送信ができます。

メッセージの送信に、チャット用の大規模なアプリ開発は不要です。Python でも curl でも JS でも、3行ぐらいでメッセージの送信ができます。

Webhook エンドポイントの作成

1. Chat を開く

https://chat.google.com/

2. チャットルームの作成

左上のメニューから、「チャットルームを作成」を選び、

適当に名前をつける。

3. Webhook エンドポイントの作成

チャットルーム名をクリックするとメニューが開くので、「Webhookを設定」をクリック

+ WEBHOOKを追加 をクリック

適当に名前をつけて、「保存」

Webhook の URL ができる。このURLを記録しておく。

メッセージを送信する

Python

import requests

webhook_url = 'https://chat.googleapis.com/v1/spaces/...%3D'

response = requests.post(
webhook_url,
json={"text": "こんにちは、世界!"}
)

requests を使えば、これだけでメッセージを送信できます。簡単ですね!

curl

curl -X POST "https://chat.googleapis.com/v1/spaces/...%3D" \
--header "Content-Type: application/json; charset=UTF-8" \
--data '{"text": "こんにちは!"}'

これで送信できます。

その他

その他のツールでメッセージを送信するには、Incoming webhook with Python を参考に

HTTPリクエストヘッダ: Content-Type: application/json; charset=UTF-8

リクエストボディはJson: {"text": "Hello from Python script!" }

で送信できます。

400エラーが出たら

テストコードを書いていたら、HTTPステータス400

{
"error": {
"code": 400,
"message": "Request contains an invalid argument.",
"status": "INVALID_ARGUMENT"
}
}

このようなエラーレスポンスが返ってきて困ってたのですが、この原因は単純なURLのコピペミスでした。URLの最後まで正しくコピペできているか、確認してください。

テキストエディターの複数行同時編集で仕事がはかどる

普段、業務でお使いのテキストエディターは、複数の行を一度に編集できる機能がついているかもしれません。

複数行の同時編集を使いこなせば、エンジニアでなくても普段の業務の効率を上げられて便利です。

エディター

非エンジニアに強くおすすめしたいのが、Microsoft の Visual Studio Code (以下VSCode) です。

Visual Studio というと、Windows用のプログラムを開発するIDEのイメージがありますが、このエディタは「ちょうどよくカスタマイズされた Atom」「無料のサブライムテキスト」「フロントエンド以外もいけるブラケッツ」といったモダンな軽量エディタの様相で、秀丸サクラエディタの代わりに使うのにちょうど良いです。

VSCodeはマイクロソフトのサイトからダウンロードして無料で使えます。

エンジニアは、JetBrains のエディターを買うのがおすすめです。

今回は、VSCode と JetBrains のエディター(Android Studio, PHPStorm, PyCharm, RubyMine等) を例にします。

操作方法

1. 矩形選択からの複数行入力

<li class="">りんご</li>
<li class="">バナナ</li>
<li class="">いちご</li>
<li class="">みかん</li>

このテキストの、class="" に一度に 文字を入れます。

[VSCode] Shift+Alt を押しながら下にドラッグ

[JetBrains] Alt を押しながら下にドラッグ

class="" ←このダブルクオーテーションの間をクリックし、クリックしたまま Shift + Alt を一緒に押してドラッグするとキャレットが下に増えます。増えた状態でキーボードから文字を入力すると、複数行に同時に入力されます。

上下方向だけでなく、左右の選択も行えます。矩形に選択した状態で、削除・カット・コピー・ペーストを行うことができます。

2. 同文字列選択からの複数行入力

<li class="">
りんご
</li>
<li class="">
バナナ
</li>
<li class="">
いちご
</li>

このテキストの、class="" に、一度に文字を入れます。

[VSCode] Command+D

[JetBrains] Control+G

文字列を選択した状態で、上記キーを押すと、同じ文字列をファイル中から検索し、選択状態にします。複数選択状態になれば、同時にその箇所を編集できます。

この機能は使い勝手が良く、機械的な判定を行うので人間が目で見て判定して操作する時のようなミスがありません。使用頻度も多く、積極的に使えば速度と品質の両方の向上が期待できます。

例の動画では、class 文字列を選択して同じ文字列を選択、その後文字を入力しています。

上下左右のカーソル移動の他に、行頭([Mac]Control+A)、行末([Mac]Control+E)のカーソル移動も併用して行うことで、効率よくテキストの修正ができます。

3. キャレットを増殖させて入力

<a>りんご</a>
<span>バナナ</span>
<a>いちご</a>
<a class="primary">みかん</a>

このテキストに、class="fruit" を一度に入力します。

[VSCode][JetBrains] Alt を押しながらマウスボタンをクリック

操作することで、その位置にキャレットを増やすことができます。キャレットが増えている状態でキーボード入力をすると、すべてのキャレットに文字が入力されます。

応用編

複数位置へのペースト

複数選択してコピーしたテキストは、複数選択したキャレットへペーストすることができます。

スプレッドシート(エクセル) での結果を処理する(HTMLにする等)時に便利です。

DB(MySQL)をネットワーク越しに簡単にコピーする。mysqldump + パイプで。python subprocess の例も

本番環境のデータベース(MySQL)をネットワーク越しに開発環境にコピーしたい時のプラクティスです。

シェル + パイプ

よくやるのが、bash等 でパイプを使って流し込む方法です。

$ ssh user@production.example.com mysqldump \
--skip-lock-tables \
--host=xxxx.rds.amazonaws.com \
--user=xxxx \
--password=xxxx \
database_name table_name | \
ssh user@dev.example.com mysql \
--host=127.0.0.1 \
--user=xxxx \
--password=xxxx \
--database=xxxx

ssh で本番サーバ user@production.example.com に接続し、mysqldump を実行。その標準出力を SSH 接続を通して手元まで持ってきます。

ssh でもう一つ、開発環境サーバ user@dev.example.com に SSH接続し、mysql を起動。先ほどの本番環境の mysqldump 結果をパイプでそのまま流し込みます。

速度が充分に早く、通信経路も ssh で暗号化されるため安全に、効率良くコピーできます。mysql に余計な穴を空ける必要もありません。

mysqldump のオプションで --where を付けて読み込むデータを絞り込んだりもできます。

Python subprocess を使う

subprocess shell=True で実行

(あまり面白くない。読みにくい。)

import subprocess
subprocess.check_call("上記のコマンド", shell=True)

subprocess で、Popen や check_call などの引数に shell=True を与えることで、シェルコマンドをそのまま実行できます。

subprocess のパイプを使う (おすすめ)

subprocess でシェルのパイプと同様の処理が行えます。

dump_command = [
"ssh",
"user@production.example.com",
"mysqldump",
"--skip-lock-tables",
"--host=xxxx.rds.amazonaws.com",
"--user=xxxx",
"--password=xxxx",
"database_name",
"table_name",
]
dump_process = subprocess.Popen(
dump_command, stdout=subprocess.PIPE)

import_command = [
"ssh",
"user@dev.example.com",
"mysql",
"--host=127.0.0.1",
"--user=xxxx",
"--password=xxxx",
"--database=xxxx",
]
import_process = subprocess.Popen(
import_command, stdout=subprocess.PIPE, stdin=dump_process.stdout)

stdout, stderr = import_process.communicate()

ダンププロセスの stdout を インポートプロセスの stdin に接続して 2 つのプロセスを実行します。

Python のコードにしておけば、再利用性・メンテナンス性を高くでき、使い回しに優れます。

Django のデータベースコネクションを使う場合

from django.db.transaction import get_connection

dump_command = [
"ssh",
"user@production.example.com",
"mysqldump",
"--skip-lock-tables",
"--host=xxxx.rds.amazonaws.com",
"--user=xxxx",
"--password=xxxx",
"database_name",
"table_name",
]
dump_process = subprocess.Popen(
dump_command, stdout=subprocess.PIPE)

connection = get_connection(using='db_alias_name')
cursor = connection.cursor()

cursor.execute(dump_process.stdout.read())

ダンプコマンドの結果を read() して、そのまま Django データベース接続の cursor で流し込むこともできます。

※ダンプ結果を一旦メモリにためるため、データ量が多い場合ちゃんと動くかは不安です。そして他の例より効率は悪そうです。

サーバでSSHの通信断してもバッチを動かし続けるため、tmux を使うと便利。nohup より

tmux という、CUI (TUI) 用仮想スクリーンアプリケーションがあり、SSH 越しに Linux を操作する際大変便利です。似たようなものに byobu とか screen がありますが、私は tmux が好きで、よく使います。

tmux

便利に使うスクリプト

Linux でのシェル起動時、 (.bashrc 等) 下記のスクリプトを起動しています。

#=====================================================
# colors
#-----------------------------------------------------
C_RESET=$'\e[0m'
C_RED=$'\e[31m'
C_GREEN=$'\e[32m'
C_YELLOW=$'\e[33m'
C_BLUE=$'\e[34m'
C_MAGENTA=$'\e[35m'
C_CYAN=$'\e[36m'
C_IRED=$'\e[1;37;41m'
C_IGREEN=$'\e[1;30;42m'
C_IYELLOW=$'\e[1;30;43m'
C_IBLUE=$'\e[1;37;44m'
C_IMAGENTA=$'\e[1;37;45m'
C_ICYAN=$'\e[1;30;46m'
C_IWHITE=$'\e[1;30;47m'

#=====================================================
# tmux
#-----------------------------------------------------
alias TL="tmux ls"

function T() {
if [ "$1" ]; then
tmux a -t $1 || tmux -u new -s $1
else
TMUX_COUNT=`tmux ls 2>/dev/null |wc -l`
if [ ${TMUX_COUNT} -ge 2 ]; then
tmux ls
else
tmux a || tmux -u
fi
fi
}

TMUX_PS=`pgrep tmux`
if [ "${TMUX_PS}" ]; then
if [ ! "${TMUX}" ]; then
echo "? ${C_GREEN}tmux is running. type ${C_RESET}${C_IGREEN} T ${C_RESET}${C_GREEN} to attach.${C_RESET}"
tmux ls
# else already in tmux
fi
# else
# echo "tmux is not running."
fi

説明は下に書きます。

tmux が役立つケース

SSH セッションが切れた時に処理しているプロセスを終了させない、というのが最大の魅力です。

Linuxを触りたての頃は、SSHセッションが切れてもバッチプロセスを継続させるため

$ nohup ... >/tmp/log 2>&1 &

みたいなスクリプトをよく書いてました。が、正直長ったらしいですし、何かあった時シグナル送るのも面倒ですね。

SSH ログインしたサーバ上で tumx を起動し、その中でバッチプロセスを動かせば、回線断などでセッションが切れた場合も処理は継続しますし、事務所行っていた処理を一時中断して回線を切り、PCを会議室に持ってって新たに SSH ログインして tmux に再アタッチし、処理を続ける…なんてこともできます。

ちなみに、複数のユーザー(端末) で1つの tmux セッションにログインし、同じシェルセッションを見るってこともできます。

インストール方法

当社ではサーバ Linux は Ubuntu / Debian が多いので、インストールは

$ sudo apt-get install tmux

です。

基本的な tmux の使い方

起動オプション

$ tmux -u

UTF-8 サポートをONで起動します。日本語を使う場合入れとくと無難です。

( LANG=en_US.UTF-8 などの環境下では関係無いと思いますが、入れても問題無さそうです)

$ tmux a

起動している tmux セッションにアタッチ(再接続)します。回線を接続しなおした後、続きの作業をしたい時に使います。

$ tmux ls

起動しているセッションを一覧表示します

$ tmux a -t <セッション名>

セッション名を指定してアタッチします。

tmux起動後 (tmux内)での操作

Control + B が、tmux用のキーバインド(tmuxプレフィックス)です。これを押した後に別キーを押すことで tmux を操作します。

Control + B → D  … tmux セッションからデタッチ(退出)します。tmux は動き続けます。作業を中断したい場合に使います。tmux からデタッチした後は、Control + D で SSH セッションを終了させて問題ありません。tmux は動き続けます。再接続したい場合は、tmux a です。

Control + B → C  … 新しいウインドウを作成します。ウインドウというか、「タブ」と呼んだほうがイメージしやすいです。

Control + B → N … 次のウインドウ(タブ)

Control + B → P … 前のウインドウ(タブ)

Control + B → [ … コピーモード。スクロールバッファが見えるようになります。上にスクロールすることができるようになります。

文頭のスクリプトの使い方

この記事の上部にスクリプトを書きましたが、これをログインシェルの Bash 起動時に動作するよう、.bashrcなどに書いておくと

1. SSHログイン時、tmux のセッションがあると知らせます

Last login: Tue Mar 13 11:39:53 2018 from
? tmux is running. type T to attach.

このように表示されます。

複数セッションが起動している場合は、セッションのリストを表示します。

2. tmux に簡単にアタッチできます

T ファンクションを作ってます。

$ T

とすると、

  • tmux セッションが無ければ新規セッションを開始
  • tmux セッションがあればアタッチ

という動作をします。

また、引数でセッション名を指定できるので

$ T hello

とすると、セッション hello が無ければ作成、あればアタッチします。

3. その他

TLコマンド

tmux ls のエイリアスなので、実行するとセッションリストを表示します。

ターミナルの色付け

シェル変数 C_なんとか を定義してecho での色付けをしやすくしています。tmux とは直接関係無いですが、外してスクリプトを書き直すのも面倒だったのでついでに掲載しました。

メディアクエリを入れた Style タグつきの HTML メールを各種メーラーで見てみる

Gmail が、2016年末に HTMLメールの style のタグに対応してたということを知ったので、いよいよ style つきの HTMLメールを実用化できると思い、メディアクエリと Flex を含んだ HTML メールを送信してみました。

2017年11月現在の結果として、メディアクエリと Flex は危なそうですが style タグはメジャーな HTML メールで対応していたため、充分に実用的といえます。

HTMLソース

<head>
<meta charset="utf-8" />
<style>
*{
box-sizing: border-box;
}
.wrapper {
width: 100%;
}
.flex-container {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}

.flex-item {
border: 1px solid;
flex-grow: 1;
height: 200px;
flex-basis: 200px;
background-color: #ffd5a7;
}

.book-cover {
width: 100px;
height: 150px;
background-color: red;
}

.row {
overflow: hidden;
}

.pc2col {
width: 100%;
border: 1px solid;
background-color: #cae7ff;
float: left;
height: 100px;
}

@media (max-width: 768px) {
.visible-pc {
display: none;
}

.pc2col {
width: 100%;
}
}

@media (min-width: 769px) {
.visible-sp {
display: none;
}

.pc2col {
width: 50%;
}
}
</style>
</head>
<body>
<div class="wrapper">
<h1>Style テストメール</h1>
<h2>フレックスボックス</h2>
<div class="flex-container">
<div class="flex-item">
<div class="book-cover">A</div>
</div>
<div class="flex-item">
<div class="book-cover">B</div>
</div>
<div class="flex-item">
<div class="book-cover">C</div>
</div>
<div class="flex-item">
<div class="book-cover">D</div>
</div>
</div>

<h2>メディアクエリーブレイクポイント</h2>

<div class="visible-sp">
SPでのみ表示
</div>
<div class="visible-pc">
PCでのみ表示
</div>
<div class="row">
<div class="pc2col">
col1
</div>
<div class="pc2col">
col2
</div>
</div>
</div>

</body>

ソースコードをブラウザでそのまま表示

PCサイズ

SPサイズ

Gmail Web

PCサイズ

SPサイズ

Yahooメール Web

Outlook オンライン

iPhone の「メール」

iPhone の Gmail

iPhone Inbox

iphone Yahooメール

iPhone Yahooアプリ

Android Docomoメール

Android auメール

HTMLメルマガで避けるべき項目

メディアクエリ、Flex

本文記事で書いてある通り、メーラーによって動作まちまちなので使わない。

CSSセレクタでクラスを複数指定

style で、.header.btn のような複数クラスの指定をすると outlook オンラインで機能しない。

マイナスのマージン

margin-top: -20px; など。Gmail で機能しない。

position: relative/ position: absolute

Gmail で機能しない。

style タグ内の CSSのパースエラー

style タグ内に &quot; が混じった時、Gmail でスタイルが全て無効になった。焦るので注意。

box-shadow

Gmail で機能しない。

Django の CRUD ジェネリックビュー (ListView, DetailView, CreateView, UpdateView, DeleteView) の簡単な使い方

Django は、Python プログラミング言語で動作するウェブアプリケーションフレームワークです。

この記事は、Django の使い方を説明するものです。

Djangoの解説: The Web framework for perfectionists with deadlines | Django


Django に最初から用意されているビュー(ジェネリックビュー) を使えば、

  • オブジェクトのリスト表示
  • オブジェクトの作成
  • オブジェクトの詳細表示
  • オブジェクトの更新
  • オブジェクトの削除

のWebアプリが簡単に作成できます。

(オブジェクト=DBのレコード=モデルインスタンス=アクティブレコードオブジェクト という意味です)

Create, Read, Update, Delete をまとめて CRUD と呼んだりしますが、Django では CRUD に対応した汎用的な基底ビューコントローラが用意されているため、それを継承してビューを作ることで、安全で、読みやすいコードを書けます。

それぞれ、以下のビューコントローラに対応しています。

Create … CreateView
Read (リスト表示) … ListView
Read (詳細表示) … DetailView
Update … UpdateView
Delete … DeleteView

django.views.generic の中にいます。

Django では、これらのさらに基底となる TemplateView, FormView もありますが、やっている仕事として「モデルインスタンスを1つ作成/更新」「モデルインスタンス1つを詳しく表示」など、今回の CRUD の基底ビューに含まれる場合は、TemplateView, FormView を使うよりこれらのジェネリックビューを継承したほうがコードを読む際に書き手の意図を理解しやすいため、これらのクラスを使うべきです。

今回は、シンプルなテキストメモを作成・更新するWeb アプリのサンプルを作ってみました。
GitHubにコードを上げてありますので、同時にご参照ください。

リポジトリ

https://github.com/ytyng/django-crud-generic-view-tutorial

核心は、views.py なのでそれを見るとよくわかります。お急ぎの方はこれだけ読んでみてください。

https://github.com/ytyng/django-crud-generic-view-tutorial/blob/master/memo/memo/views.py

環境

Django 1.11, Python 3.6

ディレクトリ構造

memo/
+ manage.py
+ db.sqlite3
+ memo /
+ settings.py (Django設定(自動生成))
+ wsgi.py (WSGIランナー(自動生成))
+ urls.py (URLマップ(自動生成))
+ models.py (Memoモデルの定義)
+ views.py (ビューコントローラ)
+ forms.py (HTMLフォームクラス)
+ migrations (マイグレーションスクリプト(自動生成))
+ templates (HTMLテンプレート)
+ memo
+ base.html
+ memo_confirm_delete.html (削除確認用テンプレート)
+ memo_detail.html (詳細表示用テンプレート)
+ memo_form.html (作成、更新フォーム用テンプレート)
+ base.html (基底テンプレート)
+ memo_list.html (リスト表示用テンプレート)

コード解説

解説はコード中のコメントに書きました

models.py

from django.db import models


class Memo(models.Model):
"""
メモモデル。実質、件名と本文のみ。
"""
subject = models.CharField(
verbose_name='件名',
max_length=100,
default='',
blank=True
)

body = models.TextField(
verbose_name='本文',
default='',
blank=True
)

# created: auto_now_add を指定すると、作成日時を自動保存する
created = models.DateTimeField(
auto_now_add=True
)

# updated: auto_now を指定すると、更新日時を自動保存する
updated = models.DateTimeField(
auto_now=True
)

def __str__(self):
return self.subject

forms.py

from django import forms

from .models import Memo


class MemoForm(forms.ModelForm):
"""
Memo モデルの作成、更新に使われる Django フォーム。
ModelForm を継承して作れば、HTMLで表示したいフィールドを
指定するだけで HTML フォームを作ってくれる。
"""

class Meta:
model = Memo
fields = ['subject', 'body']

views.py

今回の核心です。Django の ListView, DetailView, CreateView, UpdateView, DeleteView をそれぞれ継承し、CRUDのビューコントローラを作成しています。

フォームクラスは、先程の MemoForm を使います。

それぞれのビューで書かなければならない内容は少なく、実務的な処理は Django に任せることで、エンジニアは UI の開発に集中できます。

from django.contrib import messages
from django.urls import reverse_lazy
from django.views.generic import \
ListView, DetailView, CreateView, UpdateView, DeleteView

from .models import Memo
from .forms import MemoForm


class MemoListView(ListView):
"""
メモを一覧表示
テンプレートは、何も指定しないと モデル名_list.html が使われる
ListView は、パジネーションもやってくれる
"""
model = Memo
paginate_by = 10 # 1ページに表示する件数


class MemoDetailView(DetailView):
"""
1つのメモを詳細表示
テンプレートは、何も指定しないと モデル名_detail.html が使われる
"""
model = Memo


class MemoCreateView(CreateView):
"""
メモ 新規作成
完了ページを作成し、success_url で指定して表示してもいいが、
django.contrib.messages の機能で、メッセージを保存して
リストビューなんかに戻した時に表示するのも簡潔で良い。
"""
model = Memo
form_class = MemoForm
success_url = reverse_lazy('memo_list')

def form_valid(self, form):
result = super().form_valid(form)
messages.success(
self.request, '「{}」を作成しました'.format(form.instance))
return result


class MemoUpdateView(UpdateView):
"""
メモ 更新
"""
model = Memo
form_class = MemoForm

success_url = reverse_lazy('memo_list')

def form_valid(self, form):
result = super().form_valid(form)
messages.success(
self.request, '「{}」を更新しました'.format(form.instance))
return result


class MemoDeleteView(DeleteView):
"""
メモ 削除
デフォルトでは、get でリクエストすると確認ページ、
post でリクエストすると削除を実行する、という動作。
実際は、レコードを削除するのではなく有効フラグを消す(いわゆる論理削除)
のケースが多いと思うので、そんな時はdeleteをオーバーライドしてその中で処理を書く。
"""
model = Memo
form_class = MemoForm

success_url = reverse_lazy('memo_list')

def delete(self, request, *args, **kwargs):
result = super().delete(request, *args, **kwargs)
messages.success(
self.request, '「{}」を削除しました'.format(self.object))
return result

urls.py

from django.conf.urls import url
from django.contrib import admin

from . import views

urlpatterns = [
url(r'^admin/', admin.site.urls),

url(r'^$',
views.MemoListView.as_view(),
name='memo_list'),

url(r'^detail/(?P<pk>\d+)/$',
views.MemoDetailView.as_view(),
name='memo_detail'),

url(r'^create/$',
views.MemoCreateView.as_view(),
name='memo_create'),

url(r'^update/(?P<pk>\d+)/$',
views.MemoUpdateView.as_view(),
name='memo_update'),

url(r'^delete/(?P<pk>\d+)/$',
views.MemoDeleteView.as_view(),
name='memo_delete'),
]

テンプレート

Bootstrap4でさっくり作ってあります。

base.html

<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{# Bootstrap4 で。#}
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css"
integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb"
crossorigin="anonymous">
<title>{% block meta_title %}{% endblock %}</title>
<body>

{% if messages %}
{# Django のメッセージに記録している内容があればここで表示 #}
<div class="container">
<div class="row">
<div class="col-12">
<div class="messages mt-3">
{% for message in messages %}
<div class="alert alert-dismissable alert-{{ message.tags }}" data-alert="alert">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{{ message }}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}

<div class="container">
<div class="row">
<div class="col-12">
<h1 class="mt-5">{% block page_title %}Memo{% endblock %}</h1>
</div>
</div>
</div>
{% block content %}
{% endblock %}
{% block foot_scripts %}
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"
integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js"
integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ"
crossorigin="anonymous"></script>
{% endblock %}
</body>
</html>

memo_list.html

シンプルなメモ一覧テンプレート。簡易的なパジネーションつき

{% extends 'memo/base.html' %}
{% block content %}

<div class="container">
<div class="row">
<div class="col-12">
<div class="card card-default">
<ul class="list-group list-group-flush">
{% for o in object_list %}
<li class="list-group-item">
<a href="{% url 'memo_detail' o.pk %}">{{ o.subject }}</a>
</li>
{% empty %}
<li class="list-group-item">
メモはありません
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="col-12">
<div class="mt-3 text-center">
{% if page_obj.has_previous %}
<a class="btn btn-secondary" href="/?page={{ page_obj.previous_page_number }}">前へ</a>
{% endif %}
<span>{{ page_obj.number }}/{{ paginator.num_pages }}ページ</span>
{% if page_obj.has_next %}
<a class="btn btn-secondary" href="/?page={{ page_obj.next_page_number }}">次へ</a>
{% endif %}
</div>
</div>
</div>
</div>

<div class="container">
<div class="row">
<div class="col-12 mt-3">
<a href="{% url 'memo_create' %}" class="btn btn-primary">新規作成</a>
</div>
</div>
</div>
{% endblock %}

memo_detail.html

詳細表示。MemoDetailView で使われる。

{% extends 'memo/base.html' %}
{% block meta_title %}{{ object.subject }}{% endblock %}
{% block page_title %}{{ object.subject }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12">
<div class="card card-default">
<div class="card-body">
{{ object.body|linebreaksbr }}
</div>
<div class="card-footer">
<a class="btn btn-primary" href="{% url 'memo_update' object.pk %}">修正</a>
<a class="btn btn-danger" href="{% url 'memo_delete' object.pk %}">削除</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

memo_form.html

新規作成/更新フォーム。 MemoCreateView, MemoUpdateView で使われる。
HTMLフォームは、MemoForm のインスタンスを bootstrap フィルタにかければ自動的にいい感じで作ってくれる。バリデーション〜エラー対応もやってくれる。

{% extends 'memo/base.html' %}
{% load bootstrap %}
{% block meta_title %}{% if object.pk %}{{ object.subject }}{% else %}新規作成{% endif %}{% endblock %}
{% block page_title %}{% if object.pk %}{{ object.subject }}{% else %}新規作成{% endif %}{% endblock %}
{% block content %}
<form method="post">
<div class="container">
<div class="row">
<div class="col-12">
{{ form|bootstrap }}
</div>
</div>
<div class="row">
<div class="col-12">
<button class="btn btn-primary">保存</button>
</div>
</div>
</div>
{% csrf_token %}
</form>
{% endblock %}

memo_confirm_delete.html

削除確認フォーム

{% extends 'memo/base.html' %}
{% load bootstrap %}
{% block meta_title %}削除確認{% endblock %}
{% block page_title %}削除確認{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12">
<p>{{ object }} を削除していいですか?</p>
<form method="post">
<button class="btn btn-danger">削除する</button>
{% csrf_token %}
</form>
</div>
</div>
</div>
{% endblock %}

パスワードを設定する時の注意とパスワード管理アプリの紹介

社内MTGで話した内容の共有です。

パスワードを新しく作る時の注意

辞書に載っている語句は使わない

辞書に載っている単語は誰でも知っている単語であり、簡単に組み合わせパターンを生成できるため使ってはいけません。

思いついたパスワードは使わない

パスワードを考える時、パスワードを思いつくことがあります。
私の姓は yotsuyanagi ですが、例えばこれを 逆にして、 o と i を数字にして、一部頭文字にして
1GanayustOy

というパスワードを思いついたとしますが、使ってはいけません。
自分が思いつくようなパスワードは、プログラムでも簡単に作れます。
他にも、PCキーボードやスマホのキーボードで打ちやすい文字を思いついた、とかもダメです。

ダメなパスワードの例

ilovetokyo2408

The-Quick-Brown-Fox

asdfzxcvjkl;m,./

このようなパスワードは、簡単に推測されます。

12345678, password, admin, 会社名なんかは論外です。

良いパスワードの例

ja3~hAb?eW%a

pAPr!p2u'u<a

これらのパスワードは、簡単に推測されることはありません。

どうやって作るのか?

パスワード管理ツールについている生成器(パスワードジェネレータ)で作りましょう。

例外を認めない

加えて、パスワードを作る時は例外を認めないようにしましょう。
・仮のパスワードだから
・検証環境だから
・試しで使ってみるだけだから
等、理由をつけて推測しやすいパスワードにしてはいけません。
例外かどうか、を判定するのにもコストがかかります。「パスワード」という単語が出てきたら、ジェネレータで作る以外に方法は無いと習慣づけましょう。

サイト毎にパスワードは変える

(他のサイトで使っているパスワードは使わない)

複数のサイトでパスワードを使いまわしていると、ある1つのサイトのパスワードが漏洩してしまった場合、他の同じパスワードのサイトがすべて乗っ取られてしまいます。

パスワードの漏洩は、フィッシングなどに引っかかって漏らしてしまう場合もあれば、サービスが保持しているパスワードがハッシュ化されておらず、悪意ある従業員が見たり、セキュリティホールからデータが漏洩してしまう危険性もあります。そのような時に被害を最小限にするため、サイトごとにパスワードは必ず変えましょう

さらに安全に使うには

サービスで2段階認証を使える場合、有効にしましょう。
少し手間は増えますが、受けられるメリットのほうがはるかに大きいです。

2段階認証を使いましょう。繰り返しお伝えします。2段階認証を使いましょう

パスワードを保存する時

パスワードツールを使いましょう

メモ帳や表計算ソフトはやめて、パスワード管理ツールを使いましょう。
マスターパスワードがつけられて、内容が暗号化される必要があります。

パスワードツールの紹介

無料

無料アプリはサポートが充実しておらず、アップデートも頻繁ではないためできれば有料アプリを使いましょう。

無料/Windows

Keepass

完全無料でいくならこれ。Windows, Android で使えます。
Mac, iPhone は 一応使えますが、クライアントアプリのアップデートがあまりされておらず、おそらく keepass ver.1 しか対応アプリは無さそうなので Windows と連携する時は注意。
あまり便利な機能などありませんが、一応使えます。

データの保存先を Dropbox など信頼できるクラウドストレージにすれば、機種間のパスワード同期も可能です。

Buttercup

比較的新しいアプリです。使ったこと無いのですが、けっこう良さそうです。(Electronかな?)

ID manager

古くからあるパスワード管理ツールです。シンプルながら十分使えます。

データの保存先を Dropbox など信頼できるクラウドストレージにすれば、機種間のパスワード同期も可能です。

無料/mac

キーチェーンアクセス

mac でお金をかけたくないなら、macにインストールされているキーチェーンアクセスアプリを使いましょう。
Windows や Android との連携はできないので、Windows でも読みたいなどあれば他のアプリにしましょう。

無料でもなんとか使える (フリーミアム)

keeper

なにかと有料プランへの告知が出ますが、無料でも十分に使えます。マルチデバイスでの対応状況も良いです。
有料の場合、¥3,600/年

Lastpass

使ったことありませんが有名、サイトを見るかぎりけっこういけそう。
有料の場合、$2/月 の年間払いで $24/年

データ漏洩がニュースになったこともありました。

有料 (サブスクリプション課金)

1Password

僕はこれです。マルチデバイスは一応対応ですが、OSごとに違うライセンス(アプリ代金)が必要なので少し高額になります。
→最近は買い切りではなくストレージつきサブスクリプションプランになってました。$2.99/月 か、$4.99/月 のファミリープランがありました。
年だと $35.88/年

True Key

Intel/マカフィーブランド。無課金だと15件しか保存できず、使い続けるのは難しそうなので有料扱いにしています。昔は5件だったような。機能十分。
有料だと2,678円/年 ($19.99)。

トレンドマイクロ パスワードマネージャー

無課金だと5件しか保存できず、使い物にならないので有料扱いにしています。
月額150円ぐらい、有料にしては安いほう。
年だと 1,800円ぐらい

有料 (買い切り)

買い切りのパスワード管理アプリは今のところ有力なものが無さそうです。

昔は 1Password が買い切りだったが今はやってない?
あと mac 用では、Forklift を作ってる会社の Locko というアプリもありましたが、(日本のストアでは?)非公開になってました。


おすすめは?

正直、どのアプリでも十分な成果は期待できそうです。

あえて言うなら家族で使うなら、1Password ファミリープラン。mac の人にすすめるなら 1Password。
Windows 使ってる人に勧めるなら、Truekey でしょうかね…

私は 1Password を過去に買い切りで買っているのでそれをずっと使っています。
当時は良いアプリでしたが、現在は競合も多くそれほどの優位性は無いと感じています。

やむおえず人にパスワードを伝える時

基本的に、アカウントの使い回しは無いに越したことありませんが、業務上まれにあります。

・メールに書かない

経路によっては回線が暗号化されておらず、内容が盗聴される可能性があります。

また、メールサーバが暗号化せず保存している場合も多いので、比較的簡単に中身が読まれる可能性があります。

どうする?

メール添付する場合は、暗号化してファイルを添付する。 gnupg とか使う

メールを使わず、別の安全な経路で伝える。お互いSSHで入れるサーバがあるならそこに書くとか。

フィッシングメールに注意

たまに、メールでフィッシングが来ます。Gmail だと見たことないのですが、Outlook online だと見かけたります。

メールの内容としては、「Apple ID がリセットされました。すぐログインして確認してください」等の文言が書いてあり、ログインを促すボタンが表示されています。その他、巧妙な文面で不安を煽る内容が書かれているはずです。

あなたは絶対にボタンを押してはいけません。

ログインボタンを押した時点で、あなたのメールアドレスは「フィッシングメールを踏む情弱カモ」と認定されリストに登録されます。その後、情弱なあなたにフィッシング攻撃のメールやらスパムやらが大量に送られてくることになります。

ログインボタンを押すと、おそらく偽物のログインフォームが設置されたページ表示されます。見た目は、本物のサイトと区別がつかないはずです。おそらくSSL対応され、ブラウザ上は安全な鍵マークが表示されていることでしょうが、ブラウザ上のドメイン表示は偽ることはできないため、ドメインをしっかり見れば偽物のサイトだと気づくかもしれません。

セキュリティ対策ソフトがインストールされている場合、ここで警告が出るはずです。最近のブラウザであれば、ブラウザ自体が警告を出すかもしれません。(出さないかもしれません。)

それでも気付かずにメールアドレスとパスワードを入力した場合、おそらくは正規のログインページに飛ぶ気がします。あなたは、「パスワードを間違ったのかな?」と思うだけかもしれませんが、攻撃者は既にパスワードを入手しています。

この次の攻撃はおそらく2つのパターンに別れます。

A) 攻撃者がパスワードを変更しない (静かな攻撃)

攻撃者はあなたのパスワードを入手しましたが、活動的な攻撃は行いません。そのメールアドレスとパスワードを使い、他のサイトにログインできるかを試します(プログラムが自動的に行います)。そして、ログインできた全てのサイトを記録し、あなたの活動を監視します。それで攻撃者の目的が達成される場合もありますし、もしくはどこかのタイミングで大きな攻撃をするために待っているだけかもしれません。

B) 攻撃者がパスワードを変更する (活発な攻撃)

パスワードを入手した攻撃者は、そのパスワードを使ってサイトにログインし、すぐパスワードを変更します。場合によってはパスワードリマインダーが使えないよう、メールアドレスも変更するかもしれません。おそらくプログラムが自動的に行います。

そして、そのアカウントを使い、SNSに広告を投稿したり、友達リストの友達に「コンビニでプリペイドカード買って」など連絡を取るかもしれません。

あなたはそのアカウントにログインする方法も、パスワードをリセットする方法も持たないため、アカウントをあきらめるしかありません。すぐさま、知人に「乗っ取られたので変なこと言われても無視して」と連絡しましょう。

Amazon マーケットプレイスWebサービス (MWS) APIから注文情報を取得する方法

当社では、Amazon のマーケットプレイスに出店していたり、FBA (フルフィルメントByアマゾン: Amazon社の倉庫に商品を納品し、販売を代行してもらう販売方法) を行っています。

マーケットプレイスにはAPIが用意されており、リクエストすることで受注情報など多くの情報を取得できるのですが、署名の計算に少し躓いたので書いておきます。

Amazon MWS 公式ガイド

アクセスに必要な情報を集める

認証情報 (クレデンシャル)

アクセスに必要な認証情報は、

  1. 出品者ID (マーチャントID, セラーIDと呼ばれることもある)
  2. AWSアクセスキーID
  3. 秘密キー(シークレットキー)

の3つです。もし、アクセスするアカウントがセラーセントラルのアカウントではなく、派生して作られた子アカウントである場合、別途「MWS認証トークン」が必要になります。

これらの情報は、すべてセラーセントラルの「設定」→「ユーザー権限」のページで取得できます。AWSアクセスキーID,秘密キー は、ページ下部の「Amazon MWS 開発者権限」の、「認証情報を表示」をクリックすると表示されます。

マーケットプレイスID

別途、マーケットプレイスID という文字列が必要になる場合があります。

マーケットプレイスIDの一覧はページから確認できます。

Amazon マーケットプレイス Web サービスエンドポイント

例えば、日本なら A1VC38T7YXB528 です。

APIアクセスのテストをする

Amazon社がAPIのテストツール「Scratchpad」を公開しているので、それを使います。後述する HMAC の計算が正しいか確認する意味でも、このツールは必ず使ってみたほうが良いです。

  1. Amazon MWS Scratchpad を開く
  2. 左上「API Selection」を適当に選択。今回は、API セクション: 注文、Operation: ListOrders
  3. Authentication 欄に認証情報を入力、SellerId: には 出品者ID, MWSAuthToken は元アカウントなら空、払い出された子アカウントならトークン文字列を入れる。AWSAccessKeyId、Secret Kye は先ほど取得した文字列を取得。
  4. API必須パラメータの「MarketplaceId.Id.1」には、A1VC38T7YXB528 を入力
  5. API任意パラメータの「LastUpdatedAfter」のみ、2017-05-05 のように入力
  6. 「送信」をクリックすると、結果が表示されます。

HMAC の値を確認しておく

結果が正常に表示された場合、「リクエスト」タブをクリックして開いてみると「署名対象の文字列」というセクションと、その下に計算した HMAC署名 が表示されています。APIを開発する時は、この情報を元に開発するとやりやすいです。「証明対象の文字列」に対して SHA 256 HMAC で計算を行い、その下に書かれている文字列が結果で得られるよう開発をしていきます。

APIライブラリを開発する

Pythonで作ります。

署名方法は、Amazonで「署名バージョン2」と言われる方法です。

公式ドキュメントの署名のロジック説明 (Java のサンプルコードあり)

HMACの計算ロジックの作成

Python には hmac ライブラリがあるので、それを使えばすぐにできます。

Python3での例

import hmac
import hashlib
import base64

secret_key = b"取得した秘密キー"

canonical = b"""
POST
… 「証明対象の文字列」をここにコピペ …
"""

h = hmac.new(secret_key, canonical.strip(), hashlib.sha256)

print(h.hexdigest())
print(base64.b64encode(h.digest()))

これを実行すると、Scratchpad に表示されている「SHA 256 HMAC」「Base64 HMAC」と同じ値が取得できるはずです。

署名対象の文字列の作成

署名対象の文字列は、HTTPメソッド(POST)、ドメイン名(mws.amazonservices.jp)、パス(/Orders/2013-09-01)、それとクエリ文字列を、改行(\n)で連結して作ります。

クエリ文字列の作成

クエリ文字列は、検索パラメータ名をソートさせ、&= で連結して作ります。値は URL エンコードします。

ディクショナリで値を用意してたとすると、

import datetime
import urllib.parse

AMAZON_CREDENTIAL = {
    'SELLER_ID': 'セラーID',
    'ACCESS_KEY_ID': 'AWSアクセスキーID',
    'ACCESS_SECRET': 'アクセスシークレット',
}

data = {
    'AWSAccessKeyId': AMAZON_CREDENTIAL['ACCESS_KEY_ID'],
    'Action': 'ListOrders',
    'MarketplaceId.Id.1': 'A1VC38T7YXB528',
    'SellerId': AMAZON_CREDENTIAL['SELLER_ID'],
    'SignatureMethod': 'HmacSHA256',
    'SignatureVersion': '2',
    'Timestamp': datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
    'Version': '2013-09-01',
}

query_string = '&'.join('{}={}'.format(
    n, urllib.parse.quote(v, safe='')) for n, v in sorted(data.items()))

print(query_string)

このようなロジックで作成できます。

sorted メソッドでキーで並び替えを行い、値は urllib.parse.quote で URLエンコードします。safe='' を入れないと / がエンコードされないので、入れます。

後は、改行で連結すれば署名対象文字列になります。

canonical = "{}\n{}\n{}\n{}".format(
    'POST', 'mws.amazonservices.jp', '/Orders/2013-09-01', query_string
)

print(canonical)

署名をつけてリクエストする方法

リクエストメソッドは POST です。ですが、POSTのデータは空で、パラメータはURLのクエリストリングに入れます。

署名は、クエリストリングの末尾に &Signature=署名 という形で付与します。

requests でリクエストしてみる

実際にリクエストするコードを書いてみます。

import base64
import datetime
import hashlib
import hmac
import urllib.parse

import requests
import six

AMAZON_CREDENTIAL = {
    'SELLER_ID': 'セラーID',
    'ACCESS_KEY_ID': 'AWSアクセスキーID',
    'ACCESS_SECRET': 'アクセスシークレット',
}

DOMAIN = 'mws.amazonservices.jp'
ENDPOINT = '/Orders/2013-09-01'


def datetime_encode(dt):
return dt.strftime('%Y-%m-%dT%H:%M:%SZ')


timestamp = datetime_encode(datetime.datetime.utcnow())

last_update_after = datetime_encode(
datetime.datetime.utcnow() - datetime.timedelta(days=1))

data = {
'AWSAccessKeyId': AMAZON_CREDENTIAL['ACCESS_KEY_ID'],
'Action': 'ListOrders',
'MarketplaceId.Id.1': 'A1VC38T7YXB528',
'SellerId': AMAZON_CREDENTIAL['SELLER_ID'],
'SignatureMethod': 'HmacSHA256',
'SignatureVersion': '2',
'Timestamp': timestamp,
'Version': '2013-09-01',
'LastUpdatedAfter': last_update_after,
}

query_string = '&'.join('{}={}'.format(
n, urllib.parse.quote(v, safe='')) for n, v in sorted(data.items()))

canonical = "{}\n{}\n{}\n{}".format(
'POST', DOMAIN, ENDPOINT, query_string
)

h = hmac.new(
six.b(AMAZON_CREDENTIAL['ACCESS_SECRET']),
six.b(canonical), hashlib.sha256)

signature = urllib.parse.quote(base64.b64encode(h.digest()), safe='')

url = 'https://{}{}?{}&Signature={}'.format(
DOMAIN, ENDPOINT, query_string, signature)

response = requests.post(url)

print(response.content.decode())

下品にベターっと書いてますが、これで動きます。

実際にはこれをライブラリ化して肉付けしていくと良いでしょう。

Chrome58で、HTTPSの自己証明書が NET::ERR_CERT_COMMON_NAME_INVALID になる場合の対応

Google Chrome をバージョン58 にアップデートすると、SSL自己証明書を使っているサイトが見れなくなる場合があります。

自己証明書(オレオレ証明書)を使っているサイトに HTTPS でアクセスすると、

この接続ではプライバシーが保護されません

攻撃者が、tech.torico-corp.com 上のあなたの情報(パスワード、メッセージ、クレジット カード情報など)を
不正に取得しようとしている可能性があります。 NET::ERR_CERT_COMMON_NAME_INVALID

セキュリティに関する事象についての詳細を Google に自動送信する。プライバシー ポリシー
セキュリティで保護されたページに戻る詳細情報を表示しない
このサーバーが tech.torico-corp.com であることを確認できませんでした。
このサーバーのセキュリティ証明書は [missing_subjectAltName] から発行されています。
原因として、設定が不適切であるか、悪意のあるユーザーが接続を妨害していることが考えられます。
詳細

tech.torico-corp.com にアクセスする(安全ではありません)

このような感じのエラーになります。

原因

SSL証明書のエントリをテキスト形式で見ると

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 35 (0x23)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=JP, ST=Tokyo, L=Chiyodaku, O=TORICO, OU=CA, CN=tech.torico-corp.com/...
        Validity
            Not Before: Apr 27 05:54:47 2017 GMT
            Not After : Apr 25 05:54:47 2027 GMT
        Subject: C=JP, ST=Tokyo, O=torico, CN=*.torico-corp.com ←※
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
...

このような感じになっていると思います。大抵、証明書を設置するドメインを「←※」の箇所の CN= に書きますが、Chrome 58 以降、この CN= を評価しなくなったようです。

そのため、閲覧しているドメインが CN= に一致しても、証明書が検証できないとしてエラーになります。

対策

ドメイン名は CN= では判定しなくなったため、どこで判定するかというと

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 35 (0x23)
        Signature Algorithm: sha256WithRSAEncryption
...
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            Netscape Comment:
                OpenSSL Generated Certificate
            X509v3 Subject Key Identifier:
...
            X509v3 Authority Key Identifier:
...

            X509v3 Subject Alternative Name:
                DNS:torico-corp.com, DNS:*.torico-corp.com ←※

    Signature Algorithm: sha256WithRSAEncryption

この、X509v3 Subject Alternative Name: DNS: で判定します。略して SAN と言われるやつです。

DNS: という名前がめちゃくちゃわかりにくいですが、ここに CN= に書いてたドメインと同じものを記入することで Chrome58+ で認識します。

この値は Chrome58 より古いバージョンでも読めるようになっていますが、Chrome58 は CN= が読み込まれなくなったため、SANに書いてます。

SAN の DNS: は複数書けるので、手持ちのドメインをすべて列挙しておくと便利でしょう。

証明書の作り方

認証局証明書 (CA証明書)

認証局証明書 (CA の PEM から作る公開証明書) の更新は不要です。今までの証明書を使いまわせます。

CSR (認証リクエスト)

openssl req -new -newkey rsa:2048 -nodes \
    -out ${CSRファイル名}.csr \
    -keyout ${鍵ファイル名}.key \
    -sha256 \
    -config ca.conf \
    -subj "/C=JP/ST=Tokyo/L=Chiyoda/O=torico/CN=${CN}"

おそらくこのような感じのコマンドで CSR を作られていると思いますが、この -config のファイルで次のように指定します。

ca.conf

[ ca ]
default_ca = CA_default

[ CA_default ]
...
x509_extensions = usr_cert
...

[ req ]
default_bits = 2048
default_keyfile = privkey.pem
distinguished_name = req_distinguished_name
attributes = req_attributes
x509_extensions = v3_ca
req_extensions = v3_req

...

[ usr_cert ]
basicConstraints=CA:FALSE
nsComment = "OpenSSL Generated Certificate"

subjectKeyIdentifier = hash
authorityKeyIdentifier=keyid,issuer:always
subjectAltName = @alt_names

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = torico-corp.com
DNS.2 = *.torico-corp.com
DNS.3 = mangazenkan.com
DNS.4 = *.mangazenkan.com
DNS.5 = sukima.me
DNS.6 = *.sukima.me

かいつまんで書いてますが、こんな感じです。

CSRを作る際

[req] req_extensions = v3_req

[v3_req] subjectAltName = @alt_names

[alt_names] DNS.1 = サーバのドメイン名

といった流れで参照します。この設定ファイルを -config で指定して CSR を作ると、CSR に X509v3 Subject Alternative Name:エントリが入ります。

作ったCSR をテキストで確認するには

openssl req -noout -text -in ${CSRファイル名}.csr

で見れます。

Certificate Request:
    Data:
        Version: 0 (0x0)
        Subject: C=JP, ST=Tokyo, L=Chiyoda, O=torico, CN=*.torico-corp.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
        Attributes:
        Requested Extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            X509v3 Key Usage:
                Digital Signature, Non Repudiation, Key Encipherment
            X509v3 Subject Alternative Name:
                DNS:torico-corp.com, DNS:*.torico-corp.com, DNS:mangazenkan.com, DNS:*.mangazenkan.com, DNS:sukima.me, DNS:*.sukima.me

このように、複数のDNS: が出力されているはずです。

CSRをCAで認可しサーバ証明書を作る

openssl ca -batch -passin pass:${CAパスワード} -config ca.conf \
    -in ${CSRファイル名}.csr \
    -keyfile ${CA鍵ファイル}.key \
    -cert ${CA鍵証明書}.pem \
    -out ${出力するサーバ証明書}.crt -days 3650

このようなコマンドでサーバ証明書を作られると思いますが、今回も -config で先ほどと同じファイルを指定しています。

先ほどは [req] のディレクティブを読み込んで使いましたが、今回は

[ CA_default ] x509_extensions = usr_cert
             ↓
[ usr_cert ] subjectAltName = @alt_names
             ↓
[alt_names] DNS.1 = ...

とたどって alt_names ディレクティブを読み込みます。

(読む項目が違うので、CSRを作る時に使った設定ファイルと設定ファイルを別にしても良いです)

(もしかして、このCSRのCA認証だけ SANを出力するよう設定しておけば、CSR を作る際の SAN出力は不要かもしれません。未確認)

これで証明書を作ると、

Certificate Details:
        Serial Number: 35 (0x23)
        Validity
...
        Subject:
            countryName               = JP
            stateOrProvinceName       = Tokyo
            organizationName          = torico
            commonName                = *.torico-corp.com
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            Netscape Comment:
                OpenSSL Generated Certificate
            X509v3 Subject Key Identifier:
...
            X509v3 Authority Key Identifier:
...

            X509v3 Subject Alternative Name:
                DNS:torico-corp.com, DNS:*.torico-corp.com, DNS:mangazenkan.com, DNS:*.mangazenkan.com, DNS:sukima.me, DNS:*.sukima.me

このように、X509v3 Subject Alternative Name:  (SAN) が出力されます。

この PEM を Nginx やら Apache やらの SSL 証明書に指定し、サーバを再起動すれば Chrome58 でも証明書のエラーが出なくなるはずです。

ブラウザ上で簡単なスクリプトをブックマークから動かす方法(ブックマークレット)

この記事は、非プログラマ向けの技術記事です。非プログラマが、プログラミングを始める足がかりとなることを目的としています。

WEBブラウザと日常業務

日常業務でウェブブラウザを使う機会はけっこうあると思います。いろいろなツールが Webアプリ化されるにつれ、ブラウザを業務で使う機会は昔と比べて増えました。

ただし、多くの項目があるフォームを毎日書く業務があったり、あるページを開いて項目を1つ1つスプレッドシートや他のWebアプリ、もしくはメールにコピペしたりといった業務があったりもします。もし、その業務が面倒だと感じているならば、自動化できる余地は十分あります。

プログラマは、面倒だと思った作業はすぐに自動化できますが、プログラミングに明るくない方でも、範囲は限定的ですが Webアプリであれば面倒が改善できるかもしれません。

WEBブラウザでプログラムを動かせる

お使いのブラウザで、今すぐに自分でプログラムを書いて実行できます。そういう機能がブラウザに入っています。

Windows の Chrome, Firefox であれば、Ctrl + Shift + i を押してみてください。Edge をお使いであれば F12 です。Mac であれば Command + Option + i です。

すると、画面が分割され別のペインが出てきます。もし、警告文が出てきたら読み、同意/認可ができるようであればブラウザの案内にそって対応してください。

おそらく最初は、Firefoxの場合は「インスペクタ」、Chromeの場合は「Elements」タブが有効になっていると思います。その隣に、「コンソール」「Console」タブがありますので、それをクリックするとコンソールモードになります。

↑ Firefox

↑ Google Chrome

Firefox では、一番下に >> となっています。ここをクリックするとプログラムが入力できます。Chrome の場合は一番上に > があり、ここにプログラムが入力できます。

試しに、簡単な計算式を書くと、計算ができます。

↑消費税計算と81の平方根の算出をした例

Math.sqrt は、ブラウザの Javascript エンジンに最初から組み込まれている平方根計算の関数です。どんなことができるかはJavascript の入門書を読むと良いと思いますが、「何かをウェブページに表示する」であれば大概できます。(ただし、表示しているサイトとは別のサイトから持ってきた情報を表示するのは少し難しい場合があります)

別の例を試します。コンソールに alert('今日は' + new Date()) と入力します。そうすると、現在の日時がポップアップで表示されます。

また、history.back() を入力すれば、ブラウザの「戻る」ボタンを押した時と同じように、前のページへ戻ることができます。

このように、ウェブブラウザには非常に簡単にプログラムを動かす仕組みが入っています。

アドレスバーからでもプログラムを実行できる

先程は開発コンソールからプログラムを実行しましたが、ブラウザ上部にあるアドレスバーからでもプログラムの実行ができます。

…できたはずなんですが、最近のブラウザは、アドレスバーに入力した文字列が URL ではないと判断した場合、検索エンジンで検索するようになっているので、アドレスバーにスクリプトを入力しての実行する方法は最近のブラウザでは難しくなっています。

昔のブラウザでは、アドレスバーに

javascript:alert('今日は' + new Date());void(0);

と入力すると、先程コンソールでプログラムを動かしたのと同様に alert を動作させることができました。

アドレスバーに直接入力しての動作はできなくなりましたが、リンクのタグとして Webページに記録すれば動作させることができます。

プログラムを動作させるリンクタグ(クリックで上記プログラムを実行)

ブックマークからでもプログラムを実行できる

アドレスバーに入力したプログラムが動作するということは、そのプログラムをブックマークとして保存しておけば、そのブックマークを呼び出した時にプログラムが動作することになります。

試しに、上記「プログラムを動作させるリンクタグ」をブックマークしてください。リンクをブックマークツールバーにドラッグアンドドロップすると良いでしょう。

ブラウザによっては、「ブックマークの新規作成」のようなメニューから、さきほどの

javascript:alert('今日は' + new Date());void(0);

このスクリプトをURLとして登録すればブックマークできます。

ブックマークができたら、クリックしてください。同様にプログラムが動いて、ポップアップでメッセージが表示されたと思います。

このように、ブックマークの中にプログラム(Javascript)を記録して、クリックして実行させる方法を「ブックマークレット」といいます。

ブックマークレットのサンプル

試しに作ってみました。

ページの表示を崩すブックマークレット

ページの表示を崩す

これをブックマークして、好きなページで実行することでページの表示を崩すことができます。

元に戻すにはページをリロードしてください。

スクリプトの内容を書いておきます

var divs = document.getElementsByTagName('div');
var mode = Math.floor(Math.random() * 2);
for (var i = 0; i < divs.length; i++){
  var delay = Math.random() * 1000;
  var d = divs[i];
  setTimeout(function(d) {
   if (d){
    var t = Math.floor(Math.random() * 2000 + 1000);
    d.style.transition = "all " + t +"ms ease-in";
    var deg = Math.random() * 30 - 15;
    d.style.transform = "translate(0, 800px) rotate(" + deg + "deg)";
  }
  }, delay, d);
}

フォームの全 Input の value に name を入れるブックマークレット

inputのvalueにnameを入れる

function d(inputs){
  for(var i =0; i<inputs.length; i++){
    inputs[i].value =inputs[i].name;
  }
}
d(document.getElementsByTagName("input"));
d(document.getElementsByTagName("textarea"));

「フォーム内容を復元するブックマークレット」を作るブックマークレット

フォーム内容を保存

実行すると、ページ上にリンクが1つできます。そのリンクがブックマークレットになっているので、ブックマークしてください。

次回、同じフォームを表示した時にそのブックマークレットを実行することで内容を再度入力することができます。

var out = "javascript:function sC(n,v,c){var elms=document.getElementsByTagName('input');for(ind in elms){o=elms[ind];if(o.name==n && o.value==v){o.checked=c;}}}function sI(n,v,c){var elms=document.getElementsByTagName('input');for(ind in elms){o=elms[ind];if(o.name==n){o.value=v;o.checked=c;}}}function sO(v){var elms=document.getElementsByTagName('option');for(ind in elms){o=elms[ind];if(o.value==v){o.selected='selected'}}}function sT(n,v){var elms=document.getElementsByTagName('textarea');for(ind in elms){o=elms[ind];if(o.name==n){o.value=v;}}}";

var elms, ind, o;
elms = document.getElementsByTagName('input');
for (ind in elms) {
    o = elms[ind];
    if (o.type == "hidden") {
        continue;
    } else if ((o.type == "checkbox" || o.type == "radio") && o.name && o.value) {
        out += "sC('" + o.name + "','" + encodeURIComponent(o.value) + "'," + o.checked + ");";
    } else if (o.name && o.value) {
        out += "sI('" + o.name + "','" + encodeURIComponent(o.value) + "');"
    }
}
elms = document.getElementsByTagName('option');
for (ind in elms) {
    o = elms[ind];
    if (o.selected) {
        out += "sO('" + o.value + "');"
    }
}
elms = document.getElementsByTagName('textarea');
for (ind in elms) {
    o = elms[ind];
    if (o.value) {
        out += "sT('" + o.name + "','" + encodeURIComponent(o.value.replace(/\s+/g, ' ')) + "');"
    }
}
out += "void(0);";
console.log(out);
var a = document.createElement('a');
a.href = out;
a.textContent = 'フォーム自動入力';
a.style.display = 'block';
a.style.position = 'fixed';
a.style.backgroundColor = '#eee';
a.style.padding = "40px";
a.style.top = "100px";
a.style.left = "100px";
a.style.zIndex = 1000;
a.style.border = "1px solid black";
document.body.appendChild(a);

ブックマークレットでできること

表示しているページの改変や操作が得意です。

ブラウザで表示しているページは HTML で出来ていて、ブックマークレットではその HTML のかなり多くの箇所を操作できるため、「表示しているページに対して何かを行う」ことに関しては得意です。

例えば、入力フォームが表示されている時、その入力フォームに対して何かを行う(自動入力など)ことについては他にないほど便利です。

複数ページにまたがった処理や、別のサイトのページに対しての処理は苦手です。

ページの遷移を行うと処理が止まってしまうため、複数ページの処理はあまり得意ではありません。特に、複数のサイトを同時に操る操作はブラウザの制約があるため難しいです。がんばればなんとかなりますが、他の手段を用いたほうが良いでしょう。Selenium とか Windows Scripting Host とか。

Javascript をブックマークレットに変換する方法

ページに対して何かを行う Javascript が書けた場合(思い浮かんだ場合)、それをブックマークレットの形式にするには少しの変換が必要です。

  • 改行や余計なスペースを除去する
  • 記号を URL エンコードする
  • 先頭に「javascript:」をつける
  • 最後に void(0) をつける、もしくは全体を void() で囲む、など

(一番最後のは、処理が undefined で評価されないとページ遷移が発生して、真っ白いページが表示されるなど変な結果になってしまうため入れてます)。

短いスクリプトであれば手で修正できますが、長いスクリプト、特にインデントが入っているものなんかは手で変換するのは大変です。

変換スクリプトを書いて github pages で上げておきましたので、よかったら使ってください。

Bookmarklet スクリプト変換

PyCharm + Docker で Django 開発環境を構築する

PyCharm 2016 の Docker サポートの使い勝手が良いです。

Docker で開発環境を作るのは初めてだったのですが、

  • ローカル ( mac ) は Docker Toolbox を使って開発環境を作る。PyCharm のデバッガでステップ実行できるようにする。
  • ソースコード、およびログディレクトリは Docker イメージの中に入れず、ホストのディレクトリをマウントして使う
  • 公開サーバを見越して uwsgi, nginx の設定も作っておく


という所までできたので、手順を書きます。

コード置いてあります。 https://github.com/ytyng/docker-django-skeleton

こちらを参考にしました。https://github.com/dockerfiles/django-uwsgi-nginx

必要アプリのインストール

インストーラに従ってインストールしておきます。

プロジェクトディレクトリを作成し、PyCharm で開く

$ mkdir docker-django-skeleton

$ open -a PyCharm docker-django-skeleton

PyCharm用の Bash プラグインのインストール

メニューバーの PyCharm → Preferences → 検索: Plugins → Plugins の検索で「BashSupport」を検索 → 該当したものをインストールしておきます。

PyCharmの再起動を求められるので、案内通り再起動します。

Docker 用のファイルを作る

プロジェクトルートに Dockerfile そのまま置くと煩雑になってしまうので、docker ディレクトリを作って関連ファイルを入れます。

docker/Dockerfile

FROM ubuntu:16.04

MAINTAINER ytyng

RUN apt-get update && apt-get install -y \
    python3 \
    python3-pip \
    python3-dev \
    python3-setuptools \
    libjpeg-dev \
    nginx \
    supervisor \
  && rm -rf /var/lib/apt/lists/*

# ipython, gnureadline が必要なら
# lib32ncurses5-dev

RUN ln -s /usr/lib/x86_64-linux-gnu/libjpeg.so /usr/lib/libjpeg.so

# RUN easy_install3 pip  # aptで入れた方が安定

COPY requirements/base.txt /tmp/requirements/base.txt
RUN pip3 install -r /tmp/requirements/base.txt

# setup all the configfiles
RUN echo "daemon off;" >> /etc/nginx/nginx.conf
COPY nginx-app.conf /etc/nginx/sites-available/default
COPY supervisor-app.conf /etc/supervisor/conf.d/

RUN mkdir -p /etc/nginx/certs
# COPY certs /etc/nginx/certs/

RUN mkdir -p /var/run/django/

# set locale
RUN locale-gen en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8

EXPOSE 80
#EXPOSE 443

↑ libjpeg などインストールしてるのは Pillow 用

docker/requirements/base.txt

Django==1.10.1
pytz
uwsgi
# MySQL使う場合
PyMySQL

↑ 今回のサンプルは、DBは SQLite3 なので PyMySQL 不要ですがサンプルとして書いてます。



docker/nginx-app.conf

server {
    listen 80;
    # listen 443 ssl;

    server_name docker-django-skeleton.example.com;

    charset utf-8;

    # ssl_certificate /etc/nginx/certs/xxx.crt;
    # ssl_certificate_key /etc/nginx/certs/xxx.key;

    location /static/ {
        alias /var/django/docker-django-skeleton/staticfiles/;
        break;
    }

    location / {
        include uwsgi_params;
        uwsgi_pass 127.0.0.1:3031;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

docker/supervisor-app.conf

[program:app-uwsgi]
command = /usr/local/bin/uwsgi --ini /var/django/docker-django-skeleton/conf/uwsgi.ini

[program:nginx-app]
command = /usr/sbin/nginx

Docker 用の設定ファイルの作成

環境変数など設定する Bash スクリプトを用意しておきます。
mac で実行した場合と公開サーバで起動した場合に読み込まれる設定を変えるようにしています。

docker/_settings.sh

#!/usr/bin/env bash

APP_NAME=docker-django-skeleton
DJANGO_APP_NAME=docker_django_skeleton

PROJECT_DIR=$(cd $(dirname ${BASH_SOURCE:-$0})/..;pwd)

if [ $(which docker-machine) ]; then
    # mac
    eval "$(docker-machine env default)"
    DOCKER_COMMAND=docker
    DOCKER_MACHINE_COMMAND=docker-machine
    LOG_DIR=/tmp/log/${APP_NAME}
    export UWSGI_PROCESSES=1
    export UWSGI_THREADS=1
    export DJANGO_SETTINGS_MODULE=${DJANGO_APP_NAME}.settings.local
else
    # ubuntu
    DOCKER_COMMAND="sudo docker"
    LOG_DIR=/var/log/${APP_NAME}
    OPTIONAL_PARAMS="--link mysql:mysql"
    RUN_COMMAND="supervisord -n"
    export UWSGI_PROCESSES=2
    export UWSGI_THREADS=2
    export DJANGO_SETTINGS_MODULE=${DJANGO_APP_NAME}.settings.production
fi


Dockerイメージ作成用スクリプト

docker/docker-01-build.sh

#!/usr/bin/env bash
cd $(dirname ${BASH_SOURCE:-$0})
. _settings.sh

${DOCKER_COMMAND} build -t ${APP_NAME} .

docker/docker-02-run.sh

#!/usr/bin/env bash
cd $(dirname ${BASH_SOURCE:-$0})
. _settings.sh

mkdir -p -m 777 ${LOG_DIR}

${DOCKER_COMMAND} run -d -P -p 80:80 -p 443:443 \
  -v ${PROJECT_DIR}:/var/django/${APP_NAME} \
  -v ${LOG_DIR}:/var/log/django \
  -v /etc/localtime:/etc/localtime:ro \
  -e DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} \
  -e UWSGI_PROCESSES=${UWSGI_PROCESSES} \
  -e UWSGI_THREADS=${UWSGI_THREADS} \
  ${OPTIONAL_PARAMS} \
  --name ${APP_NAME} ${APP_NAME} ${RUN_COMMAND}

if [ "${DOCKER_MACHINE_COMMAND}" ]; then
    docker-machine ip default
fi

${DOCKER_COMMAND} ps

docker/docker-03-bash.sh

#!/usr/bin/env bash
cd $(dirname ${BASH_SOURCE:-$0})
. _settings.sh

EXISTS=$(${DOCKER_COMMAND} ps --quiet --filter name=${APP_NAME})

if [ "${EXISTS}" ]; then
    ${DOCKER_COMMAND} exec -it ${APP_NAME} /bin/bash $@
else
    ${DOCKER_COMMAND} run --rm -it \
    -e DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} \
    -v ${PROJECT_DIR}:/var/django/${APP_NAME} \
    --name ${APP_NAME} ${APP_NAME} /bin/bash $@
fi

docker/docker-09-rm.sh

#!/usr/bin/env bash
cd $(dirname ${BASH_SOURCE:-$0})
. _settings.sh

${DOCKER_COMMAND} rm --force ${APP_NAME}

docker/manage.sh

#!/usr/bin/env bash
cd $(dirname ${BASH_SOURCE:-$0})
. _settings.sh

EXISTS=$(${DOCKER_COMMAND} ps --quiet --filter name=${APP_NAME})

if [ "${EXISTS}" ]; then
    ${DOCKER_COMMAND} exec -it ${APP_NAME} \
    /bin/bash -c "cd /var/django/${APP_NAME}/${APP_NAME}/; ./manage.py $*"

else
    ${DOCKER_COMMAND} run --rm -it \
    -e DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} \
    -v ${PROJECT_DIR}:/var/django/${APP_NAME} \
    --name ${APP_NAME} ${APP_NAME} /bin/bash -c \
    "cd /var/django/${APP_NAME}/${APP_NAME}/; ./manage.py $*"
fi


*.sh には、実行パーミッションを付与しておきます。

$ chmod +x docker-*.sh manage.sh

docker-django-skeleton
└── docker
    ├── Dockerfile
    ├── _settings.sh
    ├── docker-01-build.sh
    ├── docker-02-run.sh
    ├── docker-03-bash.sh
    ├── docker-09-rm.sh
    ├── manage.sh
    ├── nginx-app.conf
    ├── requirements
    │   └── base.txt
    └── supervisor-app.conf


Docker イメージの作成

先ほど作成した、docker-01-build.sh を PyCharm 上で右クリック → Run 'docker-01-build.sh'


Dockerイメージができます。

Django プロジェクトディレクトリの作成

既に他の件で Django が mac にインストールされている場合は、それを使って `django-admin.py startproject` すれば良いと思いますが、今回は先ほど Docker イメージにインストールした Django を使って Django プロジェクトを作成します。

プロジェクトディレクトリをまるごと Docker にマウントし、Docker内で startproject します。

$ docker run -it -v `pwd`:/var/django/docker-django-skeleton docker-django-skeleton /bin/bash

※ pwd は、docker-django-skeleton ディレクトリ。docker ディレクトリの親ディレクトリ

$ docker run -it -v `pwd`:/var/django/docker-django-skeleton docker-django-skeleton /bin/bash
root@8d2ba7a54287:/# cd /var/django/docker-django-skeleton/
root@8d2ba7a54287:/var/django/docker-django-skeleton# django-admin.py startproject docker_django_skeleton
root@8d2ba7a54287:/var/django/docker-django-skeleton# mv docker_django_skeleton docker-django-skeleton
root@8d2ba7a54287:/var/django/docker-django-skeleton#


好みの問題ですが、Django プロジェクトのディレクトリ名は アンダーバーではなく ハイフンに変更しています。

PyCharm を見てみると、docker-django-skeleton ディレクトリが作成されているのが確認できます。

settings を環境ごとに分ける

Django でよくやるプラクティスで、settings を ローカル環境と公開環境で分けるというものがあります。

docker_django_skeleton ディレクトリ内で、

1. settings ディレクトリを作成
2. settings.py は、 settings/base.py に改名
3. settings/local.py settings/production.py を作成。


settings/local.py, settings/production.py の内容は以下のようになります。

settings/local.py

from .base import *  # NOQA


内容はありませんが、今回はこれで良しとします。

manage.py のインタプリタ (シェバン shebang ) を python3 にしておく


作成された manage.py を見てみると、1行目が

manage.py

#!/usr/bin/env python

となっていますが、Docker イメージ内には Python2 が残っているため不具合が起こることがあります。

#!/usr/bin/env python3

に変更しておきます。

PyCharm のインタプリタで Docker の Python を指定する

PyCharm → Preferences → 検索: interpreter

Project Interpreter が出てきます。

ここで、右上の「...」ボタンをクリックし、Add Remote を選択。

Configure Remote Python Interpreter になるので、
「Docker」を選択、Image name は docker-django-skeleton:latest を選択してください。

それと、Python interpreter path: は python となっていますが、python3 に変更します。

すると、Docker 内の Python 環境を認識します。すごい!

Djangoサポートを有効にする

Preferences で、検索: django

Language & Frameworks の Django が出て来るので選択し、Enable Django Support にチェック。

Django project root: manage.py のある、docker-django-skeleton
Settings: docker_django_skeleton/settings/local.py

OKをクリック

Project Structure を設定する

Preferences で、検索: structure

manage.py のある、docker-django-skeleton ディレクトリを選択し、Source をクリック → OK

migrate

次は、./manage.py migrate をします。
docker コンテナ内なので、./manage.py を実行するのも少し面倒ですが、docker ディレクトリの中に manage.sh を作ってあるのでこれを使います。

$ cd docker
$ ./manage.sh

Type 'manage.py help <subcommand>' for help on a specific subcommand.

Available subcommands:

[auth]
    changepassword
    createsuperuser

[django]
    check
    compilemessages
...

$ ./manage.sh migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
...

Docker の Django環境を使ってマイグレーションしてくれました。

テストサーバの起動

PyCharm の右上、Run/Debug の選択をクリックし、Edit Configurations...

Run/Debug Configurations ダイアログで、左上の + をクリック

Django server を選択

Host: には、0.0.0.0 を入力(重要)
Name: は適当に (例: runserver )
→ OK

出来た runserver 実行環境が選択されている状態で、虫(デバッグ) ボタンを押下

System check identified no issues (0 silenced).
September 26, 2016 - 09:54:15
Django version 1.10.1, using settings 'docker_django_skeleton.settings.local'
Starting development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C.

となりますので、0.0.0.0:8000 をクリックするとブラウザが起動し、Docker マシーンにリダイレクトされてページが表示されます。
デバッガも有効な状態ですので、ブレイクポイントが使えます。すごい!

公開環境で起動する

uwsgi.ini

[uwsgi]

app = $(APP_NAME)
django_app = $(DJANGO_APP_NAME)

base = /var/django/%(app)/%(app)/
pythonpath = %(base)
chdir = %(base)

socket = 127.0.0.1:3031

module = %(django_app).wsgi:application

processes = $(UWSGI_PROCESSES)
threads = $(UWSGI_THREADS)

master = true

pidfile = /var/run/django/%(app).pid

touch-reload = %(base)/%(django_app)/wsgi.py
lazy-apps = true
thunder-lock = true

buffer-size = 32768

logto = /var/log/django/%(app)/%n.log

uwsgi の設定ファイルはこんな感じですかね。

ソースコードを公開環境のサーバに持っていって、

$ ./docker-01-build.sh

でコンテナを作り (ローカルで作ったのを Docker hub に push -> 本番で Pull でもいいですが)、

$ docker-02-run.sh

で起動すると、公開環境 (というか docker-machine がインストールされてない環境 ) では本番用設定で nginx → uwsgi → Django で起動します。

Docker コンテナを削除する時は

$ docker-09-rm.sh

で消えます。

Djangoプロジェクト間を OAuth2 連携する

ローカル環境に Django プロジェクトを2つ作り、OAuth2で(ダミーの)プロフィール情報を取得するまで書きます。

2つの Django プロジェクトを作ります。test_provider, test_consumer です。

OAuth2の認証フローは、サーバ間通信のため「Authorization Code」形式で行います。

OAuth2についての情報は IPAのページ などにあります

ソースコードを github に上げました。

Django プロジェクト 概要

test_provider

起動ポート 8000
ユーザーのIDとハッシュ化パスワードを保持します。
OAuth2 プロバイダを提供します。
ユーザーに test_provider への情報提供の認可を問います。
django-oauth-toolkit を使います

test_consumer

起動ポート 8001
ユーザーが最初にアクセスする Webページ
ユーザーに test_provider への認可ページへ誘導します。
ユーザー認可後は、このプログラムが test_provider へリクエストを行い、情報をもらいます。
django-allauth を使います。

※なお、django-allauth と同じくらい使いやすいのが python-social-auth です。Django のライブラリも含んでいます。

django-social-auth というライブラリも見かけると思いますが、Python3に対応していないのでやめておきましょう。

OAuth2プロバイダの開発

django-oauth-toolkit の素晴らしいドキュメントがここにあります。
「Make a Provider in a Minute」という挑発的なタイトルですが、
認証モデルにこだわりがなければほんとにすぐ作れます。
がんばっても1分では作れないと思いますが…。

Django プロジェクトの作成

$ mkdir test-oauth
$ cd test-oauth

$ django-admin.py startproject test_provider
$ pip install django-oauth-toolkit django-cors-headers

$ cd test_provider

test_provider/settings.py の編集

INSTALLED_APPS に追加

    'oauth2_provider',
    'corsheaders',

MIDDLEWARE_CLASSES に追加

'corsheaders.middleware.CorsMiddleware',

末尾に追加

CORS_ORIGIN_ALLOW_ALL = True

test_provider/urls.py の編集

from django.conf.urls import url, include
from django.contrib import admin
from .views import profile_view

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
    url(r'^api/profile/$', profile_view, name='profile'),
    url(r'^accounts/login/', 'django.contrib.auth.views.login', name='login',
        kwargs={'template_name': 'admin/login.html'}),
]

test_provider/views.py の作成

from django.http import JsonResponse
from oauth2_provider.views.generic import ProtectedResourceView

class ProfileView(ProtectedResourceView):
    def get(self, request, **kwargs):
        user = request.resource_owner

        return JsonResponse({
            'user_id': user.id,
            'email': user.email,
            'date_joined': user.date_joined,
            'secret_message': 'The quick brown fox',
        })

profile_view = ProfileView.as_view()

DB に反映

$ ./manage.py migrate

Adminユーザーの作成

OAuthプロバイダを追加設定するためには、Adminユーザーが必要なため作っておきます。

$ ./manage.py createsuperuser

テストサーバの起動

$ ./manage.py runserver 8000

OAuthプロバイダの設定

プロバイダの設定ページにアクセスするために、一度 Admin サイトにログインしておきます。

http://127.0.0.1:8000/admin/

Admin サイトにログイン後、アプリ追加ページを開きます。

http://127.0.0.1:8000/o/applications/

Click here をクリックします。

Register a new application ページとなるので、

Name: test-consumer (何でも良い)
Client type: Confidential
Authorization grant type: Authorization code
Redirect urls:
http://localhost:8001/accounts/testprovider/login/callback/
http://127.0.0.1:8001/accounts/testprovider/login/callback/

このように入力し、saveします。

作成した OAuthプロバイダは、このツールで簡単なテストができます。

client_id に先ほどの client_id をコピペし、
Authorization url は http://localhost:8000/o/authorize/ を入れればなんとなくのテストができます。

テストユーザーの作成

Admin から、テストユーザーを作っておきます。

http://127.0.0.1:8000/admin/auth/user/

ADD USER をクリックし、適当なユーザーを作っておいてください。
OAuth2 でのログイン時、このユーザーID/パスワードでログインを行うことになります。

引き続き、OAuthコンシューマの開発を行います。

起動中の Django テストサーバは、そのまま起動させっぱなしにしておいてください。

OAuth2コンシューマテストアプリの作成

Django プロジェクトの作成

$ cd test-oauth

$ django-admin.py startproject test_consumer
$ pip install django-allauth

$ cd test_consumer

ここでは、OAuth クライアントライブラリ django-allauth をインストールしています。

django-allauth は多彩はクライアント接続に対応した、多機能なライブラリで、django1.9 + Python 3.5 でも動きますので使い勝手が良いです。

django-allauth のドキュメントはここ

設定

http://django-allauth.readthedocs.io/en/latest/installation.html

test_consumer/settings.py

TEMPLATES の OPTIONS の context_processors に追加

'django.template.context_processors.request',

Django 1.9 なら、デフォルトで入ってると思います。

INSTALLED_APPS に追加

    'django.contrib.sites',
    'test_consumer',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.google',
    'allauth.socialaccount.providers.twitter',
    'testprovider',  # これから作る

allauth.socialaccount.providers.* は任意に選んで追加してください。
テストのために google と twitter を追加しています。

どんなプロバイダが準備されているかは公式ドキュメントに書いてあります。

http://django-allauth.readthedocs.io/en/latest/installation.html

末尾に追加

AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
    'allauth.account.auth_backends.AuthenticationBackend',
)

SITE_ID = 1
SESSION_COOKIE_NAME = 'test-consumer-session-id'

test_consumer/urls.py

from django.conf.urls import url, include
from django.contrib import admin
from django.views.generic import TemplateView

urlpatterns = [
    url(r'^admin/', admin.site.urls),

    url(r'^$', TemplateView.as_view(template_name='index.html')),
    url(r'^accounts/', include('allauth.urls')),

]

テンプレートの作成

$ mkdir test_consumer/templates
test_consumer/templates/index.html を作成

{% load socialaccount %}
{% load account %}

<html>
<head>
<meta charset="utf-8" />
</head>
<body>

<a href="{% provider_login_url "twitter" %}">Twitterでログイン</a><br />
<a href="{% provider_login_url "google" %}">Googleでログイン</a><br />
<a href="{% provider_login_url "testprovider" %}">Test Provider でログイン</a><br />

<hr />
{% if user.is_authenticated %}
  ログインユーザー: {% user_display user %}<br />
  {% for sa in user.socialaccount_set.all %}
    {{ sa.extra_data }}<br />
  {% endfor %}
{% endif %}
</body>
</html>

testprovider アダプタの作成

allauth に入っている、google のアダプタを改修して作るのが最も手軽でしょう。

allauth/socialaccount/providers/google このディレクトリをまるごとコピーし、
testprovider としてペーストします。 ( manage.py がいるディレクトリに)

内容は以下のようになります。

testprovider/provider.py

from allauth.socialaccount import providers
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider

class TestAccount(ProviderAccount):

    def to_str(self):
        dflt = super(TestAccount, self).to_str()
        return self.account.extra_data.get('name', dflt)

class TestProvider(OAuth2Provider):
    id = 'testprovider'
    name = 'Test Provider'
    account_class = TestAccount

    def get_default_scope(self):
        return ['read', 'write']

    def get_site(self):
        settings = self.get_settings()
        return settings.get('SITE', 'testprovider')

    def extract_uid(self, data):
        uid = str(data['user_id'])
        return uid

    def extract_common_fields(self, data):
        return dict(username=data.get('email', 'no name'))

providers.registry.register(TestProvider)

testprovider/urls.py

from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
from .provider import TestProvider

urlpatterns = default_urlpatterns(TestProvider)

testprovider/views.py

import requests

from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter,
                                                          OAuth2LoginView,
                                                          OAuth2CallbackView)
from allauth.socialaccount.providers import registry

from .provider import TestProvider

from django.conf import settings

server_url_prefix = getattr(
    settings, 'TEST_PROVIDER_URL_PREFIX',
    'http://127.0.0.1:8000')

class TestOAuth2Adapter(OAuth2Adapter):
    provider_id = TestProvider.id
    access_token_url = server_url_prefix + '/o/token/'
    authorize_url = server_url_prefix + '/o/authorize/'
    profile_url = server_url_prefix + '/api/profile/'

    def complete_login(self, request, app, token, **kwargs):
        provider = registry.by_id(app.provider)
        resp = requests.get(self.profile_url,
                            params={'access_token': token.token})

        extra_data = resp.json()
        return self.get_provider().sociallogin_from_response(
            request, extra_data)

oauth2_login = OAuth2LoginView.adapter_view(TestOAuth2Adapter)
oauth2_callback = OAuth2CallbackView.adapter_view(TestOAuth2Adapter)

DBのマイグレーション

$ ./manage.py migrate

Adminユーザーの作成

$ ./manage.py createsuperuser

テストサーバの起動

$ ./manage.py runserver 8001

アプリケーションの登録

http://127.0.0.1:8001/admin/

Admin サイトにログイン後、SOCIAL ACCOUNTS の Social applications の +Add をクリックします。

Provider: Test Provider
Name: Test Provider
Client id: 先ほどの 8000 admin で作った Client id
Secret key: 先ほどの 8000 admin で作った Secret key
Key: 空
Sites: example.com を選択

これで、Save します。

Google, Twitter など他サービスへのログインも必要であれば、ここから追加できます。

クライアントとしての動作テスト

先ほど、8000ポートにログインしたブラウザとは別のブラウザを使います。(ログアウト状態になっているため)

http://127.0.0.1:8001/

ここで、「Test Provider でログイン」をクリックします。

( AllAuth が自動的にログインフォームを提供してますので、このURLからでもいけます。/accounts/login/ )

そうすると、8000 ポートへ転送され、ログインフォームが出てきます。
今回は時間節約のため、Django Admin のログインフォームを流用していますが、
通常は自作のログインフォームをここで表示させることになると思います。

8000 (test_provider) で作ったテストユーザーでログインしてみましょう。

ログインが成功すると、認可を求めるページとなりますので、「Authorize」をクリック。
ちなみに、この認可を求めるページは、8000 (test_provider) の Admin の アプリ設定の、「Skip authorization」にチェックを入れると省略できます。

ログインに成功すると、8001 ポートに戻ってきます。

http://127.0.0.1:8001/accounts/profile/
が表示されますが、このページはまだ作っていませんので Not found になります。

情報取得は成功していますので、トップページのURLを手で入力して情報を見てみます。

http://127.0.0.1:8001/

トップページ下部に、API取得した extra_data を表示する箇所があります。
そこに、OAuth2 のプロバイダから取得した情報が表示されるのが確認できます。

OAuthプロバイダの /api/profile/ から取得した 'secret_message': 'The quick brown fox' が、OAuthコンシューマで取得できているのが確認できます。

ソースコードは Github に上げました。

Search

Recent Tweets

  • ytyng

    ytyng @ytyng

    うちもです https://t.co/RuEZET6joq
    1 month, 3 weeks ago

  • ytyng

    ytyng @ytyng

    『進撃の巨人』が28巻まで無料で読める!連載10周年感謝企画を開催。 https://t.co/llKlURtYFP @PRTIMES_JPさんから
    3 months, 1 week ago

  • リュウジ@料理のおにいさんバズレシピ

    リュウジ@料理のおにいさんバズレシピ @ore825

    ytyng

    スキマさんにて、67倍くらい美化された僕が料理で数々の難事件を解決するハートフルクッキングマンガが連載になりました!! 記念すべき第一回のレシピは僕の代名詞とも言える 無限に食える「無限キャベツ」です! レシピこちら!! https://t.co/y5pDbH83aK
    3 months, 1 week ago

  • ytyng

    ytyng @ytyng

    キン肉マン 40巻無料読み放題 https://t.co/UINciSV10K #漫画全巻ドットコム
    6 months ago

  • 尚月地『艶漢』公式ツイッター

    尚月地『艶漢』公式ツイッター @AdekanOfficial

    ytyng

    (リプライズ)祝祝祝・池袋虜グランドオープン!! https://t.co/JUt8qcuPIX
    8 months, 1 week ago