S3ファイル作成をトリガーにしてLambda起動

S3ファイル作成をトリガーにしてLambda起動させてみて、どんなオブジェクトがLambdaに渡されるのかを確認しました。

Lambda作成

Runtimeは今回は Node.js 14.x を選択しました。

ソースコードはこれだけ。

exports.handler = async (event) => {
    console.log(JSON.stringify(event));
};

S3イベント設定

マネジメントコンソール上で、S3のPropertiesのEvent notifications(プロパティのイベント通知)のところから設定します。

こんな感じ。

f:id:suzuki-navi:20210421013648p:plain

通知したいイベントのタイプはオブジェクト作成を選択しました。

下のほうにイベントの通知先の設定欄がありますので、先ほど作ったLambdaを選択します。

f:id:suzuki-navi:20210421013708p:plain

試してみる

$ echo Hello | aws s3 cp - s3://xxxxxxxx/eventsample/1.txt

CloudWatch Logsには以下のJSONが出力されました。

{
    "Records": [
        {
            "eventVersion": "2.1",
            "eventSource": "aws:s3",
            "awsRegion": "ap-northeast-1",
            "eventTime": "2021-04-20T15:45:05.043Z",
            "eventName": "ObjectCreated:Put",
            "userIdentity": {
                "principalId": "AWS:AIDAXXXXXXXXXXXXXXXXX"
            },
            "requestParameters": {
                "sourceIPAddress": "35.xxx.xxx.xxx"
            },
            "responseElements": {
                "x-amz-request-id": "3VVRXXXXXXXXXXXX",
                "x-amz-id-2": "xxxxxxxxxxxxxxxx"
            },
            "s3": {
                "s3SchemaVersion": "1.0",
                "configurationId": "s3event-sample",
                "bucket": {
                    "name": "xxxxxxxx",
                    "ownerIdentity": {
                        "principalId": "XXXXXXXXXXXXXX"
                    },
                    "arn": "arn:aws:s3:::xxxxxxxx"
                },
                "object": {
                    "key": "eventsample/1.txt",
                    "size": 6,
                    "eTag": "09f7e02f1290be211da707a266f153b3",
                    "sequencer": "00607EF70623E54228"
                }
            }
        }
    ]
}

Google Compute Engineのstartup scriptsをgcloudコマンドで取得

Google Compute Engineのstartup scriptsを取得するコマンド。(INSTANCE_NAMEのところはインスタンスの名前を入れます)

$ gcloud compute instances describe INSTANCE_NAME --format json | jq '.metadata.items|from_entries."startup-script"' -r

説明

gcloud compute instances describeコマンドで、startup scriptsをJSONで取得できるのですが、JSONエンコーディングされてしまっています。長いスクリプトをこれで見るのは辛いです。

$ gcloud compute instances describe INSTANCE_NAME --format json | jq '.metadata.items'
[
  {
    "key": "startup-script",
    "value": "echo Hello\n"
  }
]

jqコマンドで from_entries というのを使うと、

[
  {
    "key": "startup-script",
    "value": "echo Hello\n"
  }
]

{
  "startup-script": "echo Hello\n"
}

に変換できます。なので from_entries."startup-script" と書くとスクリプトの部分を取得でき、 -r を付ければできあがりです。

to_entries, from_entries, with_entries | jq Manual (development version)

おまけ:startup scriptsをインスタンスからインスタンスにコピー

gcloud compute instances add-metadataコマンドの--metadata-from-fileは標準入力も受け取れるようですので、パイプでつないで以下のようにすれば、startup scriptsをインスタンスからインスタンスにコピーできました。

$ gcloud compute instances describe SRC_INSTANCE_NAME --format json | jq '.metadata.items|from_entries."startup-script"' -r | gcloud compute instances add-metadata DST_INSTANCE_NAME --metadata-from-file startup-script=/dev/stdin

2021/05/20 追記

インスタンスからはcurlコマンドでも自身のstartup scriptsを取得できます。

$ curl -H "Metadata-Flavor: Google" http://metadata/computeMetadata/v1/instance/attributes/startup-script

Google Container RegistryにVue.jsアプリを置いてCompute Engineにデプロイ

Vue.jsのウェブアプリをDockerコンテナにし、コンテナイメージからGoogle Compute Engineのインスタンスを作成して、Vue.jsのウェブアプリを動かしてみます。

Vue.jsのプロジェクト作成

$ vue create sample

Vue 2 or 3 は2を選択しました。

$ cd sample
$ npm run serve

サーバが立ち上がりますので、ブラウザでlocalhostにアクセスしてみます。ポート番号は npm run serve を実行しているコンソール上に表示されているはずですが、たぶん8080番です。すでに使われていたら8081、8082と順番に空きを探してくれるみたいです。

f:id:suzuki-navi:20210419092042p:plain

コンテナイメージ作成

Dockerfile は以下の内容です。

FROM node:15.14.0-alpine3.10

RUN npm install -g http-server

WORKDIR /opt/app

COPY package*.json ./

RUN npm install

COPY . .
# Dockerレイヤーのキャッシュを活用するために2段階のコピー

RUN npm run build
# distディレクトリに静的なファイルが生成される

EXPOSE 80
CMD ["http-server", "dist", "-p", "80"]

以下のVue.js公式ドキュメントを参考にしました。

Dockerize Vue.js App — Vue.js

ビルドします。

$ docker build -t vuejs-sample .
$ docker images
REPOSITORY                                TAG                  IMAGE ID            CREATED             SIZE
vuejs-sample                              latest               9d7cbcd7bd1f        About an hour ago   360MB

Google Container Registryにpush

Google Container Registryにpushします。

やり方はきのうの記事にも書きました。

Google Container RegistryにHello, WorldなDockerイメージを置いてみる

$ docker tag vuejs-sample gcr.io/xxxxxxxx/vuejs-sample

$ gcloud docker -- push gcr.io/xxxxxxxx/vuejs-sample

Google Compute Engineを作成し、デプロイ

sample1という名前のGoogle Compute Engineのインスタンスを作成します。その際にコンテナイメージを指定します。

$ gcloud compute instances create-with-container sample1 --container-image gcr.io/xxxxxxxx/vuejs-sample --tags http-server

このコマンドで作成されたインスタンスは、ディスク容量は10GB、マシンタイプn1-standard-1、OSはコンテナ実行に最適化された専用OS(Container-Optimized OS from Google documentation  |  Google Cloud)でした。

コンテナがLISTENしているポートは自動でホスト側のポートにもマッピングされます。

コンテナを実行する際のオプションの構成  |  Compute Engine ドキュメント  |  Google Cloud

ただし、firewallの設定は必要なようです。

$ gcloud compute firewall-rules create allow-http --allow tcp:80 --target-tags http-server

作成したインスタンスグローバルIPにブラウザでアクセスすると、さきほどのVue.jsのデモアプリが動いていることが確認できます。

Google Container RegistryにHello, WorldなDockerイメージを置いてみる

Google Container Registryを初めて触ってみます。

Dockerコンテナイメージ作成

以下のような適当なDockerfileを作成します。

FROM ubuntu:20.04
RUN echo "Hello, World!" > /hello.txt

コンテナイメージをビルドします。Dockerfile のあるディレクトリで以下のコマンドでビルドできます。

$ docker build -t hello-world .

作成したコンテナイメージは、docker imagesコマンドで見えるようになります。

 $ docker images
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
hello-world          latest              21deb0595efa        4 minutes ago       72.9MB

試しにコンテナイメージを実行してみます。

$ docker run -it --rm hello-world bash
root@3b8150776095:/# cat /hello.txt
Hello, World!

Google Container Registryにpush

Google Container Registryにpushするために、タグを付けます。新しいタグの名前は gcr.io/PROJECT_ID/NAME というフォーマットです。PROJECT_IDはGoogle Cloud PlatformのプロジェクトID、NAMEはコンテナイメージの名前です。

$ docker tag hello-world gcr.io/xxxxxxxx/hello-world

docker imagesコマンドで確認できます。

$ docker images
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
gcr.io/xxxxxxxx/hello-world   latest              21deb0595efa        6 minutes ago       72.9MB
hello-world                   latest              21deb0595efa        6 minutes ago       72.9MB

このコンテナイメージをGoogle Container Registryにアップロードします。

$ gcloud docker -- push gcr.io/xxxxxxxx/hello-world
$ gcloud container images list
NAME
gcr.io/xxxxxxxx/hello-world

Google Container Registryからpull

gcloudコマンドを同じプロジェクトが使えるように設定してあれば、別の環境からもコンテナイメージにアクセスできることが確認できます。

$ gcloud docker -- run -it --rm gcr.io/xxxxxxxx/hello-world bash -c "cat /hello.txt"
Hello, World!

追記 2021/04/22

AWSでも同じことをしました。

AWSのECR(Container Registry)にHello, WorldなDockerイメージを置いてみる

Google Compute Engineのstartup scriptsを試してみる

Google Compute Engineのstartup scripts(起動スクリプト)の機能を試してみました。

(最近GCPの「やってみた」系の記事が続いています)

やってみる

インスタンス作成。

$ gcloud compute instances create sample1 --image-project ubuntu-os-cloud --image-family ubuntu-2004-lts

startup-script という名前のメタデータとしてスクリプトの中身を文字列で直接指定すればよいようです。起動した日時をテキストファイルに書き残すスクリプトを指定してみました。

$ gcloud compute instances add-metadata sample1 --metadata startup-script="date > /startup-date.txt"

メタデータを確認してみます。

$ gcloud compute instances describe sample1 --format json | jq ".metadata.items"
[
  {
    "key": "startup-script",
    "value": "date > /startup-date.txt"
  }
]

起動していたインスタンスだったので、いったんSTOP。

$ gcloud compute instances stop sample1
Stopping instance(s) sample1...done.
Updated [https://compute.googleapis.com/compute/v1/projects/xxxxxxxx/zones/asia-northeast1-a/instances/sample1].

STOPが完了するまでコマンドは待ってくれるようです。

続いて起動。

$ gcloud compute instances start sample1
Starting instance(s) sample1...done.
Updated [https://compute.googleapis.com/compute/v1/projects/xxxxxxxx/zones/asia-northeast1-a/instances/sample1].
Instance internal IP is 10.xxx.xxx.xxx
Instance external IP is 34.xxx.xxx.xxx

startコマンドも起動が完了するまで待ってくれます。しかも、起動完了したら、取得したIPアドレスがコンソールに表示されるんですね。便利。

$ ssh suzuki@34.xxx.xxx.xxx cat /startup-date.txt
Mon Apr 12 13:49:34 UTC 2021

add-metadataコマンドで直接スクリプトの中身を指定するのではなく、ローカルファイルに置いてから指定することもできるようです。

ローカルのカレントディレクトリに startup-script.txt という名前でスクリプトを置きます。

$ cat > startup-script.txt
echo Hello >> /startup-date.txt
date >> /startup-date.txt

このファイルをメタデータで指定します。ファイルの中身がメタデータで登録されます。

$ gcloud compute instances add-metadata sample1 --metadata-from-file startup-script=./startup-script.txt
$ gcloud compute instances describe sample1 --format json | jq ".metadata.items"
[
  {
    "key": "startup-script",
    "value": "echo Hello >> /startup-date.txt\ndate >> /startup-date.txt\n"
  }
]

再起動。

$ gcloud compute instances stop sample1 && gcloud compute instances start sample1
Stopping instance(s) sample1...done.
Updated [https://compute.googleapis.com/compute/v1/projects/xxxxxxxx/zones/asia-northeast1-a/instances/sample1].
Starting instance(s) sample1...done.
Updated [https://compute.googleapis.com/compute/v1/projects/xxxxxxxx/zones/asia-northeast1-a/instances/sample1].
Instance internal IP is 10.xxx.xxx.xxx
Instance external IP is 34.xxx.xxx.xxx
$ ssh suzuki@34.xxx.xxx.xxx cat /startup-date.txt
Mon Apr 12 13:49:34 UTC 2021
Hello
Mon Apr 12 13:58:34 UTC 2021

追記 2021/04/20

Google Compute Engineのstartup scriptsをgcloudコマンドで取得

RubyでマルチスレッドなTCP Serverのサンプルコード

RubyTCP/IPのソケットを試してみました。サーバ側はマルチスレッドです。

サーバ側サンプルコード

server.rb

require 'socket'

maxlen = 10

server = TCPServer.open(3000)
loop do
  Thread.start(server.accept) do |socket|
    begin
      loop do
        buf = socket.readpartial(maxlen) # クライアントから受信
        socket.write(buf) # そのままクライアントに返答
        $stdout.write(buf) # 動作確認のためサーバ側標準出力
      end
    rescue EOFError => e
      $stdout.write("eof\n") # 切断
    rescue => e
      print e.backtrace.join("\n")
    ensure
      socket.close
    end
  end
end

クライアント側サンプルコード

client.rb

require 'socket'

maxlen = 10

TCPSocket.open("localhost", 3000) do |socket|
  t1 = Thread.start do
    socket.write("Hello\n") # サーバに送信
    sleep(1)
    socket.shutdown() # 1秒たったらクライアントから切断
  end
  t2 = Thread.start do
    begin
      loop do
        buf = socket.readpartial(maxlen) # サーバから受信
        $stdout.write(buf) # 動作確認のためクライアント側標準出力
      end
    rescue EOFError => e
      $stdout.write("eof\n") # 切断
    end
  end
  t1.join # スレッドが終了するまで待つ
  t2.join # スレッドが終了するまで待つ
end

実行例

サーバ側。起動しただけだと黙って待ち受けてます。

$ ruby server.rb

クライアント側。Helloと送信し、同じテキストを受信し、1秒後に切断します。

$ ruby client.rb
Hello
eof

クライアントを複数の端末を使うなどして同時に起動してもサーバ側は並行して処理できています。

$ ruby server.rb
Hello
eof
Hello
eof
Hello
Hello
eof
eof

Javaならレファレンスを見ながら書けば動くだろうという安心感(自信)があります。が、他の言語だとこういうのは、実際に書いて試行錯誤しないと私は書けないです)

AWS SQSをRubyで試してみる

RubyからAWSのSQSにメッセージを送受信してみます。前回Pythonで試しましたが、今回はそのRuby版です。

CloudFormationでSQSを作成するところは前回と同じなので省略します。

RubyAWS SDKをインストール。

$ gem install -N aws-sdk

Rubyのサンプルコード samplr.rb

require 'json'
require 'aws-sdk'

profile = "default"

credentials = Aws::SharedCredentials.new(profile_name: profile)
sqs_client = Aws::SQS::Client.new(credentials: credentials)

queue_url = "https://ap-northeast-1.queue.amazonaws.com/xxxxxxxxxxxx/samplesqs-SampleSQS-XXXXXXXXXXXX"

# 送信
idx = 1
while idx <= 10 do # 10個のメッセージを送信
  message = {msg: "Hello, world!", foo: idx}
  sqs_client.send_message({
    queue_url: queue_url,
    message_body: JSON.dump(message),
  })
  idx += 1
end

# 受信
loop do
  res = sqs_client.receive_message({
    queue_url: queue_url,
  })

  # 受信したものをなにか処理 (このサンプルでは表示するだけ)
  delete_entries = [] # 処理済みメッセージ一覧
  id = 1
  for msg in res.messages do
    message = JSON.parse(msg.body)
    p message
    delete_entries.push({id: id.to_s, receipt_handle: msg.receipt_handle})
    id += 1
  end

  # 処理済みメッセージを削除
  if delete_entries.size > 0 then # 0件でdelete_message_batchを呼び出すとエラーになる
    sqs_client.delete_message_batch({
      queue_url: queue_url,
      entries: delete_entries,
    })
  else
    break
  end
end

実行してみます。

$ ruby sample.rb
{"msg"=>"Hello, world!", "foo"=>5}
{"msg"=>"Hello, world!", "foo"=>1}
{"msg"=>"Hello, world!", "foo"=>9}
{"msg"=>"Hello, world!", "foo"=>2}
{"msg"=>"Hello, world!", "foo"=>7}
{"msg"=>"Hello, world!", "foo"=>10}
{"msg"=>"Hello, world!", "foo"=>3}
{"msg"=>"Hello, world!", "foo"=>4}
{"msg"=>"Hello, world!", "foo"=>6}
{"msg"=>"Hello, world!", "foo"=>8}

できました。順序は保証されていないので、受信したメッセージの順番はめちゃめちゃです。