サーバーレスな社内Gyazoの作り方 [AWS SAM+Api Gateway+Lambda(Ruby)+S3]
2019/02/14
Table of Contents
きっかけ
弊社テストエンジニアの吉武からこんなDMが届いたことがきっかけです。
彼にはいつもテストで ご迷惑をかけて お世話になっているので、望みを叶えてあげることにしました。
Gyazoとは
オープンソースのスクリーンショットソフトウェアです。撮影したスクリーンショットが即時サーバーへアップロードされ、アクセスするためのURLが生成されます。
画像データをURL化することで、画像データを直接扱う場合に発生する、一覧化や整理の手間、GithubやBacklog等に貼り付ける際の手間といった問題を、解決してくれます。
サーバーレスで構築する意義
Gyazoのソースコードは Github に公開されていますが、これはCGIとして実装されています。これをそのまま利用する場合、以下のような懸念がありました。
- サーバーの運用・保守のコストが高い
- CGIを使うためだけに、Webサーバーのインストール・設定が必要
- アップロードした画像のバックアップ方法を検討する必要がある
- インターネット・クラウドを利用した機能拡張が難しい(slackbotとの連携等)
これらを解決すべくサーバーレスでGyazoと同等のシステムを構築することにしました。
要件
社内Gyazoに求める要件は以下の通り。
- システムのテストで問題が見つかったときに画面のスクリーンショットを取得する目的で使用する
- 取得したスクリーンショットはGithub issueやBacklogに貼り付ける。画像が展開されなくてもOK。
- アップロード・ダウンロードにはIP制限をかけたい
- Gyazoで公開されているクライアントを利用したい。とりあえずmacのみ対応すれば良い。
構成
インフラをAWS上に構築します。
Gyazoのクライアントからのリクエスト先としてAPI Gateway、リクエストの処理先としてLambdaを配置します。ファイルの格納先としてS3を準備します。
クライアントには画像のURLを返す必要があるので、S3の公開URLを返します。
構築
出来上がったソースコードは こちら で公開しています。
AWS SAMを利用したサーバーレス環境の構築
いわゆるサーバーレスの基本的な構成なので、AWS SAMを用いて構築しました。まずは、 aws-sam-cli
をインストールします。
$ brew install awscli
$ aws configure
$ brew install aws-sam-cli
これで sam
コマンドが使えるようになります。 sam init
コマンドで必要なファイル一式を自動生成します。
$ sam init --runtime ruby2.5
$ cd sam-app
$ bundle install
sam local
コマンドでAWSへデプロイすることなく、ローカル環境でLambda Functionの動作確認ができます。
$ sam local generate-event apigateway aws-proxy \> event\_file.json
$ sam local invoke HelloWorldFunction --event event\_file.json
※このあたりの手順は この記事 を参考にしました。
template.ymlに追記
AWS SAMを使う以上、清く正しくInfrastructure as Codeします。ポイントとしては以下の通りです。
- パブリックアクセス可能なS3のバケットを追加する
- Lambda FunctionによるバケットへのPUTを許可する
- API Gatewayのバイナリメディアタイプとして
image/png
を追加
template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: \>
sam-app
Sample SAM Template for sam-app
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Api:
BinaryMediaTypes:
- image~1png
Function:
Timeout: 3
Resources:
Bucket:
Type: AWS::S3::Bucket
Properties:
AccessControl: PublicRead
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: error.html
DeletionPolicy: Retain
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket:
Ref: Bucket
PolicyDocument:
Statement:
-
Action:
- "s3:GetObject"
Effect: "Allow"
Resource:
Fn::Join:
- ""
-
- "arn:aws:s3:::"
-
Ref: Bucket
- "/\*"
Principal: "\*"
GyazoUploadFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: app/
Handler: app.gyazo\_upload
Runtime: ruby2.5
Policies:
- AWSLambdaExecute # Managed Policy
-
Version: '2012-10-17' # Policy Document
Statement:
-
Effect: Allow
Action:
- s3:PutObject
Resource: !GetAtt Bucket.Arn
Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object
Variables:
REGION: !Ref "AWS::Region"
BUCKET\_NAME: !Ref Bucket
Events:
GyazoUpload:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Path: /upload
Method: post
Lambda Functionを作成
Lambda Functionでやるべきことはそこまで多くありません。
- リクエストに含まれる画像ファイルを取り出す(必要があればデコードする)
- URL(≒ファイル名)に付与するランダムな文字列を生成
- S3にファイルを格納する
- URLをレスポンスとして返す
ソースコードにすると以下の通り。わずか20行程度です。
app.rb
require 'json'
require 'aws-sdk-s3'
require 'base64'
require 'securerandom'
def gyazo\_upload(event:, context:)
body = event['body']
body = Base64.decode64(body) if event['isBase64Encoded']
key = SecureRandom.urlsafe\_base64
object = Aws::S3::Resource
.new(region:ENV['REGION'])
.bucket(ENV['BUCKET\_NAME'])
.put\_object({ key: "#{key}.png", content\_type: 'image/png', body: body })
{
statusCode: 200,
body: object.public\_url
}
end
クライアントを改修
Gyazoが公開しているクライアントをそのまま使えると最高!だったのですが、以下2点を改修しました。
- リクエスト先をAPI Gatewayのエンドポイントに変更する
- リクエストのContent-Typeを
multipart/form-data
からimage/png
に変更
1はhostsファイルで強引にリクエスト先を変えても良かったかもしれません。
2はLambda Functionで multipart/form-data
から画像ファイルを取り出すのが面倒になったので、素直に画像ファイルだけを送付するように修正しました。
ソースコードは以下。(ライセンス上の懸念があったのでこのソースはリポジトリに含めていません)
/Applications/Gyazo.app/Contents/Resources/script
##################################
# 元ファイル198行目付近
##################################
CGI = '/Prod/upload'
teams = nil
PORT = 443
# HOST = 'upload.gyazo.com'
# teams = (get\_info('CFBundleIdentifier') == 'com.gyazo.mac.teams')
# if teams
# CGI = '/teams/upload'
# else
# CGI = '/upload.cgi'
# end
# PORT = 443
##################################
# 元ファイル210行目付近
##################################
data = imagedata
# data = \<\<EOF
# --#{boundary}\r
# content-disposition: form-data; name="id"\r
# \r
# #{id}\r
# --#{boundary}\r
# content-disposition: form-data; name="metadata"\r
# \r
# #{metadata}\r
# --#{boundary}\r
# content-disposition: form-data; name="imagedata"; filename="gyazo.com"\r
# \r
# #{imagedata}\r
# --#{boundary}\r
# content-disposition: form-data; name="scale"\r
# \r
# #{scale}\r
# --#{boundary}--\r
# EOF
header = {
'Content-Length' =\> data.length.to\_s,
'Content-type' =\> "image/png",
# 'Content-type' =\> "multipart/form-data; boundary=#{boundary}",
'User-Agent' =\> UA,
}
IP制限をかける
以上の改修でGyazoとして利用できる状態にはなっていますが、社外からの画像アップロード・ダウンロードを禁止するために、IP制限を設定することにしました.
「清く正しくInfrastructure as Code」を貫きたかったのですが、SAMのテンプレートでAPI Gatewayのリソースポリシーを設定する方法がわからず、やむを得ず手動設定しました。
API Gatewayのリソースポリシー
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "\*",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:{{region}}:{{account\_id}}:{{resource\_id}}/\*",
"Condition": {
"IpAddress": {
"aws:SourceIp": "xxx.xxx.xxx.xxx/xx"
}
}
}
]
}
S3のバケットポリシー
{
"Version": "2008-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "\*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::{{バケット名}}/\*",
"Condition": {
"IpAddress": {
"aws:SourceIp": "xxx.xxx.xxx.xxx/xx"
}
}
}
]
}
デプロイ
README.mdに記載の通り、以下コマンドで簡単にデプロイできます。デプロイ用にS3のバケットを予め別に用意しておく必要があるので注意してください。
$ sam package \
--output-template-file packaged.yaml \
--s3-bucket REPLACE\_THIS\_WITH\_YOUR\_S3\_BUCKET\_NAME
$ sam deploy \
--template-file packaged.yaml \
--stack-name sam-app \
--capabilities CAPABILITY\_IAM
動作確認
このように撮影したスクリーンショットがブラウザに表示され、URLが付与されます。
将来的にやりたいこと
- IP制限をかけていることもあって、Github issueやSlackにこのURLを貼り付けても画像が展開されない問題があります。S3の一時URLを活用したり、slackbotを作るなどして、もっと便利に使えるようにしたいところです。
- Windowsのクライアントにも対応したいですね。
まとめ
ちょっと持ち上げすぎな気がしますが、喜んでもらえたようです。
めでたし。めでたし。