この記事の内容
この記事には以下の内容が含まれています。
- 作ったアプリの仕様の説明
- 画像にLGTM文字列を合成するAWS Lambdaのコード
- CloudFront->API Gateway->Lambda構成での設定
- お名前.comで取得したドメインをCloudFrontに適用する際に、ACMで証明書を取得する流れ
LGTM画像関連のサービス
LGTM画像を生成したり探したりするサービスは色々あります。
日々お世話になっていますが、私も簡単なものを作ってみました。
calgtm
何これ
GUIなしで、クエリパラメータに画像URLを指定するとLGTM画像にして返却するだけのサービスです。
以下のURLの${img_url}に画像URLを指定すると、LGTM画像となって返却されます。
http://calgtm.inabajun.work/?img=${img_url}
例
このURLが
http://example.com/cat.jpg
この画像だとすると、
このURLは
http://calgtm.inabajun.work/img=http://example.com/cat.jpg
こうなります。
- 今の所予定はないですが、理由なく突然サービスを停止する可能性があります。
- なので停止したら困る環境で利用する場合は別の環境にデプロイして使ってもらえればと思います。
- 変な挙動があったらGitHubのIssueか何かで教えてもらえるとありがたいです。
- GitHub
- 巨大な画像を指定するとダウンロード中にタイムアウトして、例外用のLGTM画像を返却します。
- その他本サービスからアクセスできない画像URLを指定した場合も例外用のLGTM画像を返却します。
仕組みとデプロイについて
以下はこのサービスの構成とコードとデプロイの設定です。
システム構成
全部AWSです。CloudFront->API Gateway->Lambdaです。
calgtmはCloudFront API Gateway Lambda GTMの略です。構成を変えても名前を変えるつもりはあんまりないです。あんまり考えてないです。
コード
Python3.6です。初めてのPythonでしたがPillowが便利そうだったのでPythonにしました。
import requests
from PIL import Image, ImageFont, ImageDraw
import urllib.request
import io
import base64
def lambda_handler(event, context):
img_url = event["queryStringParameters"]['img']
try:
img_binary = get_img(assemble_query_parameter(event["queryStringParameters"], img_url))
lgtm_img_binary = lgtm(img_binary)
return generate_response(200, {"Content-Type": "image/jpeg"}, True, base64.b64encode(lgtm_img_binary).decode('utf-8'))
except BaseException as err:
# when occur error, return default error lgtm image.
img_binary = io.BytesIO(open("error_image.jpg", "rb").read())
img = Image.open(img_binary)
output = io.BytesIO()
img.save(output, format='JPEG')
return generate_response(200, {"Content-Type": "image/jpeg"}, True, base64.b64encode(output.getvalue()).decode('utf-8'))
def generate_response(status_code, headers, is_base_64_encoded, body):
response = {
"statusCode": status_code,
"headers": headers,
"isBase64Encoded": is_base_64_encoded,
"body" : body
}
return response
# when img parameter's url has multiple query parameters, this can not be interpreted part of url.
# so must assemble img to original image url
def assemble_query_parameter(query_parameters, url):
for k, v in query_parameters.items():
if k != "img":
url = url + "&" + k + "=" + v
return url
def lgtm(img_binary, fillcolor="white", shadowcolor="black"):
img = Image.open(img_binary)
# convert to jpg
img.convert("RGB")
# compress for too big image
img.thumbnail((1024, 1024), Image.ANTIALIAS)
width, height = img.size
# adjust font size
font_size = width / 2 if width <= height else height / 2
draw = ImageDraw.Draw(img)
# calculate text size
font_name = "LiberationSans-Bold.ttf"
font = ImageFont.truetype(font_name, int(font_size))
text = "LGTM!"
text_w, text_h = draw.textsize(text, font)
# resize font size until text does not run off image.
while text_w > width or text_h > height:
font_size = font_size - 1
font = ImageFont.truetype(font_name, int(font_size))
text_w, text_h = draw.textsize(text, font)
# adjust place of text
x, y = (width - text_w)/2, (height - text_h)/2
# draw border
draw.text((x, y-1), text, font=font, fill=shadowcolor)
draw.text((x, y+1), text, font=font, fill=shadowcolor)
draw.text((x+1, y-1), text, font=font, fill=shadowcolor)
draw.text((x+1, y), text, font=font, fill=shadowcolor)
draw.text((x+1, y+1), text, font=font, fill=shadowcolor)
draw.text((x-1, y+1), text, font=font, fill=shadowcolor)
draw.text((x-1, y), text, font=font, fill=shadowcolor)
draw.text((x-1, y-1), text, font=font, fill=shadowcolor)
# draw main text
draw.text((x, y), text, font=font, fill=fillcolor)
# discarding the alpha channel. JPEGs can't represent an alpha channel.
if img.mode in ('RGBA', 'LA'):
background = Image.new(img.mode[:-1], img.size, "white")
background.paste(img, img.split()[-1])
img = background
output = io.BytesIO()
img.save(output, format='JPEG')
return output.getvalue()
def get_img(url):
req = urllib.request.Request(url)
image_read = urllib.request.urlopen(req, timeout=3).read()
img_binary = io.BytesIO(image_read)
return img_binary
こちらを特に参考にさせていただきました。
Pillowが使いやすかったので特筆すべき部分はあんまりないですが、ポイントは
- Lambda統合でバイナリデータを返却する際レスポンスの形式に制約があること
- パラメータとして受け取った画像URLが複数のクエリパラメータを持っている場合に、画像URLと画像URLが持つクエリパラメータが分離してしまうため、組み立てる必要があったこと
くらいでしょうか。
後者は例えば、画像URLとして https://example.com/?key1=value1&key2=value2 を渡すと、クエリパラメータが&で分断されるため、eventオブジェクトが
{
"queryStringParameters":{
"img" : "https://example.com/?key1=value1",
"key2" : "value2"
}
}
となってしまうため、assemble_query_parameter
で無理やりURLを組み立て直しているところです。
はっきり言って微妙だなと思いつつも、URLを入力する側に何かさせるのは嫌だったのでこうしました。今後拡張して何かパラメータを渡したくなった時どうすればいいのかが悩みどころです。
ビルド
こちらを参考にさせていただきました。
【Python3.6】AWS Lambdaを再現するDocker Imageのdocker-lambdaを使ってみた
というかそのままですが、
- AWS Lambdaで外部モジュールを利用する場合、zipにしてアップロードする必要がある
- PillowのビルドをローカルではなくLambdaの実行環境で行う必要がある
ため、Lambda関数を作成した場所にDockerfileを置いて以下でイメージを作成し、
docker build -t mylambda .
以降は
docker run -v "$PWD":/var/task mylambda
で、dockerを起動し、環境内でrequirements.txtに記載されたモジュールを取得してLambda関数と共にzipにしています。
デプロイ
Lambda
何それ
ざっくり言ってしまえば、サーバを維持せずにデプロイした関数を動かせるサービスです。
設定
特別なことはしていません。ランタイムをPython 3.6にして、上記ビルドコマンドで作成されたzipファイルをアップロードしました。
API Gateway
何それ
APIのエンドポイントを作成できるサービスです。バックエンドにAWSの各サービスや、HTTPのエンドポイントを指定できます。入力と出力のデータをAPI Gatewayだけである程度変換できたりします。あとはエンドポイントにいく前のバリデーションとか権限のチェックとか。
設定
そこそこはまりました。ポイントは、
の2点です。
詳しくは別の記事にまとめました。
API Gateway の Lambda プロキシ統合でバイナリデータをレスポンスする(Python3.6)
こちらを色々と参考にさせていただきました。
API Gatewayでサーバレスな画像リサイズAPIを作る
CloudFront
何それ
CDNです。バックエンドのレスポンスをキャッシュできます。あとバックエンドに渡すヘッダを設定したりできます。
CloudFrontを使った理由
API Gatewayの設定ができた時点でサービスとしては動くのですが、
- API Gatewayに指定したバイナリメディアタイプに一致するAcceptヘッダを指定してURLにアクセスしないと、返却されるデータがデコードされない(テキストで返ってきてしまう)
- 単純にキャッシュしたい。同じ画像に対してなんどもLambdaをキックしたくない。
ため、CloudFront経由でアクセスさせるようにしました。
設定
Origin Settings
Origin SettingsはOrigin Domain NameにAPI GatewayでデプロイしたエンドポイントのURLをコピペすると勝手に設定されます。あとはOrigin Protocol PolicyをHTTPS Onlyに指定しました。
Origin Custom Headerは上記の理由のため以下を指定します。
Default Cache Behavior Settings
頻繁にデータを更新する必要もない気がするので、デフォルトのTTLは長めに設定しました。
Distributions Settings
取得したドメインを使用する場合、Alternate Domain Namesにドメインを指定します。Custom SSL Certificateを選択するとRequest or Import a Certificate with ACM
ボタンが現れるので、そちらをクリックします。今回はACMで証明書を発行しています。
クリックすると以下の画面が表示されるので、ドメインを入力します。
検証方法を選択します。今回はDNSの検証を選択しました。
DNS設定をファイルにエクスポート
をクリックすると、設定値がダウンロードできるので、設定値に合わせてDNSにCNAMEレコードを追加します。
今回はお名前.comで取得したドメインを使ったので、お名前.comのコンソールからDNSにレコードを設定しました。
即座には完了しないようだったので一晩置いたところ、朝には発行済みとなっていました。
発行が完了すると、以下に発行した証明書を指定できます。
これでCloudFrontの配信を開始しある程度待つと、指定したドメインでデプロイしたサービスにアクセスできるようになりました。
デプロイ作業は以上となります。
今後やりたいこと
- インフラをコード化してコマンド一発でデプロイしたい
- SAMとか?ServerlessFrameworkとか?
- ACMもいけるのかな・・・
- SAMとか?ServerlessFrameworkとか?
- 多少パラメータを追加したい
- LGTM文字の色を変えるとか
- 単体テスト書きたい
デプロイした後で思ったこと
- 当初はFBにアップした写真とかをサクッとLGTM画像にして遊びたいという意図で作った
- 完成してちょっと触ってたらGitHubに貼るときにFBから画像のURLコピペしてこのサービスのURLとくっつけて云々やるのだるくね?って思ってしまった
- ブックマークレットとかChrome拡張とかで画像URLを入力するとこのサービスのURLと合体してmarkdownにして云々やってくれるやつがあるとまだマシかもしれない
- でもやっぱりLTTMの快適さには勝てないと思った
- LTTM的なサービスのバックエンドとして使われる道はあるかもしれないと思った
- 例えばLTTMでGitHubにコピペされるURLにこのサービスのURLをくっつけるだけでLGTM画像(LGTMという文字が入っている画像の意)に変換できる