<mazgi.github.io 移行済>Amazon SageMakerをそれなりの人数で使うときの設定

移動しました

mazgi.log :: Amazon SageMakerをそれなりの人数で使うときの設定


AWSのマネージドJupyterサービスである「Amazon SageMaker」を数十名規模で使う機会があったのでインフラ的に設定した内容などを書いておく。
SageMakerで何をしたかなどはいずれちゃんとした情報が出ると思う。

Amazon SageMakerとは?

AWSのWebUIでぽちぽちクリックしていくとJupyterが起動する、そういうやつです。
SageMakerのオフィシャルサイトはこちら。 aws.amazon.com

Amazon SageMakerの起動方法

まずアクセスすべきはこちら。 https://console.aws.amazon.com/sagemaker/home#/notebook-instances

「Create notebook instance」ボタンをクリックするとこのような画面になるので項目を埋めていく。 f:id:mazgi:20180305225954p:plain

ある程度の人数で使う場合には以下のような項目を取り決めておくと良さそう。

  • Notebook instance name
    • 文字通りNotebookインスタンスの名前になるほか、Jupyter NotebookのURLの一部としても使われる
    • 誰が作ったかとか何の目的なのかとか命名ルールを決めておくと良さそう
  • Notebook instance type
    • いくつか選べる
    • Notebookインスタンスは純粋にNotebookであって、このインスタンスでjobが動くわけではないのでそんなに強力なインスタンスでなくてもよい
    • なお実際にjobを実行するインスタンスでは p2/p3 などのGPUインスタンスも選べる
  • IAM role(後述)
    • Notebookインスタンスに与えるRole、この権限でjobなどのインスタンスを作ろうとする
    • Roleを作成するか既存のRoleから選択するかなどが選べる
    • チームやグループで使うのであればあらかじめRoleを作っておいてARNを入力してもらう方が管理上よいと思う
  • Custom IAM role ARN
    • 前述の「IAM role」で既存のRoleを選ぶことにすると表示される
    • 私はあらかじめ作っておいたRoleを入力してもらうことにした
  • VPC
    • とりあえず使うだけなら No VPC でよい
    • ここで指定したVPCにSageMakerがアクセスできるようになるらしい(未検証)
    • "Notebook instances will have internet access independent of your VPC setting."とある通り、ここでVPCを指定したからといってSageMakerやNotebookを特定のVPCに閉じ込めることはできないので注意

以上のような項目を埋めて、ページ下部の「Create notebook instance」ボタンをクリックするとNotebookインスタンスが準備されるので「InService」になるまで数分〜10数分程度待つ。
案外時間がかかるが裏側でプロビジョニングなどを行なっているのでしょう。

「InService」になったNotebookインスタンスの詳細をみると右上に「Open」ボタンがあるのでクリックする。 f:id:mazgi:20180305230017p:plain

そうすると別タブで見慣れたJupyter Notebookの画面が開くので好きなように使う。 f:id:mazgi:20180305230045p:plain

こんなに楽だと自分でJupyter Notebook立てるモチベーションがなくなって良いですね。

IAM Role/Group作成

以下を用意した

  • IAM Role
    • 前述の、Notebookインスタンスに与えるRole
    • 最小限 AmazonSageMakerFullAccess をアタッチしておけば良い
  • IAM Group
    • 今回は1つのAWSアカウントで数十名が同時にSageMakerを使うため、グループを作ってユーザーを紐づけることにした
    • 以下をアタッチした(もう少し絞り込めそうではある)
      • AmazonSageMakerFullAccess
      • AmazonEC2FullAccess
      • IAMReadOnlyAccess
      • IAMUserChangePassword (これは運用上必要だっただけ)
  • IAM User(s)
    • SageMakerを使う人全員分のユーザーを払い出し、前述のグループに紐付けた
    • 今回は初期パスワードを私の手元でコンソール出力し、各自初回ログイン時に変更してもらった

以上の設定をTerraformで行う .tf ファイルの例は次の通り。
内容はほぼそのまま、個々のユーザー名は別途変数から読んでいる。

gist.github.com

AWSリソース上限緩和申請

AWS SAの方にご相談の上、SageMakerを利用する IAM User数 * n で事前にリソース上限の緩和申請を行なった。
デフォルト値はこちら。
AWS サービス制限 - アマゾン ウェブ サービス

内容はざっと次の通り。
各ユーザーがNotebookインスタンスを1つ、学習と推論のjobを2つずつ実行することを想定した。

  • SageMaker のホスト
    • インスタンス数: IAM User数 * 3
    • (使用するインスタンスタイプ): IAM User数 * 3
    • エンドポイントのインスタンス数: IAM User数 * 3
      • これは不要だったようだ
  • SageMaker のトレーニング
    • インスタンス数: IAM User数 * 3
    • トレーニングジョブのインスタンス数: IAM User数 * 3
    • (使用するインスタンスタイプ): IAM User数 * 3
  • SageMaker ノートブック
    • (使用するインスタンスタイプ): IAM User数 * 1.5
    • 実行中のノートブックインスタンスの数: IAM User数 * 1.5

一部CloudTrailの上限に引っかかり同時実行できなかったりもしたが、概ねこれでユーザー全員が目的の操作を行えた。
AWSのリソース上限はなかなか難しいのでAWS SAの方に相談するに限る🙏

感想

プロビジョニングしてルールを決めておけば使いたい人にサクッと使ってもらえて大変便利。
各種セキュリティ担保の方法や込み入った使い方については今後使ってもらいながら工夫していきたい。

<mazgi.github.io 移行済>SSE-KMSで暗号化したS3バケットをs3fsでmountする

移動しました=> mazgi.log :: SSE-KMSで暗号化したS3バケットをs3fsでmountする

タイトルの通り「AWS Key Management Service (AWS KMS) 」を使って暗号化したAmazon S3バケットをs3fsでUbuntu 16上でmountした。
KMSについては以下のドキュメントが詳しいが要は暗号化の際に煩雑な鍵の管理をAWSにお願いできる仕組み。

docs.aws.amazon.com

S3バケットの準備

S3バケットを作り、画像のように Default encryptionAWS-KMS に設定する。
なおこのS3バケットは記事公開時点で削除済み。

f:id:mazgi:20180301035936p:plain

s3fsの設定

Install

GitHubからアーカイブをダウンロードして

$ ./autogen.sh
$ ./configure
$ make
$ sudo make install

する。

github.com

mount

以下のようにAWSのcredentialを .secret というファイルに ACCESS_KEY:SECRET_KEY というフォーマットで書く。
またKMSの鍵IDを環境変数に設定した。

$ cat .secret
****ACCESS_KEY****:****SECRET_KEY****
$ export AWSSSEKMSID='********'

そしてmountする。
endpoint, uid, gid , umask あたりをきちんと設定しないと読み書きできない、ハマった。
なお鍵IDは環境変数使わなくても use_sse=kmsid:"${AWSSSEKMSID}" でいける模様。

また -d はdebug、 -f はフォアグラウンド実行。

$ s3fs mazgi-s3-sse-kms-test-01-bucket-01 bucket -o passwd_file=.secret,use_sse=kmsid,endpoint=ap-northeast-1,allow_other,uid=1234,gid=1234,umask=227 -d -f
[CRT] s3fs.cpp:set_s3fs_log_level(271): change debug level from [CRT] to [INF] 
[INF]     s3fs.cpp:set_mountpoint_attribute(4206): PROC(uid=4600, gid=4600) - MountPoint(uid=4600, gid=4600, mode=40775)
[INF] s3fs.cpp:s3fs_init(3371): init v1.83(commit:unknown) with OpenSSL
[INF] s3fs.cpp:s3fs_check_service(3747): check services.
[INF]       curl.cpp:CheckBucket(3068): check a bucket.
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/
[INF]       curl.cpp:insertV4Headers(2400): computing signature [GET] [/] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2051): HTTP response code 200

ファイル操作

ls してみる。

なおS3バケットに入っているJPEG画像はこれ。かわいい。
ぱくたそ」からお借りした。

f:id:mazgi:20180301042415j:plain

$ ls -l bucket
total 179
-r-xr-x--- 1 user group 96870 Feb  1 07:10 cat.jpg*
-r-xr-x--- 1 user group 84999 Feb  1 07:13 cat_plain.jpg*

その時のコンソールログ。

[INF] s3fs.cpp:s3fs_getattr(841): [path=/]
[INF] s3fs.cpp:s3fs_opendir(2281): [path=/][flags=100352]
[INF] s3fs.cpp:s3fs_readdir(2432): [path=/]
[INF]   s3fs.cpp:list_bucket(2477): [path=/]
[INF]       curl.cpp:ListBucketRequest(3103): [tpath=/]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01?delimiter=/&max-keys=1000&prefix=
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com?delimiter=/&max-keys=1000&prefix=
[INF]       curl.cpp:insertV4Headers(2400): computing signature [GET] [/] [delimiter=/&max-keys=1000&prefix=] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2051): HTTP response code 200
[WAN] s3fs.cpp:append_objects_from_xml_ex(2575): contents_xp->nodesetval is empty.
[INF]   s3fs.cpp:readdir_multi_head(2346): [path=/][list=0]
[INF]       curl.cpp:PreHeadRequest(2657): [tpath=/cat.jpg][bpath=cat.jpg][save=/cat.jpg][sseckeypos=-1]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/cat.jpg
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/cat.jpg
[INF]       curl.cpp:insertV4Headers(2400): computing signature [HEAD] [/cat.jpg] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:PreHeadRequest(2657): [tpath=/cat_plain.jpg][bpath=cat_plain.jpg][save=/cat_plain.jpg][sseckeypos=-1]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/cat_plain.jpg
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/cat_plain.jpg
[INF]       curl.cpp:insertV4Headers(2400): computing signature [HEAD] [/cat_plain.jpg] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:Request(3999): [count=2]
[INF]       curl.cpp:RequestPerform(2051): HTTP response code 200
[INF]       curl.cpp:RequestPerform(2051): HTTP response code 200
[INF]       cache.cpp:AddStat(356): add stat cache entry[path=/cat_plain.jpg]
[INF]       cache.cpp:AddStat(356): add stat cache entry[path=/cat.jpg]
[INF] s3fs.cpp:s3fs_getattr(841): [path=/cat.jpg]
[INF] s3fs.cpp:s3fs_getattr(841): [path=/cat_plain.jpg]

アップロード前のチェックサムがこれ。

$ shasum -a 1 cat.jpg
fb9f3c47ad3d91ced2e62c82f0ae753330351b32  cat.jpg

mountしたS3バケットから読み取りテスト兼ねてチェックサムを取得してみる。
一致しているので正しく読み取れていることがわかる。

$ sha1sum bucket/cat.jpg
fb9f3c47ad3d91ced2e62c82f0ae753330351b32  bucket/cat.jpg

ファイル読み取り時のコンソールログ。

[INF] s3fs.cpp:s3fs_getattr(841): [path=/cat.jpg]
[INF] s3fs.cpp:s3fs_open(2063): [path=/cat.jpg][flags=32768]
[INF]       cache.cpp:DelStat(565): delete stat cache entry[path=/cat.jpg]
[INF]       curl.cpp:HeadRequest(2708): [tpath=/cat.jpg]
[INF]       curl.cpp:PreHeadRequest(2657): [tpath=/cat.jpg][bpath=][save=][sseckeypos=-1]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/cat.jpg
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/cat.jpg
[INF]       curl.cpp:insertV4Headers(2400): computing signature [HEAD] [/cat.jpg] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2051): HTTP response code 200
[INF]       cache.cpp:AddStat(356): add stat cache entry[path=/cat.jpg]
[INF]       fdcache.cpp:SetMtime(1019): [path=/cat.jpg][fd=7][time=1517436613]
[INF]       curl.cpp:GetObjectRequest(3043): [tpath=/cat.jpg][start=0][size=96870]
[INF]       curl.cpp:PreGetObjectRequest(2983): [tpath=/cat.jpg][start=0][size=96870]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/cat.jpg
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/cat.jpg
[INF]       curl.cpp:insertV4Headers(2400): computing signature [GET] [/cat.jpg] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:GetObjectRequest(3058): downloading... [path=/cat.jpg][fd=7]
[INF]       curl.cpp:RequestPerform(2051): HTTP response code 206
[INF] s3fs.cpp:s3fs_getattr(841): [path=/cat.jpg]
[INF] s3fs.cpp:s3fs_flush(2185): [path=/cat.jpg][fd=7]
[INF]       fdcache.cpp:RowFlush(1434): [tpath=][path=/cat.jpg][fd=7]
[INF] s3fs.cpp:s3fs_release(2238): [path=/cat.jpg][fd=7]
[INF]       fdcache.cpp:GetFdEntity(1995): [path=/cat.jpg][fd=7]

今度はS3バケットにファイルを書き込んでみる。
適当にファイルを作りチェックサムを取得。

$ head -1 /dev/urandom|od -x > rand.txt
$ sha1sum rand.txt
bb02ee0d5fc5b459ca1978fcc0e53649d144554c  rand.txt

マウントポイントにコピーする。
コピー後のチェックサムが一致しているので正しくコピーできたことがわかる。

$ cp rand.txt bucket/
$ sha1sum bucket/rand.txt
bb02ee0d5fc5b459ca1978fcc0e53649d144554c  bucket/rand.txt

書き込み時のコンソールログ。

[INF] s3fs.cpp:s3fs_getattr(841): [path=/]
[INF] s3fs.cpp:s3fs_getattr(841): [path=/rand.txt]
[INF]       curl.cpp:HeadRequest(2708): [tpath=/rand.txt]
[INF]       curl.cpp:PreHeadRequest(2657): [tpath=/rand.txt][bpath=][save=][sseckeypos=-1]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/rand.txt
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/rand.txt
[INF]       curl.cpp:insertV4Headers(2400): computing signature [HEAD] [/rand.txt] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2073): HTTP response code 404 was returned, returning ENOENT
[INF]       curl.cpp:HeadRequest(2708): [tpath=/rand.txt/]
[INF]       curl.cpp:PreHeadRequest(2657): [tpath=/rand.txt/][bpath=][save=][sseckeypos=-1]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/rand.txt/
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/rand.txt/
[INF]       curl.cpp:insertV4Headers(2400): computing signature [HEAD] [/rand.txt/] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2073): HTTP response code 404 was returned, returning ENOENT
[INF]       curl.cpp:HeadRequest(2708): [tpath=/rand.txt_$folder$]
[INF]       curl.cpp:PreHeadRequest(2657): [tpath=/rand.txt_$folder$][bpath=][save=][sseckeypos=-1]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/rand.txt_%24folder%24
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/rand.txt_%24folder%24
[INF]       curl.cpp:insertV4Headers(2400): computing signature [HEAD] [/rand.txt_$folder$] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2073): HTTP response code 404 was returned, returning ENOENT
[INF]   s3fs.cpp:list_bucket(2477): [path=/rand.txt]
[INF]       curl.cpp:ListBucketRequest(3103): [tpath=/rand.txt]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01?delimiter=/&max-keys=2&prefix=rand.txt/
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com?delimiter=/&max-keys=2&prefix=rand.txt/
[INF]       curl.cpp:insertV4Headers(2400): computing signature [GET] [/] [delimiter=/&max-keys=2&prefix=rand.txt/] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2051): HTTP response code 200
[WAN] s3fs.cpp:append_objects_from_xml_ex(2575): contents_xp->nodesetval is empty.
[WAN] s3fs.cpp:append_objects_from_xml_ex(2575): contents_xp->nodesetval is empty.
[INF] s3fs.cpp:s3fs_getattr(841): [path=/rand.txt]
[INF]       curl.cpp:HeadRequest(2708): [tpath=/rand.txt]
[INF]       curl.cpp:PreHeadRequest(2657): [tpath=/rand.txt][bpath=][save=][sseckeypos=-1]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/rand.txt
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/rand.txt
[INF]       curl.cpp:insertV4Headers(2400): computing signature [HEAD] [/rand.txt] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2073): HTTP response code 404 was returned, returning ENOENT
[INF]       curl.cpp:HeadRequest(2708): [tpath=/rand.txt/]
[INF]       curl.cpp:PreHeadRequest(2657): [tpath=/rand.txt/][bpath=][save=][sseckeypos=-1]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/rand.txt/
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/rand.txt/
[INF]       curl.cpp:insertV4Headers(2400): computing signature [HEAD] [/rand.txt/] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2073): HTTP response code 404 was returned, returning ENOENT
[INF]       curl.cpp:HeadRequest(2708): [tpath=/rand.txt_$folder$]
[INF]       curl.cpp:PreHeadRequest(2657): [tpath=/rand.txt_$folder$][bpath=][save=][sseckeypos=-1]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/rand.txt_%24folder%24
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/rand.txt_%24folder%24
[INF]       curl.cpp:insertV4Headers(2400): computing signature [HEAD] [/rand.txt_$folder$] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2073): HTTP response code 404 was returned, returning ENOENT
[INF]   s3fs.cpp:list_bucket(2477): [path=/rand.txt]
[INF]       curl.cpp:ListBucketRequest(3103): [tpath=/rand.txt]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01?delimiter=/&max-keys=2&prefix=rand.txt/
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com?delimiter=/&max-keys=2&prefix=rand.txt/
[INF]       curl.cpp:insertV4Headers(2400): computing signature [GET] [/] [delimiter=/&max-keys=2&prefix=rand.txt/] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2051): HTTP response code 200
[WAN] s3fs.cpp:append_objects_from_xml_ex(2575): contents_xp->nodesetval is empty.
[WAN] s3fs.cpp:append_objects_from_xml_ex(2575): contents_xp->nodesetval is empty.
[INF] s3fs.cpp:s3fs_create(999): [path=/rand.txt][mode=100664][flags=32961]
[INF]       curl.cpp:HeadRequest(2708): [tpath=/rand.txt]
[INF]       curl.cpp:PreHeadRequest(2657): [tpath=/rand.txt][bpath=][save=][sseckeypos=-1]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/rand.txt
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/rand.txt
[INF]       curl.cpp:insertV4Headers(2400): computing signature [HEAD] [/rand.txt] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2073): HTTP response code 404 was returned, returning ENOENT
[INF]       curl.cpp:HeadRequest(2708): [tpath=/rand.txt/]
[INF]       curl.cpp:PreHeadRequest(2657): [tpath=/rand.txt/][bpath=][save=][sseckeypos=-1]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/rand.txt/
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/rand.txt/
[INF]       curl.cpp:insertV4Headers(2400): computing signature [HEAD] [/rand.txt/] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2073): HTTP response code 404 was returned, returning ENOENT
[INF]       curl.cpp:HeadRequest(2708): [tpath=/rand.txt_$folder$]
[INF]       curl.cpp:PreHeadRequest(2657): [tpath=/rand.txt_$folder$][bpath=][save=][sseckeypos=-1]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/rand.txt_%24folder%24
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/rand.txt_%24folder%24
[INF]       curl.cpp:insertV4Headers(2400): computing signature [HEAD] [/rand.txt_$folder$] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2073): HTTP response code 404 was returned, returning ENOENT
[INF]   s3fs.cpp:list_bucket(2477): [path=/rand.txt]
[INF]       curl.cpp:ListBucketRequest(3103): [tpath=/rand.txt]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01?delimiter=/&max-keys=2&prefix=rand.txt/
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com?delimiter=/&max-keys=2&prefix=rand.txt/
[INF]       curl.cpp:insertV4Headers(2400): computing signature [GET] [/] [delimiter=/&max-keys=2&prefix=rand.txt/] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2051): HTTP response code 200
[WAN] s3fs.cpp:append_objects_from_xml_ex(2575): contents_xp->nodesetval is empty.
[WAN] s3fs.cpp:append_objects_from_xml_ex(2575): contents_xp->nodesetval is empty.
[INF]     s3fs.cpp:create_file_object(960): [path=/rand.txt][mode=100664]
[INF]       curl.cpp:PutRequest(2872): [tpath=/rand.txt]
[INF]       curl.cpp:PutRequest(2889): create zero byte file object.
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/rand.txt
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/rand.txt
[INF]       curl.cpp:insertV4Headers(2400): computing signature [PUT] [/rand.txt] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:PutRequest(2969): uploading... [path=/rand.txt][fd=-1][size=0]
[INF]       curl.cpp:RequestPerform(2051): HTTP response code 200
[INF]       cache.cpp:DelStat(565): delete stat cache entry[path=/rand.txt]
[INF]       curl.cpp:HeadRequest(2708): [tpath=/rand.txt]
[INF]       curl.cpp:PreHeadRequest(2657): [tpath=/rand.txt][bpath=][save=][sseckeypos=-1]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/rand.txt
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/rand.txt
[INF]       curl.cpp:insertV4Headers(2400): computing signature [HEAD] [/rand.txt] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2051): HTTP response code 200
[INF]       cache.cpp:AddStat(356): add stat cache entry[path=/rand.txt]
[INF] s3fs.cpp:s3fs_getattr(841): [path=/rand.txt]
[INF] s3fs.cpp:s3fs_flush(2185): [path=/rand.txt][fd=7]
[INF]       fdcache.cpp:RowFlush(1434): [tpath=][path=/rand.txt][fd=7]
[INF]       curl.cpp:PutRequest(2872): [tpath=/rand.txt]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/rand.txt
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/rand.txt
[INF]       curl.cpp:insertV4Headers(2400): computing signature [PUT] [/rand.txt] [] [2a5b392dff6867a115948ff04fbec762a6f007cffebf40544c62308ec9eab099]
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:PutRequest(2969): uploading... [path=/rand.txt][fd=7][size=996]
[INF]       curl.cpp:RequestPerform(2051): HTTP response code 200
[INF] s3fs.cpp:s3fs_release(2238): [path=/rand.txt][fd=7]
[INF]       cache.cpp:DelStat(565): delete stat cache entry[path=/rand.txt]
[INF]       fdcache.cpp:GetFdEntity(1995): [path=/rand.txt][fd=7]

こちらはチェックサム取得時のコンソールログ。

[INF] s3fs.cpp:s3fs_getattr(841): [path=/rand.txt]
[INF]       curl.cpp:HeadRequest(2708): [tpath=/rand.txt]
[INF]       curl.cpp:PreHeadRequest(2657): [tpath=/rand.txt][bpath=][save=][sseckeypos=-1]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/rand.txt
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/rand.txt
[INF]       curl.cpp:insertV4Headers(2400): computing signature [HEAD] [/rand.txt] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2051): HTTP response code 200
[INF]       cache.cpp:AddStat(356): add stat cache entry[path=/rand.txt]
[INF] s3fs.cpp:s3fs_open(2063): [path=/rand.txt][flags=32768]
[INF]       cache.cpp:DelStat(565): delete stat cache entry[path=/rand.txt]
[INF]       curl.cpp:HeadRequest(2708): [tpath=/rand.txt]
[INF]       curl.cpp:PreHeadRequest(2657): [tpath=/rand.txt][bpath=][save=][sseckeypos=-1]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/rand.txt
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/rand.txt
[INF]       curl.cpp:insertV4Headers(2400): computing signature [HEAD] [/rand.txt] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:RequestPerform(2051): HTTP response code 200
[INF]       cache.cpp:AddStat(356): add stat cache entry[path=/rand.txt]
[INF]       fdcache.cpp:SetMtime(1019): [path=/rand.txt][fd=7][time=1517862525]
[INF]       curl.cpp:GetObjectRequest(3043): [tpath=/rand.txt][start=0][size=996]
[INF]       curl.cpp:PreGetObjectRequest(2983): [tpath=/rand.txt][start=0][size=996]
[INF]       curl.cpp:prepare_url(4253): URL is https://s3.amazonaws.com/mazgi-s3-sse-kms-test-01-bucket-01/rand.txt
[INF]       curl.cpp:prepare_url(4285): URL changed is https://mazgi-s3-sse-kms-test-01-bucket-01.s3.amazonaws.com/rand.txt
[INF]       curl.cpp:insertV4Headers(2400): computing signature [GET] [/rand.txt] [] []
[INF]       curl.cpp:url_to_host(101): url is https://s3.amazonaws.com
[INF]       curl.cpp:GetObjectRequest(3058): downloading... [path=/rand.txt][fd=7]
[INF]       curl.cpp:RequestPerform(2051): HTTP response code 206
[INF] s3fs.cpp:s3fs_getattr(841): [path=/rand.txt]
[INF] s3fs.cpp:s3fs_flush(2185): [path=/rand.txt][fd=7]
[INF]       fdcache.cpp:RowFlush(1434): [tpath=][path=/rand.txt][fd=7]
[INF] s3fs.cpp:s3fs_release(2238): [path=/rand.txt][fd=7]
[INF]       fdcache.cpp:GetFdEntity(1995): [path=/rand.txt][fd=7]

以上、ちょっとハマったができてみるとあっさり暗号化したS3バケットが扱えた。

で、「これをprovisioningするの時間がないなー」とか思ってたら斜め後ろのベテランエンジニアがサクッとitamaeのrecipeにしてくれた。感謝。

<mazgi.github.io 移行済>S3+CloudFrontをTerraformで設定してCircleCIで更新する

移動しました=> mazgi.log :: S3 + CloudFrontをTerraformで設定してCircleCIで更新する

「TerraformでS3+CloudFront+SSL/TLS証明書 w/ ACMを設定してHugoで作ったstaticなWebサイトをCircleCIで自動deployする」やつができた。

できたもの

普通のいかにもHugoで作ったWebサイトができた。
もう2018年なので手オペなどせずInfrastructure as Codeで構築かつCIでコンテンツdeployです。
中身はまだない。
きっと酒とメシについての何かが書かれるのでしょう。

f:id:mazgi:20180215023829p:plain https://sakemeshi.love/

これはそもそも先日開催したハッカソンでやろうとして途中までしか進められなかったので、その補習も兼ねてます。

なお次回ハッカソンはGoです!

denatechstudio.connpass.com

構成

図を描く気力がなかったのでテキストで。

インフラ構築編

Terraformで以下を行なっている。

  1. Route 53にドメインのゾーン情報を登録する
  2. コンテンツ更新用のIAMグループとIAMユーザーを払い出す
  3. コンテンツ格納用のS3バケットを作る
  4. ACMでSSL/TLS証明書を発行する
  5. CloudFront distributionを設定する(ACMの証明書使いたいので)
  6. CloudFront distributionをRoute 53に登録する

コンテンツ更新編

GitHubへのpushをトリガーにCircleCIで以下を行なっている。

  1. HugoでstaticなWebコンテンツ生成する
  2. 生成したWebコンテンツをS3に同期する
  3. CloudFrontのキャッシュをクリアする

GitHub

GitHubで以下を管理している。

  • sakemeshi.terraform
    • Terraformリポジトリ
    • 中身はRoute 53にドメインのゾーンを登録してmoduleを呼び出しているくらい
    • AWSのcredential等は sakemeshi.terraform-secret リポジトリに置いてsubmoduleとして参照している
  • terraform-aws-static-website
    • 自分用Terraform module
    • S3+CloudFrontでstaticなWebサイトを作るもろもろが詰まっている
  • (private) sakemeshi.terraform-secret
    • AWSのcredentialなどが入っている
    • credential store導入とかはまた今度
  • (private) sakemeshi.content
    • HugoによるWebサイトの中身
    • このリポジトリのmasterにpushするとCircleCIが動いてS3にdeployされる

つくりかた

コンセプトとしてはWebUIを手でぽちぽちしたくないのでTerraformでInfra as Codeします!

AWS Webコンソールぽちぽち

最初からコンセプトに反するようだが2018年時点では手でポチポチすることもまだ必要なのです。(あるいはCLI)
作るものは2つ。

Terraform 実行用 IAM User

terraform-admin という名前で AdministratorAccess をattacheしたIAMユーザーを作る。
credentialも払い出して、今回の場合は sakemeshi.terraform-secret リポジトリにpushしておく。

f:id:mazgi:20180215033043p:plain

tfstate 格納用 S3 Bucket

Terraformは設定したクラウド環境の状態を tfstate というファイルで管理するのでそれを格納するS3バケットを作る。

バケット名は ${AWSアカウント名}-terraform としてバージョニングを有効にする。
先ほど作ったIAMユーザー terraform-admin からアクセスできれば良い。

f:id:mazgi:20180215033214p:plain

詳しくは以下参照。

Backend Type: s3 - Terraform by HashiCorp

Infra as Terraform

インフラをコードで書く

Terraformで構築するもろもろは専用のGitHubリポジトリを作って terraform.tf に書く。
先述の通りここで公開している。

github.com

主な内容は以下の通り。

  • prepare.sh
    • terraformバイナリのダウンロードと展開
    • 必要な環境変数のexport
    • なお命名は職場の文化に由来する
  • terraform.tf
    • Terraformで行いたいもろもろ
    • ただしほとんどはmoduleに追い出しているので、実際の内容はRoute 53のゾーン設定とmoduleの読み出し程度
  • terraform.tfvars
    • 後述のprivateなcredentialリポジトリ内へのsymlink
    • AWSのACCESS_KEYやSECRET_KEYなどが書かれている

terraform.tf で参照しているmoduleは公開しているが後述のエラーやリージョンが固定などの残課題がある。
Terraform Module Registry

先述の通りTerraformを実行するIAMユーザーのcredential等は別のprivateリポジトリを作って配置している。
公開できないが中身はこの程度。

f:id:mazgi:20180215043508p:plain

Terraform 実行

ようやくTerraformを実行できる環境が整ったので実行!
なお prepare.sh は環境変数をexportするので必ず source する。

$ source prepare.sh
$ bin/terraform apply

実行すると初回はもろもろのリソースが作られ、1件のエラーが発生する。
2回目以降はこのように1件のエラーが発生する。。
後述する。

f:id:mazgi:20180215035427p:plain

細かいことを書くと今回のドメイン自体はRoute 53で管理していないため、別途ネームサーバーをRoute 53に向けておく等の手順が必要です。

CircleCIで自動build && deploy

Terraformで空のWebサイトができたので中身を作っていく。

GitHubリポジトリを用意しておもむろに hugo new site . する。
適当なテーマをsubmoduleとして追加する。
Quick Start通り。

Hugoは今後がんばることにして今回はCircleCI 2.0をがんばる。
結論を書くとこういう .circleci/config.yml を書いた。
ハマった。

そのほかは本当に hugo new site . したまま。

f:id:mazgi:20180215045406p:plain

何はともあれこれでCircleCIでのWebサイトのビルドとS3への同期が行えるようになった!めでたい!

f:id:mazgi:20180215045503p:plain

おわりに

ということで(ほぼ)手でぽちぽちせずにInfrastructure as CodeでstaticなWebサイトが作れたし更新の仕組みもできた!やったね!

ポイント

以下あまり書けてないので機会があったら書く。

Terraform

  • 最近のTerraformは terraform init が必要
    • その作業ディレクトリでの初回実行やmoduleのバージョン上げた際など
  • 最近のTerraformは terraform apply したあと本当に実行するか聞いてくる
    • そのため terraform apply[ENTER] してコーヒーを買いにいくと何十分後に戻っても Do you want to perform these actions? というメッセージが出迎えてくれる(もちろんあなたのクラウド環境には何も変化はない)
    • terraform apply -auto-approve というわるいオプションがある
  • tfstate格納用S3バケット(S3 backend)を使うためには環境変数or ~/.aws 内にcredentialが必要
    • 私は prepare.sh の中でexportしている(ので必ず source する)
  • ACMの検証が従来のメールだけではなくDNSでもできるようになっていてTerraformでも対応していた

CircleCI 2.0

  • Docker便利だけども
    • 使うDockerイメージにもよるが当然 curlgit も入ってなかったりする
    • 自分用Dockerイメージ作りたくなる
    • 開発環境がDocker Hubに公開できるDockerイメージで完結している場合は便利そう
  • checkoutgit clone --recursive 相当の機能がない(?)

(特に)ハマりどころ

terraform apply 時の aws_acm_certificate_validation エラー

terraform apply する際に毎回このエラーが発生する。

Error: Error applying plan:

1 error(s) occurred:

* module.static-website.aws_acm_certificate_validation.website: 1 error(s) occurred:

* aws_acm_certificate_validation.website: Certificate needs [VALIDATON_RECORD.YOURDOMAIN VALIDATON_RECORD.YOURDOMAIN] to be set but only [VALIDATON_RECORD.YOURDOMAIN] was passed to validation_record_fqdns

これ YOURDOMAIN*.YOURDOMAIN でバリデーションレコードが同じで、Route 53上は1レコードしかないものをTerraformのAWS providerが2レコード返ってくることを期待しているように見えるんだけどどうしたものか。

CircleCI 2.0 で attach_workspace するために ca-certificates パッケージが必要

前フェイズで永続化したワークスペースを後フェイズで attach_workspace して取り出すために ca-certificates パッケージ必要なのはハマった。
こちらの記事に助けられた。
CircleCIでx509という証明書エラーに遭遇したときの対処

それはそうと再掲するが .circleci/config.yml がこんな長さになった。つらい。
workflowとしてbuildとdeployが分かれるのは綺麗だけども。。

gist.github.com

ともかくこういうサイトをいくつか作りたいニーズがあったのでやっていきます。

<mazgi.github.io 移行済>簡易な技術ドキュメントをHugoで書くと便利だった

この記事は以下に移行しました。

mazgi.log :: 簡易な技術ドキュメントをHugoで書くと便利だった


サンプルコードのドキュメントをHugoで書いてサンプルコードと一緒に配ったら便利そうだったのでやってみた。

gohugo.io

やりたいこと

仕事で他社さんにサンプルコードとドキュメントをセットでお渡ししたいのだけど社ではGitHub Enterpriseを使っているのでリポジトリを直接見ていただくことが難しいケースがある。

規模が大きいプロジェクトならSphinx使うと(やる気次第で)いくらでも綺麗なドキュメントが書けそうではあるけど、規模がそれほどじゃないうちはサクッとMarkdownで書いて、でもソースコードとドキュメントが整合性取れててほしいという気持ちになる。
(今はSphinxもMarkdownで書けるそうだ、最近知った)

そもそも(私は)ドキュメント書きたくないので、できるだけスクリプトの提供やソースコードコメントで賄って文章量は最小限に抑えたいというモチベーションもあった。

そこで以下のような作戦を考えた。

さくせん

  • ドキュメントはHugoでプレビューしながらMarkdownで書く
  • 生成したHTMLは /docs ディレクトリに突っ込む
  • 社内向けにはGitHub PagesでmasterのHEADをホスティングする
  • 社外向けには最新リリースのアーカイブを提供する

プレビューできるから書くの楽だし、tag打てるからソースコードとドキュメントの整合性も取りやすい。きっと便利!

できたもの

リポジトリはこちら。

github.com

GitHub Pagesはこちら。

Example Document with Hugo

テーマはこちらを使わせていただいた。
サイドバーでエントリが一覧できて技術ドキュメントらしさがある。

github.com

工夫

設定ファイルはこれ。

  • 以下で生成したHTML内のリンクが / からの相対PATHになるふいんき(ちゃんと調べていない)
    • baseURL = "/"
    • relativeURLs = true
    • uglyurls = true
  • themeの指定
baseURL = "/"
languageCode = "en-us"
DefaultContentLanguage = "en"
title = "Example Document with Hugo"
publishDir = "../docs"
relativeURLs = true
uglyurls = true
theme = "docdock"

[params]
themeVariant = "gray"

また、この設定ファイルとドキュメントのソースコード(Markdown他)を /docs.source に置き、 publishDir = "../docs" と設定することでリポジトリのrootにあまりファイルを置かないようにしている。
これはこのリポジトリにサンプルコードも同居する想定であり、rootにファイルが増えて見通しがわるくなることを避けるため。

HTMLを生成してcommitしてpushしてtagを打ったものがこちら。

Release v0.0.1 · mazgi/example-document-with-hugo · GitHub

アーカイブをダウンロードして手元で開くとこんな感じで index.html が見える。
アーカイブファイルにも展開後のディレクトリにもバージョンナンバーが含まれわかりやすい。

f:id:mazgi:20171207195147p:plain

index.html もリンク先のドキュメントもブラウザで表示できる。便利。

f:id:mazgi:20171207195157p:plain

<mazgi.github.io 移行済>EC2 P3で使えるChainerMN入りのDockerイメージを作った

移動しました=> mazgi.log :: EC2 P3で使えるChainerMN入りのDockerイメージを作った

sonots先生によるこの記事をやってみたという話です。

qiita.com

概要

Dockerfileはここにあります。

github.com

ベースはNVIDIAさんのオフィシャルイメージです。

https://hub.docker.com/r/nvidia/cuda/

実行結果

ChainerMNのexampleを試した結果はこちら。
今のところシングルNodeシングルGPUとシングルNodeマルチGPUしか試してないです。

gist.github.com

手順はこちらの通りなんですが時間が取れていない...

docker (nvidia-docker) を使ってマルチノードで ChainerMN を実行する方法(仮)

P3じゃなくても動くはずですが、ホスト側にGPUとnvidia-dockerは必要です。
今回は社の環境で試したのでsonots便利先生環境の恩恵を受けてます🙏

blog.livedoor.jp

イチから構築する場合はこういう記事が参考になりそう。

chie8842.hatenablog.com

経緯とか

めでたく記事も出たので色々言えるようになったのですが、実はありがたいことにP3の先行検証というのをさせていただいてました。
(なおイベント当日はカメラマンしてました)

engineer.dena.jp

この検証時点ではChainerMNではなくChainerで、環境もVM上に直接作ってたのですが、その後部内から「Dockerイメージになってたほうが便利」とフィードバックいただき先行者の記事を参考に手探りしてる状況です。
私はMLわからないマンなのですが、最初 cuDNN 7 + CuPy 1.0.3 で環境作ろうとしてバージョンが合わなくてどうしよ!と思ってたら「今日 cuDNN 7 対応の CuPy 2.0 リリースするよ!」と教えていただいたりとか、この業界本当に数時間単位で進歩しててすごい。

こういう用途だとコンテナほんと便利なんですけど、でもDockerイメージの命名むずかしい。
mazgi/cuda-cv:9.0-cudnn7-devel-ubuntu16.04 じゃなくて、
mazgi/cuda-cv-9.0-cudnn7-devel-ubuntu16.04:latest とかにしたほうがいいのかな(長い)。
そもそもの話としては実質Chainer & ChainerMNイメージなのでそういう名前にすべきだし(さらに長くなる)。

<mazgi.github.io 移行済>Adobe CCのライセンスを別のPCに移す方法(ライセンス認証解除→再認証)

移行しました=> mazgi.log :: Adobe CCのライセンスを別のPCに移す方法(ライセンス認証解除→再認証)

Adobe CCが不要なPCの認証を解除して、使いたいPCで何かしらのアプリを起動すれば認証されて使えるようになる。
PC買い替えなどのタイミングで必要になるが、そういうときは大体ライセンス認証解除の手順を忘れているのでメモ。

手順

認証解除の手順を3行で。
なお認証解除はWeb上での操作なので、使うPCはなんでもいい(はず)。

  1. https://accounts.adobe.com/plansを開く
  2. 「プランを管理」をクリック
  3. 「ライセンス認証したデバイス」から不要な方のデバイスを削除

ここまでできれば、あとはAdobe CCを使いたいPCでPhotoshopやIllustratorなどのアプリを起動すれば認証され使えるようになる。

f:id:mazgi:20170212015541p:plain

f:id:mazgi:20170212015557p:plain

ちゃんとAdobeのヘルプに書いてあるのだが、公式ドキュメントなので「ライセンス認証とは?」やスタンドアローン版の説明も併記されていて文量が多いので簡単にまとめてみた。

helpx.adobe.com

<mazgi.github.io 移行済>Hadoop黙々会を始めました

移行しました=> mazgi.log :: Hadoop黙々会を始めました

@usaturnさんと一緒に「Hadoop黙々会」を始めました。 hadoop-bootup.connpass.com 私の思惑としては「エンジニアたる者、何歳になっても学び続けないとね。でも機会を作らないとなかなか集中して学べないので人を巻き込んでイベントにしてしまおう!」といったところです。

Read more