Gyazo + Amazon CloudFront で簡単☆高速スクリーンショット共有
個人用の Gyazo 環境向けに、クライアントに手を入れて直接 Dropbox や S3 へスクリーンショットをアップロードするというテクニックがある。自前サーバを管理することなく利用できて大変ナイスなものの、クライアント側で比較的重要な固有情報を含んだ状態になっているので、取り扱いが若干難しいと思った。
そこで AWS Lambda を利用して、S3 へのアップロードを行う署名付き URL の発行をそちらで行い、クライアントでは受け取った情報だけでアップロード処理が進めれるような体制を作ってみた。
処理の流れと役割
Gyazo
スクリーンショットを撮る。
通常ならば Gyazo サーバへスクリーンショットをアップロードするところを、後述する AWS Lambda へ S3 アップロードのための署名付き URL を発行するようリクエストを送る。その後、署名付き URL を使って S3 へスクリーンショットをアップロードする。
Amazon S3
Gyazo スクリーンショットを保存すると共に参照リクエストされたデータを表示する。
AWS Lambda
aws-sdk を使って S3 へのファイルアップロードのための署名付き URL を発行する。
Amazon API Gateway
上記の Lambda Function を HTTPS 経由でやりとりするためのインターフェース。
構築、カスタマイズ手順
Amazon S3
画像を格納するバケットを作成する。
Gyazo はスクリーンショットを撮った次の瞬間にブラウザでその画像が表示されるのが軽快で素晴らしく、それは同等の動きになって欲しい。そこでこのバケット内オブジェクトを公開状態にするべくバケットポリシーを設定する。 (BUCKET_NAME
の部分は実際の S3 バケット名を記述)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1462499000000",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::BUCKET_NAME/*"
}
]
}
AWS Identity and Access Management (IAM)
IAM で Lambda 用のロールを作る。ここでは S3 への PutObject 操作と、Lambda 自身のログの書き出し権限を付与する。(BUCKET_NAME
の部分は実際の S3 バケット名を記述)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::BUCKET_NAME",
"arn:aws:s3:::BUCKET_NAME/*"
]
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
AWS Lambda
aws-sdk を使って S3 への PutObject を行うことができる署名付き URL を発行するコードを用意する。
OSSとして公開されている Gyazo のサーバの振る舞いを見てみると、画像データを受け取り MD5 ハッシュダイジェストを算出してファイル名を決定したりしているが、今回の Lambda のコードでは画像データ自体は受け取らず必要データを Gyazo クライアント側から受け取って処理をする形式にした。したがって元々サーバ側でやっていた処理の一部が Gyazo クライアントへ移ることになる。
実際のコード中身は以下のとおり。(BUCKET_NAME
の部分は実際の S3 バケット名を記述)
ファイルのハッシュダイジェストと Content-Type を受け取り内容を検証、その内容に基づいて S3 の署名付き URL を発行して返却している。
余分ながら、PNG, GIFといった画像データを扱っているので、せっかくなのでJPEG画像も受け入れ可能にしておいた。
var aws = require('aws-sdk');
var s3 = new aws.S3();
var BUCKET = "BUCKET_NAME";
var URL_PREFIX = 'https://s3-ap-northeast-1.amazonaws.com/' + BUCKET + '/';
exports.handler = function (event, context, callback) {
var hash = event.hash;
var contentType = event.contentType;
if (!hash) {
callback('no set a file hash digest.');
return;
}
if (!contentType) {
callback('no set content type.');
return;
}
var key;
if (contentType.match(/(\.|\/)png$/i)) {
key = hash + ".png";
} else if (contentType.match(/(\.|\/)gif$/i)) {
key = hash + ".gif";
} else if (contentType.match(/(\.|\/)jpe?g$/i)) {
key = hash + ".jpg";
} else {
callback('invalid content type (gif, jpg, or png)');
return;
}
// console.log('Content-Type: ' + contentType);
var params = {
Bucket: BUCKET,
Key: key,
Body: '',
ContentType: contentType,
Expires: 60
};
s3.getSignedUrl('putObject', params, function (err, url) {
if (err) {
callback(err);
return;
}
// console.log('Signed-URL: ', url);
callback(null, {
'signedUrl': url,
'imageUrl': URL_PREFIX + key
});
});
};
作成した Lambda function の設定画面から「API endpoints」をタブを開いて API Gateway 経由でこの function が実行されるようにする。作成後に API Gateway 側をチェックして、この function のエンドポイント URL をチェックする。
Gyazo クライアント
Gyazo クライアントとして振る舞いは、OSX 版の場合 Gyazo.app/Contents/Resources/script
に ruby コードとして記述されているので、これを AWS 向けの振る舞いとなるようコードを書き換える。(元の Gyazo.app を別名にコピーした上で別アプリとして動くように)
まずはコードの先頭で、追加で必要になるモジュールを読み込むように。
require 'uri'
require 'mime/types'
require 'digest/md5'
続けて AWS 向けの新しい振る舞いを関数として追加する。
- 署名付きURLの発行リクエスト
- S3へのファイルアップロードリクエスト
まずは署名付き URL を発行する関数。ここでは API Gateway を通じて Lambda へリクエストすることができるエントリポイント URL を設定する必要がある。
@resource = "https://example.execute-api.ap-northeast-1.amazonaws.com/prod/s3_put_url"
def create_upload_url(hash, content_type)
uri = URI.parse(@resource)
use_ssl = uri.scheme == 'https' ? true : false
begin
res = Net::HTTP.start(uri.host, uri.port, :use_ssl => use_ssl) do |http|
body = {
"hash" => hash,
"contentType" => content_type,
}.to_json
header = {
"Content-Type" => "application/json"
}
http.open_timeout = 10
http.read_timeout = 10
http.post(uri.request_uri, body, header)
end
case res
when Net::HTTPSuccess
JSON.parse(res.body)
else
abort("Failed to upload url creation.\n#{res.code}: #{res.message}")
end
rescue => e
abort("Failed to upload url creation.\n#{e}")
nil
end
end
もう一つの S3 へのアップロードを行う関数。発行された署名付き URL を渡して処理を行うので、S3 に関する設定はここでは登場しない。
def upload_file(url, data, content_type)
uri = URI.parse(url)
use_ssl = uri.scheme == 'https' ? true : false
begin
res = Net::HTTP.start(uri.host, uri.port, :use_ssl => use_ssl) do |http|
req = Net::HTTP::Put.new(uri.request_uri)
req["Content-Type"] = content_type
req.body = data
http.request(req)
end
case res
when Net::HTTPSuccess
res
else
abort("Failed to upload.\n#{res.code}: #{res.message}")
end
rescue => e
abort("Failed to upload.\n#{e}")
nil
end
end
最後に、元々のアップロード処理を行っている箇所のコード以下を、下記のように前述関数を利用するコードへ書き換える。
# upload
hash = Digest::MD5.hexdigest(imagedata)
content_type = "image/png"
urls = create_upload_url(hash, content_type)
res = upload_file(urls["signedUrl"], imagedata, content_type)
if res.code.to_i >= 400
abort("Failed to upload.\n#{res.code}: #{res.message}")
end
IO.popen("pbcopy","r+"){|io|
io.write urls["imageUrl"]
io.close
}
system "open #{urls["imageUrl"]}"
以上で完了。
カスタマイズ版 Gyazo.app を使ってスクリーンショットを撮ってみると、ブラウザ上で表示される画像やクリップボードへコピーされる URL は S3 へアップされたものになっているはず。
まとめ
Gyazo のデータを格納する流れをカスタマイズして、AWS Lambda で S3 アップロードのための署名付き URL を発行してスクリーンショットを格納する仕組みを紹介した。
この仕組みではクライアント(= Gyazo クライアント)に AWS のアクセスキー情報などが一切登場することがなく唯一 Lambda を叩く API Gateway エントリポイント URL が現れるのみなので、たとえばチームなどで利用する際にも安全にクライアントを配ることができる。