Top View


Author Yuhei Okazaki

サーバーレスな社内Gyazoの作り方 [AWS SAM+Api Gateway+Lambda(Ruby)+S3]

2019/02/14

きっかけ

弊社テストエンジニアの吉武からこんな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でやるべきことはそこまで多くありません。

  1. リクエストに含まれる画像ファイルを取り出す(必要があればデコードする)
  2. URL(≒ファイル名)に付与するランダムな文字列を生成
  3. S3にファイルを格納する
  4. 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点を改修しました。

  1. リクエスト先をAPI Gatewayのエンドポイントに変更する
  2. リクエストの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のクライアントにも対応したいですね。

まとめ

ちょっと持ち上げすぎな気がしますが、喜んでもらえたようです。
めでたし。めでたし。

Yuhei Okazaki

Yuhei Okazaki

Twitter X

2018年の年明けに組込み畑からやってきた、2児の父 兼 Webエンジニアです。 mockmockの開発・運用を担当しており、組込みエンジニア時代の経験を活かしてデバイスをプログラミングしたり、簡易的なIoTシステムを作ったりしています。主な開発言語はRuby、時々Go。