ようへいの日々精進XP

よかろうもん

hubot を Kubernetes クラスタ上で動かす一例

この記事は

YAMAP エンジニア Advent Calendar 2020 の八日目になる予定です。

qiita.com

tl;dr

hubot を Kubernetes で動かそうと試行錯誤したのでメモしておきます。とりあえず、docker desktop 上の Kubernetes で動くところまで確認出来ました :tada:

尚、本記事で登場するツールのバージョンについては、以下の通りです。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H15

$ docker version
Client: Docker Engine - Community
 Version:           19.03.12
...

Server: Docker Engine - Community
 Engine:
  Version:          19.03.12
...
 runc:
  Version:          1.0.0-rc10
...
 docker-init:
  Version:          0.18.0
...

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"16+", GitVersion:"v1.16.6-beta.0", GitCommit:"e7f962ba86f4ce7033828210ca3556393c377bcc", GitTreeState:"clean", BuildDate:"2020-01-15T08:26:26Z", GoVersion:"go1.13.5", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"16+", GitVersion:"v1.16.6-beta.0", GitCommit:"e7f962ba86f4ce7033828210ca3556393c377bcc", GitTreeState:"clean", BuildDate:"2020-01-15T08:18:29Z", GoVersion:"go1.13.5", Compiler:"gc", Platform:"linux/amd64"}

docker desktop のバージョンは以下の通りです。

f:id:inokara:20201205091534p:plain

hubot 側の準備

hubot の作成

hubot の準備は、こちら の記事を参考にさせて頂きました。ありがとうございます。

$ npm install -g yo generator-hubot
$ mkdir kappabot
$ cd kappabot
$ yo kappabot

Hubot と Slack の連携

Hubot と Slack を連携させます。

f:id:inokara:20201205091605p:plain

スクリプトの追加

以下のような、指定した日付が祝日かどうかを返すスクリプトを書いてみました。

holiday.js

module.exports = (robot => {
  robot.respond(/today/, async res => {
    let today = getToday()
    let holidays = 'holidays.jp から取得した日付のリスト'
    // today が holidays に含まれているかチェック
    res.send(today)
  })

  robot.respond(/holiday$/, async res => {
    let result
    if (todayIsHoliday()) {
      result = '今日は, 祝日です!'
    } else {
      result = '今日は祝日ではないですね.'
    }
    res.send(result)
  })

  robot.hear(/holiday (\d{4}-\d{2}-\d{2})$/, async res => {
    let day = res.match[1]
    let holiday = todayIsHoliday(day)
    let result
    if (holiday != '') {
      result = day + ' は `' + holiday + '` です.'
    } else {
      result = day + ' は祝日ではないですね.'
    }
    res.send(result)
  })

  const todayIsHoliday = (day) => {
    if (day != null) {
      today = day
    } else {
      today = getToday()
    }

    holidays = getHolidays()
    if (today in holidays) {
      return holidays[today]
    } else {
      return ''
    }
  }

  const getToday = () => {
    let t = new Date
    let dd = t.getDate()
    let mm = t.getMonth() + 1
    let yyyy = t.getFullYear()

    if (dd < 10) {
      dd = '0' + dd
    }

    if (mm < 10) {
      mm = '0' + mm
    }
 
    let today = yyyy + '-' + mm + '-' + dd
    return today
  }

  const getHolidays = () => {
    const req = require('sync-request')
    res = req('GET', 'https://holidays-jp.github.io/api/v1/date.json')
    if (res.statusCode == '200') {
      return JSON.parse(res.body)
    } else {
      console.log('祝日の一覧取得に失敗しました.')
    }
  }
})

このスクリプトscripts ディレクトリ以下に放り込んでおきます。ちなみに、このスクリプト、当初は他のサンプルスクリプトに倣って拡張子を .coffee にしていたら、シンタックスエラーが出てしまって hubot が起動しませんでしたが、.js だと動きました。

尚、このスクリプトでは、以下のような感じのやりとりを楽しむことが出来ます。

f:id:inokara:20201205091624p:plain

動作確認

以下のように hubot を起動します。

fish> env HUBOT_SLACK_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ./bin/hubot --adapter slack

起動したら、適当なチャンネルにボットを招待しましょう。

/invite @your-bot-name

あとは、下図のようにやり取りを楽しみましょう。

f:id:inokara:20201205091624p:plain

Kubernetes で動かす

Dockerfile

ここから本題です。Kubernetes 上で動かすにあたって、以下のような Dockerfile を書きました。

FROM node:15.3.0-buster
WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

CMD ["bin/hubot", "--adapter", "slack"]

docker-compose

一旦、docker-compose 起動出来るようにしましょう。

version: '3.4'

services:
  kappabot:
    env_file: .env
    container_name: kappabot
    build:
      context: .
      dockerfile: Dockerfile
    image: inokappa/kappabot
    volumes:
      - .:/app
    command:
      - bin/hubot
      - "--adapter"
      - slack

HUBOT_SLACK_TOKEN は、.env ファイル記載しました。

HUBOT_SLACK_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

あとは、コンテナを起動しましょう。

$ docker-compose build && docker-compose up -d

deployment.yml

引き続き、Kubernetes で動かしてみたいと思いますので、docker desktop で Kubernetes が利用出来る状態にしておきましょう。

以下のように deployment.yml を書きました。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-kappabot
  labels:
    app: kappabot
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kappabot
  template:
    metadata:
      labels:
        app: kappabot
    spec:
      containers:
        - name: kappabot
          image: inokappa/kappabot
          imagePullPolicy: IfNotPresent
          command:
          env:
            - name: HUBOT_SLACK_TOKEN
              valueFrom:
                secretKeyRef:
                  name: kappabot-secret
                  key: kappabot_hubot_slack_token

まだまだ、ちゃんと Kubernetes を理解出来ていませんが、上記程度のマニフェストであれば、なんとなく雰囲気で書けるようになったはず。各項目の説明については割愛 (ちゃんと説明出来ない) させていただきます。

一点だけ。

HUBOT_SLACK_TOKEN環境変数に仕込んでいますが、トークン自体は Secrets に保存することを想定しています。Secrets については、次のセクションで少しだけ詳しく書きます。

Secrets からの sealed-secrets

Secrets

Secrets は、パスワードやトークン等の機密情報を登録する為の機能を提供して、Secrets (以後、Secret と書きます) を作成するには、ざっくりと二つの方法があります。

一つ目は、以下のように登録したいデータをテキストファイルに書き込んでおいて、kubectl create secret を実行する方法。

$ echo -n 'admin' > ./username.txt
$ echo -n '1f2d1e2e67df' > ./password.txt
$ kubectl create secret generic db-user-pass --from-file=./username.txt --from-file=./password.txt

また、以下のようにマニフェストを用意して、kubectl apply する方法があります。

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  username: YWRtaW4=
  password: MWYyZDFlMmU2N2Rm

尚、この際に usernamepassword の値は base64 エンコードした値を登録する必要があります。

$ echo -n 'admin' | base64
YWRtaW4=
$ echo -n '1f2d1e2e67df' | base64
MWYyZDFlMmU2N2Rm

今回は、マニフェスト自体を Github で管理したかったので、後者の方法を選びました。ただ、マニフェストに登録している秘匿情報 (上記の例だと、usernamepassword) は、単純に Base64 エンコードだけされた状態なので、簡単にデコードして読み取ることが出来てしまいます。この課題を解決するのが、後述する sealed-secrets です。

sealed-secrets で暗号化

sealed-secrets は、ざっくり言うと、sealed-secrets controller が、暗号化された Secret マニフェスト (SealedSecrets) を読み取って復号化し、Pod から利用出来るようにするものです。

github.com

Secret マニフェストは、kubeseal という関連ツールと sealed-secrets コントローラによって作成される公開鍵を使って暗号化することが出来ます。暗号化した、Secret マニフェストは SealedSecrets として Github リポジトリに登録することになります。

ということで、HUBOT_SLACK_TOKENKubernetes クラスタに登録したいと思いますが、既に、sealed-secrets コントローラは作成され、公開鍵は手元にある前提で進めます。

$ kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.13.1/controller.yaml
$ kubeseal --fetch-cert \
    --controller-namespace=kube-system \
    --controller-name=sealed-secrets-controller > pub-cert.pem

まずは、HUBOT_SLACK_TOKENBase64 エンコードします。

$ echo -n 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' | base64
eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHg=

-n オプションを忘れないようにしましょう。

そして、以下のように Secret マニフェスト secret.yml を用意します。

apiVersion: v1
kind: Secret
metadata:
  name: kappabot-secret
type: Opaque
data:
  kappabot_hubot_slack_token: eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHg=

引き続き、Secret マニフェストを暗号化します。

$ kubeseal --format=yaml --cert=pub-cert.pem < secret.yml > sealedsecret.yml

SealedSecrets は、以下のような内容となります。

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  creationTimestamp: null
  name: kappabot-secret
  namespace: default
spec:
  encryptedData:
    kappabot_hubot_slack_token: AgAY//FLFic2Ga3ZrlsucZmxY/tN1k/v+CD+BrHVhWkzFjbK6tVSKgCW2PANfKAe8Zm2d/nsbVmRUOzZr3mU/kh+MppeEQnCztnVqUxflOrJqwwchWAmf5/3nwVUUjnr6ueVNTI3eLX/vECOpJlS51KyGM4vrg8eOGqLxAHvQxTqsKGQu/EqbQ3VN3AzHYVJlnK7Yni3z2nBt91uHjNm4fltVI1Jado84/R+uFYj27/O8dkq0gcvQiHttjXxUgNm+zJVXNurR6yXd++TR236rFBbsJkSkxfS70Fs5BriJ36pyvlK6vf95ZZDxH+1JNrOBWNluyLl+keDlJBpUfjnYmMzSRlBdzImAoxBk8SwP4BzMnadXoIn9Vti+DjOgMTAhPAjtHYGO8R8GLKn+6QUAUNRyLkKg5RMFctGTGJA9RWhFTON4KUNGjDJF6EfCzQMgCtOm7019wEW08Gq+L18clgruskL+LeJ4a2Z8YI7CG07QLv4zkqNVGTQYozHwOcn3+SMMQ3Ff+TN3tpGt5nMcH3Y7opbnZAaYsc5MyIvPYaOeiYR64XjznxE0kyDbNBkXQtB9opFRWH10SsM+8k6glJDihluGX+jtuigWMyRwyo9BA4Ok7ojBcMv2q3MOBlCR8dMeQX3vA0mJ3rcXeHov1HTesc3OfCDEXqI+u0PB9+p9ga/6P1Hw9cBhzBRvyzjvKVIufb69ez7xRgzn2b9Ej2EW32aikCmycG1hmCvSGQipCx8Lg==
  template:
    metadata:
      creationTimestamp: null
      name: kappabot-secret
      namespace: default
    type: Opaque

そして、最後に Secret を Kubernetes クラスタ上に作成します。

$ kubectl apply -f sealedsecret.yml

そして、hubot を Kubernetes クラスタにデプロイ

以下を実行して hubot を Kubernetes クラスタにデプロイします。

$ kubectl apply -f deployment.yml 

後は、適当にやりとりしてみましょう。

f:id:inokara:20201205091741p:plain

以上

今回は、手元の docker desktop 上の Kubernetes クラスタを利用しましたが、GKE とかにデプロイしてみたい気持ちが高まって参りました。

参考

kubernetes.io

kubernetes.io

kubernetes.io

kubernetes.io

github.com

tech.uzabase.com

2020 年 12 月 04 日 (金)

アクティビティ (今までの走行 (歩行) 距離)

https://pixe.la/v1/users/inokappa/graphs/fitbit-activity

Fitibit Charge2 のアクティビティから走行 (歩行) 距離を Fitbit Web API で取得して Pixela で草生やしている。色が濃くなれば濃くなる程強度が高い (歩行、走行距離が長い) ということで。実装の詳細はこちら

ジョギング

レアジョブ

実用英会話 LEVEL 3 Day 3。今日は Val 先生。Val 先生の方がゆっくりと話してくれるので、安心して会話に挑める感じ。しかし、実用英会話は全くついていけない。やばい。

夕飯

2020 年 12 月 03 日 (木)

アクティビティ (今までの走行 (歩行) 距離)

https://pixe.la/v1/users/inokappa/graphs/fitbit-activity

Fitibit Charge2 のアクティビティから走行 (歩行) 距離を Fitbit Web API で取得して Pixela で草生やしている。色が濃くなれば濃くなる程強度が高い (歩行、走行距離が長い) ということで。実装の詳細はこちら

ジョギング

レアジョブ

実用英会話 LEVEL 3 Day 2。今日もテンション高めのチューターだった。一応、通じている感じなんだけど、「ちゃんと」身につけたいなあ。

エンジニアフレンドシティアワード 2020

efc.fukuoka.jp

YAMAP や Fukuoka.rb 等が選ばれていて嬉しかった。

夕飯

美味しゅうございました、おごちそうさまでした。

S3 バケットに保存されている画像ファイルのファイルサイズや大きさ (縦横) の一覧を取得する方法の一例

この記事は

YAMAP エンジニア Advent Calendar 2020 の六日目になる予定です。

qiita.com

tl;dr

ギョームにて、S3 バケットに保存されている画像ファイルのファイルサイズや大きさの一覧が欲しいというリクエストを頂きましたので、AWS CLIgrepawk 等のトラディショナルなツール、たまに Python スクリプトを駆使してリストアップしてみたのでメモしておきます。

尚、本記事で登場するツールのバージョンについては、以下の通りです。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H15

$ aws --version
aws-cli/2.1.1 Python/3.9.0 Darwin/19.6.0 source/x86_64

$ docker run --rm -t -i python:buster python --version
Python 3.9.0

尚、ここで書いてあるコマンドやシェルスクリプトは、あくまでも一例であり、他の実装があれば、是非、コメント等いただけると嬉しいです。

ファイルサイズ

シンプルに一覧を取得

#
# * --recursive: 再帰的にリストを出力する
# * --human: ファイルサイズに単位を付与して出力する
# * --sum: サマリー (オブジェクト個数と合計サイズ) を出力する
#

$ aws s3 ls s3://${BUCKET}/${OBJECT_PATH}/ --recursive --human --sum

以下のように出力されます。

...
2020-09-27 18:09:07  527.4 KiB /path/to/image1.png
2020-09-27 18:09:07    1.0 MiB /path/to/image2.png

Total Objects: 8436
   Total Size: 1.6 GiB

こちらでも一覧を取得できます。

fish> env AWS_PAGER="" aws s3api list-objects-v2 --bucket=${BUCKET} \
  --query='Contents[].[LastModified, Size, Key]' --output=text

以下のように出力されます。

--human 相当のオプションが存在していないことが残念です。2 列目がオブジェクトのサイズで、単位は bytes となります。

2020-09-27T09:09:07+00:00       540036  /path/to/image1.png
2020-09-27T09:09:07+00:00       1055619 /path/to/image2.png

env AWS_PAGER="" としているのは、AWS CLI v2 はデフォルトでページャーが有効になっているので、これを一時的に無効にしています。

MiB 及び GiB サイズの一覧を取得

MiB と GiB サイズの一覧を取得したい場合には、以下のように実行します。

$ aws s3 ls s3://${BUCKET}/${OBJECT_PATH}/ --recursive --human --sum \
  | egrep 'MiB|GiB' \
  | grep -v 'Total Size:' \
  | sort -k3 -r

また、容量が大きい順にソートして出力するように調整しています。

以下のように出力されます。

2020-05-08 15:55:40   26.9 MiB  /path/to/image1.png
2020-11-19 23:18:22   16.5 MiB /path/to/image2.png
...
2020-08-19 21:32:30    1.0 MiB /path/to/image099.png
2020-08-19 21:33:38    1.0 MiB /path/to/image100.png

100KiB 以下の一覧を取得

100KiB 以上のオブジェクトの一覧を取得した場合には、以下のように実行します。

aws s3 ls s3://${BUCKET}/${OBJECT_PATH}/ --recursive --human --sum \
  | egrep 'KiB' \
  | grep -v 'Total Size:' \
  | sort -k3 -r \
  | awk '$3 < 100.0{print}'

以下のように出力されます。

2020-09-02 14:08:18   99.9 KiB  /path/to/image1.png
2020-11-20 18:13:43   99.8 KiB  /path/to/image2.png
...
2020-05-08 15:41:50    1.3 KiB /path/to/image099.png
2019-10-10 17:27:04    1.3 KiB /path/to/image100.png

awkawk '$3 < 100.0{print}' こう書くことで、対象列の数値比較が出来ることを初めて知りました。

大きさ (縦横)

残念ながら

AWSAPI で取得する方法を見つけることが出来ませんでしたので、Python と boto3 を使うことにしました。

雑な Python スクリプト

以下のように雑な Python スクリプトを書きました。このスクリプトを適当なファイル名で保存します。(今回は check.py としました。)

from PIL import Image
import boto3

s3 = boto3.client('s3')

bucket = 'YOUR-BUCKET-NAME'
f = open('IMAGE-LIST')
lines = f.readlines()
for line in lines:
    key = line.replace('\n' , '')
    local = '/tmp/' + line.split('/')[-1]
    s3.download_file(bucket, key, local)
    im = Image.open(local)
    width, height = im.size
    
    print("key: %s, height: %s, width: %s" % (key, height, width))

画像ファイルの縦、横サイズを取得する為、Pillow というライブラリを利用しました。

また、IMAGE-LIST は、以下のコマンドで取得したオブジェクトの一覧が記載されたファイルとなります。

$ aws s3 ls s3://${BUCKET}/${OBJECT_PATH}/ --recursive --human --sum | awk '{print $5}' > IMAGE-LIST
$ cat IMAGE-LIST
 /path/to/image1.png
 /path/to/image2.png
...
 /path/to/image099.png
 /path/to/image100.png

一旦、この Python スクリプトを使って、全ての画像ファイルの縦横サイズを取得します。

$ docker run -t -i --rm -v $PWD:/work python:buster bash
docker> pip install Pillow boto3
docker> python check.py
key:  /path/to/image1.png, height: 150, width: 150
key:  /path/to/image2.png, height: 576, width: 1024
...

いい感じです。縦 (height) と横 (width) とオブジェクトキー (ファイル名) が出力されました。

以上

こういう細かいけど、なんとなくエンジニアリングな作業が大好きです。また、上記以外にも様々な実装がありそうなので、ワクワク感もたまりませんな。

以上、素敵な S3 バケットライフをお過ごし下さい。

参考

stackoverflow.com

2020 年 12 月 02 日 (水)

アクティビティ (今までの走行 (歩行) 距離)

https://pixe.la/v1/users/inokappa/graphs/fitbit-activity

Fitibit Charge2 のアクティビティから走行 (歩行) 距離を Fitbit Web API で取得して Pixela で草生やしている。色が濃くなれば濃くなる程強度が高い (歩行、走行距離が長い) ということで。実装の詳細はこちら

ジョギング

もう少し呼吸に負荷をかけたい。

ランニング食堂さんが、10000M で 29 分台で走っていた。

www.youtube.com

すごい!市民ランナーの誇り。福岡マラソンも頑張って欲しい。

レアジョブ

実用英会話 LEVEL 3 開始。やばい、まったくついていけてない感じ。今日もテンション高めのチューター。

ハイテンションチューターが、自分の緊張をほぐそうとして、

Do you have a long time to do your hair?

って聞いてきたのが、今日のハイライトだった。

夕飯

AWS re:Invent 2020

やっている。当然、全部を追うことは出来ないので、安定のクラスメソッドさんのブログを読むことにしている。

dev.classmethod.jp

AWS Lambda で Docker イメージがサポートされたのでチュートリアルしてみました

この記事は

YAMAP エンジニア Advent Calendar 2020 の五日目になる予定です。

qiita.com

tl;dr

従来、AWS Lambda では、ソースコードや関連するパッケージを zip 形式でまとめたファイルをアップロードして Lambda 関数を作成していましたが、Docker イメージでも Lambda 関数を作成出来るようになったとのアナウンスがあったので、サラッとチュートリアルしてみました。

aws.amazon.com

この手のアップデートでは、いきなり東京リージョンでは使えないのかなーと思っていましたが、既に東京リージョンでも利用出来る状態でした。ありがたや。

チュートリアル

アプリケーション

以下の記事を参考に、せっかくなので Ruby コンテナでチュートリアルしています。

aws.amazon.com

早速、コンテナ内で動かすアプリケーションを書きます。

require 'json'

def handler(event:, context:)
  { statusCode: '200', body: JSON.generate('Test!+1') }
end

app.rb という名前で保存します。

Docker イメージだからと言って、特別なコードを書く必要はありません。

Dockerfile

超シンプル Dockerfile です。

FROM amazon/aws-lambda-ruby:latest
COPY app.rb ./
CMD [ "app.handler" ]

肝となるのは、ベースイメージである amazon/aws-lambda-ruby で、このイメージには、Lambda のランタイム API と連携する為の AWS Lambda Runtime Interface Clients というライブラリが同梱されている状態とのことです。

尚、このコンテナイメージで動く Ruby のバージョンは 2.7.2 でした。(2020/12/02 現在)

$ docker run -d --rm --name=ruby-docker-lambda -p 9000:8080 ruby-docker-lambda:latest
$ docker exec -t -i ruby-docker-lambda ruby --version
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]

entrypoint として指定されている /lambda-entrypoint.sh を見てみたいと思います。

$ docker exec -t -i ruby-docker-lambda cat /lambda-entrypoint.sh
#!/bin/sh
# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.

if [ $# -ne 1 ]; then
  echo "entrypoint requires the handler name to be the first argument" 1>&2
  exit 142
fi
export _HANDLER="$1"

RUNTIME_ENTRYPOINT=/var/runtime/bootstrap
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
  exec /usr/local/bin/aws-lambda-rie $RUNTIME_ENTRYPOINT
else
  exec $RUNTIME_ENTRYPOINT
fi

/usr/local/bin/aws-lambda-rie ってなんだろうって思ったら、Lambda をローカル上で実行出来るエミュレータのようです。これを使うことで、ローカル環境での動作確認が可能になるということですね。

ビルド

コンテナイメージをビルドします。

$ docker build -t ruby-docker-lambda .

一旦、確認

以下のようにコンテナを起動します。

$ docker run --rm --name=ruby-docker-lambda -p 9000:8080 ruby-docker-lambda:latest

起動したら、以下のように出力されました。

time="2020-12-02T11:45:55.14" level=info msg="exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)"

コンテナが起動したら、curl コマンドを使ってアクセスすると、以下のように出力されました。

$ curl http://localhost:9000/2015-03-31/functions/function/invocations -d '{}'
{"statusCode":"200","body":"\"Test!+1\""}

おおー、Lambda が動いている感じ。なるほど、先程の /usr/local/bin/aws-lambda-rie が頑張ってくださっていますね。

コンテナイメージを push

ECR にコンテナイメージを push します。

$ aws ecr create-repository --repository-name=ruby-docker-lambda --image-scanning-configuration scanOnPush=true
$ docker tag ruby-docker-lambda:latest 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/ruby-docker-lambda:latest
$ docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/ruby-docker-lambda:latest

Lamda 関数を作成

マネジメントコンソールポチポチで作成します。

f:id:inokara:20201202213529p:plain

API Gateway との連携

API Gateway と連携させてみます。

今回は、マネジメントコンソールからポチポチで連携を設定します (詳細は割愛します)。

f:id:inokara:20201202211314p:plain

連携の設定が完了したら、curl コマンドを使ってアクセスすると、以下のように出力されました。

$ curl https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/default/ruby-docker-lambda
"Test!+1"

おおー。なんか、満足。

Gem パッケージを追加する場合

試しに terminal-table を組み込んでみたいと思います。

コードは以下のように修正します。

require 'json'
require 'terminal-table'

def handler(event:, context:)
  rows = []
  rows << ['One', 1]
  rows << ['Two', 2]
  rows << ['Three', 3]
  table = Terminal::Table.new :headings => ['Word', 'Number'], :rows => rows
  { statusCode: '200', body: table }
end

Dockerfile は以下のように修正します。

FROM amazon/aws-lambda-ruby:latest
COPY app.rb Gemfile ./
RUN bundle install --path vendor/bundle
CMD [ "app.handler" ]

Gemfile は以下のように書きました。

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'terminal-table'

あとは、コンテナイメージをビルドして、ローカル環境でコンテナを起動して確認します。

$ curl -s http://localhost:9000/2015-03-31/functions/function/invocations -d '{}' | jq -r .body
+-------+--------+
| Word  | Number |
+-------+--------+
| One   | 1      |
| Two   | 2      |
| Three | 3      |
+-------+--------+

いい感じですね。あとは、Lambda のコンテナイメージを更新すれば、API Gateway からも確認出来るようになるはずです。

ちなみに、12/02 時点で、コンテナイメージの更新を行った場合、マネジメントコンソールからコンテナイメージを指定し直す必要がありました。

f:id:inokara:20201202212828p:plain

マネジメントコンソールを「日本語」にしておくと、この「画像を参照」っていうのが、ジワジワきますね (笑

以上

Lambda で Docker イメージを利用出来るメリットってなんだろうって考えてみました。

  1. 実装や検証にあたって、これまで Docker と共に使われてきたツール等が利用可能
  2. サードパーティパッケージも丸っとコンテナイメージにパッケージ出来る (レイヤーとか考えなくても良い)
  3. ローカルでの動作確認を行い易い

また、デメリットとしては、

  • あくまでも、コンテナイメージなので、マネジメントコンソール上でソースコードを見ることが出来ない
  • docker build と zip を比較すると、docker build の方がお手軽感は小さい (zip の方がお手軽のように感じる)

という感じでしょうか。あと、普段は Serverless Framework を使って Lambda を扱っているので、Serverless Framework が、この Docker イメージに対応したら嬉しいなあと思った次第です。

このリリースを見た時に、「おおおーーーーっ」と激しく興奮しましたが、実際に触ってみると、今すぐに既存の Lambda 関数について、Docker イメージ版に乗り換えるモチベーションは湧かなかったのが正直なところです。ごめんなさい。

ユースケースによっては、Docker イメージ版の方が良いという場合もあるので、今後、用法用量を守って適切な使い分けを考えたいと思います。

参考

aws.amazon.com

aws.amazon.com

Creating Lambda container images - AWS Lambda

www.keisuke69.net

dev.classmethod.jp

GitHub Actions や CircleCI で AWS リソースをいじる際に Assume Role を利用する

この記事は

YAMAP エンジニア Advent Calendar 2020 の四日目になる予定です。

qiita.com

tl;dr

Github Actions (CircleCI でもほぼ同じアプローチでイケると思います) で AWS リソースを操作する際、強い権限を付与したアクセスキー、シークレットアクセスキーを使い続けたくないので、Assume Role を設定して、AWS リソースを操作する時だけ (デプロイする時だけ) 強い権限を払い出すようにしてみたのでメモします。

Assume Role とは

Assume Role

Assume Role とは、直訳すると、「Assume Role (役割を引き受ける)」となります。ある Role に紐付いているポリシー (権限) をユーザーやグループ、他の AWS アカウントのユーザーやグループ、別の Role に委譲する機能です。

以下は、Assume Role のイメージです。

f:id:inokara:20201201235359p:plain

How to Use a Single IAM User to Easily Access All Your Accounts by Using the AWS CLI より引用

Assume Role を利用するメリット

Assume Role を利用することのメリットとしては、以下のようなことが挙げられます。

  • Assume Role されるユーザーに払い出す権限はデプロイ用ロールに対する sts:AssumeRole だけで良いので、シンプルに保つことが出来る
  • Assume Role される権限の、アクセスキー、シークレットアクセスキーは一時的に払い出されるもので、期限が切れると全くの無力である為、安心安全

登場人物

Assume Role でググると、複数の AWS 間での権限委譲の実装例を良く見ますが、本記事で紹介する実装例では、とてもシンプルな一つの AWS アカウント内のお話となります。

そして、本記事内で登場する登場人物は以下の通りです。

  • Assume Role されるユーザー (以後、デプロイ用ユーザーと記載)
  • Assume Role されるユーザーに付与するポリシー (以後、デプロイ用ユーザーポリシー)
  • Assume Role するロール (AWS リソースを操作する為に必要なポリシーが付与されている、以後、デプロイ用ロールと記載)

実装例

リポジトリ

このサンプルですが、Github Actions を使って、S3 バケットのオブジェクト一覧を取得したり、オブジェクトを削除したり、オブジェクトを追加します。

準備

以下のリソースを Terraform を使って作成します。

  • Assume Role されるユーザー (以後、デプロイ用ユーザーと記載)
  • Assume Role されるユーザーに付与するポリシー (以後、デプロイ用ユーザーポリシー)
  • Assume Role するロール (AWS リソースを操作する為に必要なポリシーが付与されている、以後、デプロイ用ロールと記載)
  • S3 バケット

以下のように実行します。

$ git clone git@github.com:inokappa/deploy-sample-for-assume-role.git
$ cd deploy-sample-for-assume-role
$ cd setup
$ make plan
$ make apply

これらのリソースを作成する際には、それなりの権限が付与された IAM ユーザーで作成しましょう。

以下のように、出力されますので、それぞれの値を控えておきましょう。

...
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

id = AKxxxxxxxxxxxxxxxxxxxxxxx
role = arn:aws:iam::012345678901:role/deploy-user-role
secret = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Github リポジトリの設定

上記で出力された、idsecret 及び secret 等の情報を、下図のように Github リポジトリSettings > Secrets に設定します。

f:id:inokara:20201201235602p:plain

Github Actions の YAML

以下のように設定します。

name: sandbox

on:
  push:
    branches:
      - 'master'
      - 'main'

jobs:
  sandbox:
    runs-on: ubuntu-latest
    env: 
      S3_TARGET_BUCKET: ${{ secrets.S3_TARGET_BUCKET }}

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1
          role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }}
          role-duration-seconds: 900

      - name: List up Objects
        run: |
          aws s3 ls s3://${S3_TARGET_BUCKET}/

      - name: Put Objects
        run: |
          aws s3 cp test.txt s3://${S3_TARGET_BUCKET}/test.txt

      - name: Remove Objects
        run: |
          aws s3 rm s3://${S3_TARGET_BUCKET}/test.txt

肝心なのは、以下の部分で、AWS から Configure AWS Credentials が提供されているので、これを利用することで、とても簡単に設定することが出来ました。

    steps:
... 略 ...
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1
          role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }}
          role-duration-seconds: 900
... 略 ...

以下の内容を設定します。

  • aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} には、デプロイ用ユーザーのアクセスキーを設定します
  • aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} には、デプロイ用ユーザーのシークレットアクセスキーを設定します
  • role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }} には、デプロイ用ロール名を設定します

デプロイ用ユーザーのアクセスキーとシークレットアクセスキーだけでは、S3 バケットにアクセスことは出来ません。あくまでも、Assume Role でロールを引き受けるだけの権限が付与されている状態です。

尚、role-duration-seconds では、Assume Role された権限の猶予時間を指定します。上記の例では、900 秒 (15 分) を設定しています。

git push

適当に test.txt を修正して、git push します。

$ echo 'foo' > test.txt
$ git add && git commig -m 'Add foo' && git push

すると、下図のように正常に S3 バケットにアクセスしてオブジェクトの操作が行えていることが判ります。

f:id:inokara:20201201235632p:plain

試しに、YAML ファイルを以下のように修正して push してみると...

diff --git a/.github/workflows/sandbox.yml b/.github/workflows/sandbox.yml
index b946edd..2fae2d7 100644
--- a/.github/workflows/sandbox.yml
+++ b/.github/workflows/sandbox.yml
@@ -22,8 +22,8 @@ jobs:
           aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
           aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
           aws-region: ap-northeast-1
-          role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }}
-          role-duration-seconds: 900
+            # role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }}
+            # role-duration-seconds: 900

       - name: List up Objects
         run: |

Assume Role 出来ておらず、権限が付与されていないので、下図のようにエラーとなりました。

f:id:inokara:20201201235653p:plain

いい感じです!

以上

正直、思ったよりも簡単に Assume Role を使って、Github Action から AWS リソースを操作することが出来たので、まだまだ、俺も捨てたもんじゃないなって自画自賛しています :sweat_smile:

Assume Role を利用することで、Github Actions や CircleCI 等の外部サービス用の IAM ユーザーに対して、個別に細かい権限を付与する必要がなく、また、時限的に権限が付与される為、アクセスキー、シークレットアクセスキーの漏洩に対して、安心安全に運用出来そうです。

ということで、素敵な Assume Role ライフをお過ごし下さい。

ちょっと待った、CircleCI だとどうする?

いくつか Assume Role の為の orbs が提供されていましたが、どれを使ったら良いのか判断出来なかったので、泥臭くシェルスクリプトで書いてみました。

# reference: https://dev.classmethod.jp/articles/circleci-use-aws-cli-v2/
version: 2.1

executors:
  my-executor:
    docker:
      - image: cimg/base:2020.11
    working_directory: ~/work
    environment:
      PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$HOME/work/aws-cli/v2/current/bin

commands:
  restore:
    steps:
      - restore_cache:
          key: work-v1-{{ .Revision }}

  save:
    steps:
      - save_cache:
          paths:
            - "aws-cli"
          key: work-v1-{{ .Revision }}

  install:
    steps:
      - run:
          name: install
          command: |
            if [[ ! -d aws-cli ]]; then
              curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
              unzip awscliv2.zip
              sudo ./aws/install --install-dir ~/work/aws-cli
            fi

  assume_role:
    steps:
      - run:
          name: Assume Role
          command: |
            AWS_STS_CREDENTIALS="$(aws sts assume-role \
              --role-arn=${AWS_ASSUME_ROLE_ARN} \
              --role-session-name "deply-user-session" \
              --external-id 00001 \
              --duration-seconds 900 \
              --query "Credentials" \
              --output "json")"
            echo "export AWS_ACCESS_KEY_ID=$(echo ${AWS_STS_CREDENTIALS} | jq -r '.AccessKeyId')" >> ${BASH_ENV}
            echo "export AWS_SECRET_ACCESS_KEY=$(echo ${AWS_STS_CREDENTIALS} | jq -r '.SecretAccessKey')" >> ${BASH_ENV}
            echo "export AWS_SESSION_TOKEN=$(echo ${AWS_STS_CREDENTIALS} | jq -r '.SessionToken')"  >> ${BASH_ENV}

  access:
    steps:
      - run:
          name: List up Objects
          command: |
            aws s3 ls s3://$S3_TARGET_BUCKET/
      - run:
           name: Put Object
           command: |
             aws s3 cp test.txt s3://$S3_TARGET_BUCKET/test.txt
      - run:
           name: Remove Object
           command: |
             aws s3 rm s3://$S3_TARGET_BUCKET/test.txt

jobs:
  sandbox:
    executor: my-executor
    steps:
      - checkout
      - restore
      - install
      - save
      - assume_role
      - access

workflows:
  sandbox:
    jobs:
      - sandbox

以下のように意図した通りに AWS リソースの操作が出来ています。

f:id:inokara:20201201235708p:plain

参考

docs.aws.amazon.com

aws.amazon.com

dev.classmethod.jp

2020 年 12 月 01 日 (火)

アクティビティ (今までの走行 (歩行) 距離)

https://pixe.la/v1/users/inokappa/graphs/fitbit-activity

Fitibit Charge2 のアクティビティから走行 (歩行) 距離を Fitbit Web API で取得して Pixela で草生やしている。色が濃くなれば濃くなる程強度が高い (歩行、走行距離が長い) ということで。実装の詳細はこちら

ジョギング

  • なんちゃって体幹レーニング 3 セット、30 分エアロバイク (負荷 8) + (30 秒ダッシュ漕ぎ x 3)
  • 早く走りたい〜〜〜

レアジョブ

いつものお気に入りチューターがお休みに入ったのか、全く予約が取れなくなってしまったので、恐る恐る別のチューターで受講してみた。今まで出会ったチューターの中で最もテンションが高く、話すスピードが速すぎてしゃべっている内容の 3 割くらいしか理解出来かったけど。。。解っている体の顔をするのは得意なので、なんとか 25 分をやり過ごした。

夕飯

やばい

もう、12 月だ。今年は特に何一つ、目標を達成することが出来なかった。。。けど、あと 30 日間、頑張って取り戻していきたい!

2020 年 11 月 30 日 (月)

アクティビティ (今までの走行 (歩行) 距離)

https://pixe.la/v1/users/inokappa/graphs/fitbit-activity

Fitibit Charge2 のアクティビティから走行 (歩行) 距離を Fitbit Web API で取得して Pixela で草生やしている。色が濃くなれば濃くなる程強度が高い (歩行、走行距離が長い) ということで。実装の詳細はこちら

ジョギング

  • なんちゃって体幹レーニング 3 セット、30 分エアロバイク (負荷 8)

久しぶりにオフィスに出社

久しぶりにオフィスに出社。在宅勤務が推奨がされている為、出社するメンバーは少数。室内では T シャツいっちょなのと、エアコンを付けていなかったというのも後押しして激寒で震えながら仕事した。ちゃんと羽織るものは用意しておいたほうが良いという知見を得た。

夕飯

ffmpeg を使って mov ファイルをシュッと GIF アニメーションに変換したい

この記事は

YAMAP エンジニア Advent Calendar 2020 の二日目になる予定です。

qiita.com

久しぶりにブログを書くので緊張しています。

tl;dr

macOS 上で操作内容を QuickTime Player.app (以後、QuickTime Player と記載) で撮影してブログ等に貼りたい場合、撮影した動画 (mov ファイル) を GIF アニメーションに変換したいというニーズが高まりました。以前は、LICECap.app (以後、LICECap と記載) を使っていたのですが、OS のバージョンアップによって使えなくなってしまったので、操作内容を動画で共有することは諦めていました。今回、どうしても動画で残したく、インターネット上を検索すると、あの有名な ffmpeg を使う方法が紹介されていたので試してみました。

www.cockos.com

ffmpeg.org

ffmpeg とは

ffmpeg とは、Wikipedia によると、以下のように解説されています。

FFmpeg(エフエフエムペグ)は動画と音声を記録・変換・再生するためのフリーソフトウェアである。Unixオペレーティングシステム (OS) 生まれであるが現在ではクロスプラットフォームであり、libavcodec(動画/音声のコーデックライブラリ)、libavformat(動画/音声のコンテナライブラリ)、libswscale(色空間・サイズ変換ライブラリ)、libavfilter(動画のフィルタリングライブラリ)などを含む。ライセンスはコンパイル時のオプションによりLGPLGPLに決定される。コマンドラインから使用することができる。対応コーデックが多く、多彩なオプションを使用可能なため、幅広く利用されている。

https://ja.wikipedia.org/wiki/FFmpeg

色々な種類の動画をよしなに扱いたい場合に利用するソフトウェアで、今回のように mov ファイルの取り扱いもお手の物だと思います。

また、ffmpeg は、コマンドラインツールとして提供されており、macOS では homebrew でのインストールがお手軽です。

brew install ffmpeg

尚、前後してしまいますが、本記事で利用する環境は、以下の通りです。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H15

macOS Catalina ですね。

よし、GIF アニメーションを作るぞ

撮影

早速、sl コマンドが動く様子を QuickTime Player で撮影してみました。

f:id:inokara:20201130194208p:plain

撮影した動画は mov 形式で保存されます。一旦、YouTube にアップした動画をご覧下さい。

youtu.be

GIF アニメーションを作る

既に ffmpeg はインストールされていて、既にパスが通っているものとします。

ffmpeg で mov ファイルから、GIF アニメーションを作る場合には、以下のように実行します。

$ ffmpeg -i [MOV ファイル名] -r 10  -vf scale=960:-1 [GIF ファイル名]

GIF ファイルを開くと、以下のように GIF アニメーションが生成されました。

f:id:inokara:20201130205616g:plain

いい感じですね。

ffmpeg で GIF アニメーションを作る際の詳細

ffmpeg で GIF アニメーションを作る際のオプションを見てみたいと思います。

$ ffmpeg -i [MOV ファイル名] -r 10  -vf scale=960:-1 [GIF ファイル名]

-i オプションは、変換元の動画ファイルを指定します。

usage: ffmpeg [options] [[infile options] -i infile]... {[outfile options] outfile}...

-r オプションでは、フレームレート (1 秒間の動画で見せる静止画の枚数) を指定します。上記の例だと、フレームレートは 10 なので、毎秒の静止画は 10 枚ということになります。

-r rate             set frame rate (Hz value, fraction or abbreviation)

そして、-vf オプションは、様々なフィルタを指定出来るオプションのようですが、今回は scale=960:-1 オプションを使って、横幅 960 でリサイズを行っています。

-vf filter_graph    set video filters

ちょっと遊んでみる

上記に掲載したオプションを弄ってみたいと思います。

$ ffmpeg -i [MOV ファイル名] -r 1  -vf scale=960:-1 [GIF ファイル名]

思いっきりフレームレートを下げてみました。

f:id:inokara:20201130215812g:plain

カクカク感が増したような。

続いて、上下反転させてみます。

$ ffmpeg -i  [MOV ファイル名] -r 10 -vf vflip,hflip -metadata:s:v:0 rotate=0 [GIF ファイル名]

-vf オプションは多彩です。vflip,hflip オプションを付与することで、上下反転を実現しています。

f:id:inokara:20201130220905g:plain

面白いですね。

オチ

ffmpeg にちょっとだけ慣れたところで、ffmpeg を使うきっかけとなった LICECap が動かない件。この記事を書くにあたって、改めて LICECap のサイトを訪れたところ、最新版 が配布されていた (LICEcap v1.30 for macOS) ので、最新版をインストールしてみたところ、以下のように LICECap.app で GIF アニメーションを作成することが出来ました。

f:id:inokara:20201130221915g:plain

上図は、最新の LICECap で撮影しているところを、QuickTime Player で撮影したものを ffmpeg で GIF アニメーションに変換したものです。撮影しているところを撮影していたりすると、訳が分からなくなって面白いですよね。

参考

ffmpeg.org

qiita.com