Quartz64 (Model A)にESXi ARM Editionを入れる

昨年購入したQuartz64に、ARM版ESXi 7.0を入れてみました。
導入は比較的簡単ですが、日本でQuartz64の記事をほぼ見かけないので記事を書きます。

なお、vmware Flingsに手順PDFがあります。英語がよければそちらで…
flings.vmware.com

Quartz64とは

Raspberry Pi4と性能が近い「Rockchip RK3566」を搭載したSBCです。
2022年現在も在庫があり、$80で8GB RAMを入手できます。
ESXi目的ならRaspberry Piの代わりに悪くない選択肢かと思います。

オンボードNICも、2022/7にリリースされたFling 1.10から使えるようになっています。
参考: ESXi-Arm Fling 1.10 Refresh - Arm at VMware

※1 SATAUSB3.0と共用です。USB3.0のまま使った方が良い気がします。
※2 PCIeもごく一部のボードは動くようですが、現時点ではないと思った方が良さそう

準備するもの

物理パーツ

ソフトウェア

手順

1. 必要なデータをメディアに書き込む

microSDカードにUEFIを書き込む
ボード本体にUEFIが載っていないため、microSDカードにUEFIを書き込みます。
BalenaEtcherで書き込めば特に手間取るところもないはず

USBメモリにESXiインストーラを書き込む
RufusでESXiのISOをUSBメモリに書き込みます。
こちらもデフォルトでOK

2. シリアル通信準備

ここが一番面倒なところ

① シリアルボードの結線

シリアルボードのTX/RX/GNDをQuartz64に接続します。

RS-232 Quartz64
GND PIN6
RXD PIN8
TXD PIN10

接続イメージ

5V線は接続不要です。
上で記載したボードを使っている場合、ボード上のスイッチはいずれもON(5V / LEDオン)でOKです。

② 残りの接続

USBはUSB2.0ポートに接続します。(プラスチックが白 or 黒のポート)
microSDカードをスロットに、シリアルボードをPCに、電源をコンセントに接続します。

③ シリアル通信設定

配線が終わったところでPC側でシリアル通信設定を行います。
ここではTeratermで説明します。

TeraTermでCOMポートを指定して起動します。
・デフォルトではSpeed(Baud rate)が違うため文字化けします。115200bpsに変更しましょう。
Setup > Serial Port で開けます。

・設定完了後、Quartz64のResetボタンを押せばログが流れ始めるはずです。

[トラブルシュート]
・ログが流れない場合→ UEFIの書き込みミス or 配線ミス
UEFIで止まる場合→ USBのポート間違い or ISOの書き込みミス
UEFI画面:

3. ESXiインストール

このようなESXiのログが出ていることを確認したところで数分待ちます。

待っていれば、おなじみのESXiのインストール画面が出ます。
インストーラを入れたUSBに上書きすること以外は特に困ることもないはずです。

インストール後、再起動すれば普段の画面が見られます。

Serial画面

ブラウザ画面

4. VM作成

ARM用のインストーラを使ってVMを作成します。
ここまで設定すれば特に困る項目もないため省略します。

この方の情報が参考になります。
VMware ESXi arm editionで各種Linuxディストリビューションを試す+USBブートしてみる - Qiita

5. おわりに

2022年現在、ARMでも仮想環境が実用できそうな状況が整いつつあります。
みなさんもARMなサーバを1台立ててみてはいかがでしょうか?
(もっとARMの情報が増えてほしい)

StableDiffusion (v1.4)を試した記録 その3 (アニメ絵を描かせたい)

StableDiffusionを試した記録、その3です。

今回の内容は、「StableDiffusionは写実系が得意」「MidJourneyのbetaでイラストが得意になった」等を見かけ、逆張りでアニメ絵を出そうと試行錯誤した記録です。

順番や指定ワードが雑なので、まだまだ改良の余地はあると思いますが、試したpromptを一通り公開します。

コマンド

※おことわり
メモ不足でワードの記載を間違っているものがあるかも

case 1

python optimizedSD/optimized_txt2img.py --prompt "vtuber, a girl, solo, digital art, sharp focus, hq, wallpaper, poster, hyper angle pose, dynamic angle, anime, game character, full body, popular illustration, 4k resolution, pixiv fanbox, portrait" --H 768 --W 576 --seed 1797548241 --n_iter 2 --n_samples 50 --ddim_steps 100

case 2

※ case1のstepsのみを変更しています。

python optimizedSD/optimized_txt2img.py --prompt "vtuber, a girl, solo, digital art, sharp focus, hq, wallpaper, poster, hyper angle pose, dynamic angle, anime, game character, full body, popular illustration, 4k resolution, pixiv fanbox, portrait" --H 768 --W 576 --seed 1797548241 --n_iter 2 --n_samples 50 --ddim_steps 150

case 3

英語指定はtwitterなどで私より上手い人がいるので、日本語指定を公開します。

python optimizedSD/optimized_txt2img.py --prompt "水着, 夏, 海, 女の子, 少女, solo, 一人, ソロ, 全身, アニメ, キャラクター, イラスト, 高画質, ピクシブ, fanbox" --H 512 --W 512 --seed 12 --n_iter 1 --n_samples 50 --ddim_steps 150

case 4

春っぽい画像を狙って指定

python optimizedSD/optimized_txt2img.py --prompt "女の子, 少女, 春, 桜, 制服, solo, 全身, 風景, アニメ, キャラクター, イラスト, 高画質, ピクシブ, fanbox, landscape" --H 576 --W 768 --seed 1731723048 --n_iter 1 --n_samples 50 --ddim_steps 100

case 5

逆に、冬っぽいイラストを狙ったもの
同じseedを使ったため、case 4とほぼ同じ構図が出ているのが面白いですね(3枚目同士)

python optimizedSD/optimized_txt2img.py --prompt "女の子, 少女, 冬, コート, 雪, solo, 全身, 風景, アニメ, キャラクター, イラスト, 高画質, ピクシブ, fanbox, landscape" --H 576 --W 768 --seed 1731723048 --n_iter 1 --n_samples 50 --ddim_steps 100

case 6

case 5の生成失敗を減らすため「alone」を追加
予想以上に影響が大きかった

python optimizedSD/optimized_txt2img.py --prompt "女の子, 少女, 冬, コート, 雪, solo, alone, 全身, 風景, アニメ, キャラクター, イラスト, 高画質, ピクシブ, fanbox, landscape" --H 576 --W 768 --seed 1731723048 --n_iter 1 --n_samples 50 --ddim_steps 100

case 7 (オマケ1)

雑な指定で耳を生やせるのか?

python optimizedSD/optimized_txt2img.py --prompt "女の子, 少女, solo, alone, rabbit ears, アニメ, ゲーム, イラスト, 高画質, ピクシブ, fanbox, 壁紙, wallpaper" --H 512 --W 512 --seed 1731723048 --n_iter 1 --n_samples 50 --ddim_steps 150

まとめ

試行錯誤の結果、比較的良好な結果が出たものを何件か載せました。
参考になれば幸いです。(あまり主流でない使い方の記事ではありますが…)

StableDiffusionを試した記録 その2 (img2img指定)

StableDiffusionの知見まとめ、その2です。

今回は、img2imgを試した結果を載せます。
Twitterに上げていたものをこちらに転記します。また、気が向いたら清書します。

実行条件

  • 画像

こちらの画像を使わせていただきました。
publicdomainq.net

  • コマンド
python optimizedSD/optimized_img2img.py --prompt "anime, reading book, full body, chair, illustration,game character,pixiv, pos1-XX" --init-img ./img/pos1.jpg --strength XX --n_iter 2 --n_samples 10 --H 768 --W 576 --seed 12
  • 指定する範囲

strengthの値を0.2~0.9まで0.1ずつ増加させる

結果

20枚中、特徴的なものを2枚ずつ記載します。
全体像が気になる方は、ぜひ手元でもお試しください。

0.2

外観にほぼ変化はありませんが、細かい部分が少しずつ変化します。
雰囲気を調整したい場合はこのあたりが良いかも

0.2

0.3

外観が変わり始めます。
見た目が同じまま細かい変化を加えたい場合はこのあたりが良いかも?

0.3

0.4

promptの影響が出始めます。
見た目が同じまま、全体の雰囲気を変えるならこのあたりが良さそうです。

0.4

0.5

promptの影響で、一部が絵になり始めます。
このあたりまでは見た目を維持できるので、ちょっと変えたい場合はこのあたりが良さそう。

0.5

0.6

このあたりから元絵が消え始めます。

0.6

0.7

全体の色味が多少残りますが、内容の変化がかなり大きくなっています。
恐らく、元絵は色の分布のみが参照されて、promptの影響を大きく受け始めています。

0.7

0.8

このあたりになると、元絵の雰囲気が残っているか怪しくなってきます。
また、なぜか上下で2人に分かれ始めました。
このあたりの数値は、ペイントで雑に書いた絵をソースにする用途向けの印象です。

0.8

0.9

ここまで設定すると、背景が白いこと・黄色気味なことだけを引き継いでいる気がします。
コメントしづらい…

0.9

比較テスト

promptと画像がどの程度影響しているかを検証してみます。

promptの内容変更

実行コマンド

python optimizedSD/optimized_img2img.py --prompt "pos1-08" --init-img ./img/pos1.jpg --strength 0.8 --n_iter 2 --n_samples 10 --H 768 --W 576 --seed 12

promptを"pos1-08"として、promptの効力を無くしてみます。
この結果、上記の結果と内容がかなり変わっているため0.8でのpromptの影響はかなり大きいようです。
また、白背景・ベージュ系は引き継いでいるため、色味だけは残るようです。

promptなし

txt2imgを使ったケース

実行コマンド

python optimizedSD/optimized_txt2img.py --prompt "anime, reading book, full body, chair, illustration,game character,pixiv"  --n_iter 2 --n_samples 10 --H 768 --W 576 --seed 12 --ddim_steps 50

画像を使わなかったケースも試してみました。
雰囲気が大幅に変わっているため、strength=0.9でも元画像の影響が多少出せるようです。
※ 1.0にすると元画像の効力が0になるらしい

imgなし

まとめ

img2imgのstrengthを変更してみた結果を載せました。
VRAMを非常に食うため一度に生成される枚数が限られますが、img2imgで画像を大量に生成すると楽しめるかもしれません。

StableDiffusion (v1.4)を試した記録 その1

約1週間前、StableDiffusion v1.4がリリースされたと話題になりました。
ローカル環境で動く点に魅力を感じて、1週間試した記録を載せておきます。

内容が他のサイトと被っていたり、数か月後には陳腐化しているかもしれませんが、2022年8月の記録として…(記事が雑なのはゆるして)

環境

GeForce RTX3060 (VRAM 12GB, LHR版)でStableDiffusionのFork版を使っています。
(original版はサンプルコードがOutOfMemoryになったため断念しました。)

Fork版はこちら GitHub - basujindal/stable-diffusion

コマンド関連はOriginal版と同一のはずですが、promptによる実行結果は異なるかもしれません。

StableDiffusionの雑な解説

こんな感じで処理されるようです。(詳しくないので違うかも?)

  1. seedからノイズ画像を生成し、元画像とする。
  2. (img2imgの場合) 元画像と、1.で生成したノイズ画像をstrength倍した画像を合成し、元画像として使う
  3. promptから、出力物の概念を生成する。
    (個人的には、薄いマスク画像みたいなイメージで認識しています。)
  4. 生成したノイズ画像に、3で生成した概念を焼き込む
  5. steps数の分、3~4を繰り返す
  6. 結果を出力

パラメータ関連

パラメータと、それに対する個人的な解釈です。 txt2img / img2imgどちらも同様に使えるようです。

コマンド名 詳細
--prompt 描画させたいイメージを指示する引数
--H 縦の解像度 ※ エラーになる解像度・ならない解像度がある。
手元の環境で使えたのはこのあたり→ 256,512,576,768,1024
--W 横の解像度 ※ こちらも一部の指定値ではエラーになる。
使える値は縦と同様
--seed 元にするノイズ画像の基準値。
ほかのパラメータを変更する際に比較できるので、設定した方が良い。
--n_iter 同じパラメータで何回実行するかを指定する。
ほぼ最初からやり直す機能なので、VRAM不足で欲しい枚数が生成できない場合に使うのが良い。
結果を見たいなら1、大量に生成するため放置するなら2以上を指定するのが良い?
--n_samples 一度に何枚生成するかを指定する。
VRAMギリギリまで使った方が生成効率はいいが、時間やOutOfMemoryのリスクが上がる。
--ddim_steps 一度の処理で画像補正をかける回数を指定する。
基本的には高い値の方が良好な結果になるが、かけすぎると崩壊することも。
50~250が無難かも?
--strength (img2imgのみ)0.0~1.0の範囲で元画像をどこまで残すかを指定する。
元画像の内容を残すなら0.4以下、大幅に変えたいなら0.7以上が良いかも?
--output 出力先フォルダを指定する。
デフォルトではcmdの直下にoutputフォルダを作り、その下にpromptの文字列が入力されたフォルダが生成されました。

(参考)設定した枚数と解像度

RTX3060のVRAM(12GB)で一度に生成できた件数の目安です。
(※ 30分~2時間近くかかります。ご利用は計画的に。)

  • txt2img
サイズ サンプル数
512x512 120
768x576 50
576x1024 20
  • img2img
サイズ サンプル数
512x512 50
768x576 20

(参考)その他引数について

  • seed

    Randで生成した32bitの数字を使っています。
    画像への影響が大きいため、いくつか試すのが良さそうです。

  • ddim_steps

    50前後で概形が生成されて、100付近で良くなる印象です。
    150~200は改善されるもの・されないもの・崩壊するものがありました。
    私は基本的には50か100、prompt/seedが良さそうなものは150に上げて試しています。

  • (img2imgの場合) strength

    今回は元画像の雰囲気を残したかったため0.4前後を主に使いました…が、あまり芳しい結果とはなりませんでした。

実行例

試した結果を数件記載します。
※ 基本的には成功率が数%なので、20枚作って1枚当たれば上出来くらいの心構えが必要です。

英文法がどう動くか分からないのでカンマ区切りで適当に入れています。

※ 他に検証したものはその2以降に載せます。

写実系 (夕暮れに離陸する飛行機)

python optimizedSD/optimized_txt2img.py --prompt "looking up at the plane at twilight, takeoff, aviation guide lines, dynamic angle, taken with SLR, fullsize, 4k resolution, photo, wallpaper, professional photo" --H 576 --W 768 --seed 1731723048 --n_iter 1 --n_samples 20 --ddim_steps 100

生成された20枚
成功寄りの例
失敗例

テクスチャ系 (苔むした石垣ブロック)

python optimizedSD/optimized_txt2img.py --prompt "stone wall, andesite, moss, repeating pattern, seamless, background" --H 512 --W 512 --seed 12 --n_iter 2 --n_samples 50 --ddim_steps 50

生成結果
成功寄り
失敗寄り

アニメ絵系 (初音ミク)
StableDiffusionは日本語指定できます、有効活用しましょう

python optimizedSD/optimized_txt2img.py --outdir "g7-15" --prompt "初音ミク, alone, 緑, ツインテール, 長髪, 白, ドレス, 少女, solo, アニメ, キャラクター, ゲーム, イラスト, ピクシブ, 音楽" --H 512 --W 512 --seed 1731723048 --n_iter 1 --n_samples 50 --ddim_steps 150

生成結果
成功寄り①
成功寄り②
※ 失敗側は閲覧注意になりがちなので省略

最後に

前提部分で分かったことを公開しました。参考になれば幸いです。

LocalStackが正式リリースされたので試してみた(その2 Serverless構築編)

Serverless構築が予想以上に長くなってしまったため、2章立てでお送りします。

前回の記事: LocalStackが正式リリースされたので試してみた(その1 セットアップ編) - cer12uのメモ

AWSコマンドでserverlessサービスを構築

前回作成したLocalStackを使って、Lambda + API Gatewayでローカルserverlessを立ててみます。

こちらのページが良くまとまっているので、今回はこれをパク参考にしました。

LocalStack入門(第5回) ブラウザからのアクセス方法~API GatewayからLambda関数起動

1. 実行用Lambdaの用意

1-1. 実行用のLambda関数の用意

Lambdaに上げるためのPythonデータをローカル環境に用意します。

pythonファイルを作成

$ vi lambda_environment.py
import json
def handler(event, context):
    return {
        'statusCode' : 200,
        'headers' : {
            'content-type' : 'text/html'
        },
        'body' : event['queryStringParameters']['a']
    }

アップロード用にZIP圧縮

$ zip lambda_environment.zip lambda_environment.py

1-2. awslocalで登録

作成したファイルをawslocalでLambdaに登録します。

awslocal lambda create-function --function-name lambda_function_handler --runtime python3.9 --handler lambda_environment.handler --memory-size 128 --zip-file fileb://lambda_environment.zip --role arn:aws:iam::0000000000:role/role-name

返却されるjsonはこちら
FunctionArnは別途使いますので、メモしておきます。

{
    "FunctionName": "lambda_function_handler",
    "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:lambda_function_handler",
    "Runtime": "python3.9",
    "Role": "arn:aws:iam::0000000000:role/role-name",
    "Handler": "lambda_environment.handler",
    "CodeSize": 335,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2022-07-26T12:17:10.355+0000",
    "CodeSha256": "9GKC1UVcfma25VtwtMKG96zIH6T+i7caq/phNYM8ebo=",
    "Version": "$LATEST",
    "VpcConfig": {},
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "2ca54669-bf54-40cc-9015-ad407ea4c530",
    "State": "Active",
    "LastUpdateStatus": "Successful",
    "PackageType": "Zip",
    "Architectures": [
        "x86_64"
    ]
}

1-3. API GatewayAPI環境を作成する

まずはエンドポイントを作成します。

ローカルなので名前は適当に「testapi」でいきます。

awslocal apigateway create-rest-api --name 'testapi'

結果はこちら。
この後、idの項目を使いますのでメモしましょう。

{
    "id": "jkhv86jblq",
    "name": "testapi",
    "createdDate": 1658837952.0,
    "version": "V1",
    "binaryMediaTypes": [],
    "apiKeySource": "HEADER",
    "endpointConfiguration": {
        "types": [
            "EDGE"
        ]
    },
    "tags": {},
    "disableExecuteApiEndpoint": false
}

1-4. API GatewayのルートリソースIDの取得

後で必要になるため、エンドポイントのルートリソースIDを調べておきます。

awslocal apigateway get-resources --rest-api-id <1-3のID>
# 例: awslocal apigateway get-resources --rest-api-id jkhv86jblq

結果のid項がリソースIDです。

{
    "items": [
        {
            "id": "g8hj8uj8xy",
            "path": "/"
        }
    ]
}

1-5. API Gatewayのリソース作成

API Gatewayの仕組みに合わせて、URLのパスを設定

awslocal apigateway create-resource --rest-api-id <1-3のID> --parent-id <1-4のID> --path-part env_test
# 例: awslocal apigateway create-resource --rest-api-id jkhv86jblq --parent-id g8hj8uj8xy --path-part env_test

応答はこちら。ここもidを記録しておきます。

{
    "id": "c73yv44rzx",
    "parentId": "g8hj8uj8xy",
    "pathPart": "env_test",
    "path": "/env_test"
}

1-6. API Gatewayの認証設定を行う

今回はテストなので、認証なしで設定します。

awslocal apigateway put-method --rest-api-id <1-3のID> --resource-id <1-5のID> --http-method GET --authorization-type "NONE"
# 例: awslocal apigateway put-method --rest-api-id jkhv86jblq --resource-id c73yv44rzx --http-method GET --authorization-type "NONE"

ここでは保存する項目はありません。

{
    "httpMethod": "GET",
    "authorizationType": "NONE",
    "apiKeyRequired": false,
    "methodResponses": {}
}

1-7. API GatewayとLambdaの接続

API Gatewayで指定するURIは、Lambdaのお作法に沿ってこの形式にする必要があるようです。

arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/<LambdaのFunctionARN名>/invocations
# 例: arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:000000000000:function:lambda_function_handler/invocations

それでは、LambdaをAPI Gatewayに接続します。
コマンドはこちら

awslocal apigateway put-integration --rest-api-id <1-3のID> --resource-id <1-5のID> --http-method GET --type AWS_PROXY --integration-http-method POST --uri <URI> --passthrough-behavior WHEN_NO_MATCH
# 例: awslocal apigateway put-integration --rest-api-id jkhv86jblq --resource-id c73yv44rzx --http-method GET --type AWS_PROXY --integration-http-method POST --uri arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:000000000000:function:lambda_function_handler/invocations --passthrough-behavior WHEN_NO_MATCH

レスポンスはこちら、特に記録する部分はありません。

{
    "type": "AWS_PROXY",
    "httpMethod": "POST",
    "uri": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:000000000000:function:lambda_function_handler/invocations",
    "requestParameters": {},
    "passthroughBehavior": "WHEN_NO_MATCH",
    "cacheNamespace": "c73yv44rzx",
    "cacheKeyParameters": []
}

1-8. APIの公開

最後に、ここまでで作成したAPI Gatewayを公開します。

awslocal apigateway create-deployment --rest-api-id <1-3のID> --stage-name test
# 例: awslocal apigateway create-deployment --rest-api-id jkhv86jblq --stage-name test

結果はこちら。

{
    "id": "ltq5d03f7y",
    "createdDate": 1658839374
}

1-9. 疎通確認

LocalStack v1.0はlocalhostの4566番ポートで起動しているため、実機でcurlを使って動作確認してみます。

curl http://localhost:4566/restapis/<1-3のID>/test/_user_request_/env_test?a=apigw_lambda_test

a=の後ろに設定した文字列が返却されていれば完成です。

別PCからのアクセス (nginx利用)

以前はdocker-composeの設定で対応できたようですが、v1.0からは手順が変わったようです。

リリース直後で設定方法が見当たらなかったため、今回はnginxでリバースプロキシを設定して対応します。(LocalStack自体で設定する方法をご存じでしたら教えてください…)

nginxの準備

$ sudo dnf install nginx
$ sudo firewall-cmd --add-service=http --permanent
$ sudo firewall-cmd --reload
$ sudo nano /etc/nginx/nginx.conf

localhostの4566番ポートを公開

~~略~~
        location / {
          proxy_pass http://127.0.0.1:4566/;
        }
~~略~~
$ sudo systemctl start nginx

設定後、LocalStackで作成したURLにアクセスして接続できれば完成です。

おわりに

これでServerless環境を構築できました、LocalStackはAWSに慣れている方の実験向けといった印象です。
今回はCLIコマンドで作成しましたが、CloudFormationの検証などに使えると良さそうに感じます。

LocalStackが正式リリースされたので試してみた(その1 セットアップ編)

2022/7/25にLocalStackが正式リリースされたと聞いて、早速導入してみました。

www.publickey1.jp

LocalStack 1.0の(個人的な)概要

  • AWSのサービスをローカルで構築できるサービス
  • LocalStackの全機能を使う場合は有料だが、一般的な機能は無料で使える
  • v1.0未満はdocker-composeを使い、v1.0以降は専用のコマンドで起動する
  • awsコマンド自体は互換性がある

この情報だけで面白そうです。

LocalStackの構築

※ 永続化設定は調査中です。この設定では終了すると内容が消えるので注意

環境

今回もVM上で設定していきます。

  • ホストOS: ESXi 7.0
  • ゲストOS: CentOS8 Stream
  • CPU: 4 vCPU (Ryzen 5600G)
  • RAM: 12GB
  • SSD: 120GB

インストール

公式の手順を参考にインストールします。

GitHub - localstack/localstack: 💻 A fully functional local AWS cloud stack. Develop and test your cloud & Serverless apps offline!

1. 必要なパッケージのインストール

公式の説明通り、まずはDockerを入れてサービスを起動します。 (podmanを使う場合は公式ドキュメントに言及があります。)

$ sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
$ sudo dnf install -y docker-ce docker-ce-cli containerd.io

$ sudo systemcrl start docker

LocalStackは非rootで起動する必要があるため、一般ユーザでdockerを実行できるよう設定します。
※ 設定を反映するため、設定後にログインし直しましょう。

sudo usermod -aG docker <ユーザ名>

LocalStackで使うPythonは、dnfで取得できるものを使います。

sudo dnf install python39

SELinuxは一旦permissiveにしておきます。(デバッグできる気がしないので)

$ sudo setenforce 0

これで起動に必要な環境は揃ったはずです。

2. LocalStackの起動

必要なパッケージはインストールできたので、LocalStack自体をインストールします。

入れるときはpip3でinstallするだけ。

pip3 install localstack

起動はlocalstackのコマンドを実行すればよし

$ localstack start -d

うまく起動できれば、こんな感じのメッセージが表示されてterminalに制御が戻ります。

     __                     _______ __             __
    / /   ____  _________ _/ / ___// /_____ ______/ /__
   / /   / __ \/ ___/ __ `/ /\__ \/ __/ __ `/ ___/ //_/
  / /___/ /_/ / /__/ /_/ / /___/ / /_/ /_/ / /__/ ,<
 /_____/\____/\___/\__,_/_//____/\__/\__,_/\___/_/|_|

 💻 LocalStack CLI 1.0.1

[07:41:48] starting LocalStack in Docker mode 🐳                          localstack.py:140
           preparing environment                                           bootstrap.py:667
           configuring container                                           bootstrap.py:675
           starting container                                              bootstrap.py:681
[07:41:49] detaching                                                       bootstrap.py:685

せっかくなので、公式の資料に従って、サービスのステータス表示もしてみましょう。

$ localstack status services

正常であれば、このように一覧が表示されます。

━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓
┃ Service                  ┃ Status      ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩
│ acm                      │ ✔ available │
│ apigateway               │ ✔ available │
│ cloudformation           │ ✔ available │
│ cloudwatch               │ ✔ available │
│ config                   │ ✔ available │
│ dynamodb                 │ ✔ available │
│ dynamodbstreams          │ ✔ available │
│ ec2                      │ ✔ available │

3. awslocalパッケージをインストールする

無料版ではGUIが使えないようなので、ここからはCLIで設定を行っていきます。

LocalStackを使う際はローカルに特化したaws cliである、awslocalが便利です。

インストールはこのコマンド。

$ pip3 install awscli
$ pip3 install awscli-local

動作確認にLambdaの一覧を表示してみます。

awslocal lambda list-functions

今は設定していないので、空の結果が返ってくるはずです。

{
    "Functions": []
}

おわりに

非常にシンプルな手順でAWSのローカル環境が立つことが分かりました。
立て直しが非常にラクなので、お試しに構築するのに非常に便利そうです。

※ Serverlessの構築を行ってみましたが、記事が長くなってしまったので別に分けて公開しました。 →

LocalStackが正式リリースされたので試してみた(その2 Serverless構築編) - cer12uのメモ

AWS Consoleのログイン通知をDiscordに飛ばす

AWSコンソールに入った記録を残したい!!
というわけで、Discord WebHookでログイン通知を飛ぶ仕組みを作成してみました。

はじめに

ログイン通知を実装する方法は、少なくとも2パターンあるようです。
今回は、コストが下げられる & 手順が簡単なので1.の方を設定します。

1. EventBridge経由

フロー: CloudTrail(標準機能) → EventBridge → Lambda → Discord

メリット

  • ほぼ即時でアラートが飛ぶ
  • 比較的低コストで済む

デメリット

  • 各リージョンでEventBridge設定が必要
    ※ イベントバスやCloudFormationを使えば、ある程度集約できるらしい

2. CloudWatch Logs経由

フロー: CloudTrail(証跡追加) → CloudWatch Logs → サブスクリプションフィルタ → Lambda → Discord

メリット

  • 1テナントの設定で、全リージョンのアラートが取れる
    (Virginiaを選択すれば、全リージョンの証跡が取れる)

デメリット

  • 証跡(=操作)に応じてコストが嵩む(軽く使って$1/月?)
  • アラートに遅延がある(数分程度、最大15分?)
  • ログの処理が少し複雑(BASE64デコード → gzip解凍 → JSON Parse)

手順

今回はこの手順で設定します。

  1. CloudTrailの証跡を確認する
  2. DiscordのWebHook URLを取得する
  3. Discordに送信するLambdaを作成する
  4. EventBridgeの作成

1. CloudTrailの証跡を確認する

CloudTrailのログイン証跡があることを確認する

そもそもログが出ていないとかなりハマります。
念のためログイン証跡が記録されていることを確認しましょう。

CloudTrail > イベント履歴 > 「ConsoleLogin」を探す

※ 表示されない場合は別のリージョンを選択するなどして「ConsoleLogin」を探しましょう。
絞り込みはルックアップ属性に「イベント名」を選択→「ConsoleLogin」で検索。

2. DiscordのWebHook URLを取得する

※ ご存じの方・Slack等を使う方は3.に進んでください。

AWSの設定を行う前に、送信先の設定を行います。
Botは難しそうだったので、今回はWebHookを設定していきます。

2022/5時点では、この手順でURLを取得できます。
Discordの通知先サーバー > サーバー設定 > 連携サービス > WebHook > 「新しいウェブフック」

この「ウェブフックURLをコピー」でWebHook URLが取得できます。どこかに保存しておきましょう。

3. Discordに送信するLambdaを作成する

ここからが本題。
まずはEventBridgeで発行される値を元に、Pythonで処理を作ります。

Lamdaが受けるデータ形式はこんな感じです。

データ形式

{
  "version": "0",
  "id": "76f2f25f-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "detail-type": "AWS API Call via CloudTrail",
  "source": "aws.cloudtrail",
  "account": "123456789012",
  "time": "2022-05-31T14:00:00Z",
  "region": "ap-northeast-1",
  "resources": [],
  "detail": {
    "eventVersion": "1.08",
    "userIdentity": {
      "type": "IAMUser",
      "principalId": "PRINCIPALID1234567890",
      "arn": "arn:aws:iam::123456789012:user/testuser",
      "accountId": "123456789012",
      "accessKeyId": "PRINCIPALID1234567890",
      "userName": "testuser",
      "sessionContext": {
        "sessionIssuer": {},
        "webIdFederationData": {},
        "attributes": {
          "creationDate": "2022-05-31T14:00:00Z",
          "mfaAuthenticated": "true"
        }
      }
    },
    "eventTime": "2022-05-31T14:00:00Z",
    "eventSource": "cloudtrail.amazonaws.com",
    "eventName": "LookupEvents",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "AWS Internal",
    "userAgent": "AWS Internal",
    "requestParameters": {
      "lookupAttributes": [
        {
          "attributeKey": "ReadOnly",
          "attributeValue": "false"
        }
      ],
      "maxResults": 50
    },
    "responseElements": null,
    "requestID": "b1a9fc9c-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "eventID": "15e67291-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "readOnly": true,
    "eventType": "AwsApiCall",
    "managementEvent": true,
    "recipientAccountId": "123456789012",
    "eventCategory": "Management",
    "sessionCredentialFromConsole": "true"
  }
}

コードのテスト用に、これをLambdaのTestにコピペして入れておくと良いです。

Lambdaには、こんな感じのコードをデプロイしました。(雑)
※ rootユーザの表記がバグりますが、知らないログイン通知が来た時点で事件なので保留

コード lambda_function.py

from dateutil import parser
from dateutil.tz import gettz
import urllib.request
import logging
import json

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    url = "取得したDiscordのWebHook URL"

    headers = {
        "Content-type" : "application/json",
        "User-Agent": "AWS/Lambda"
    }
    payload = {
        "username":"discordbot",
        "content": "ログインがありました。",
        "embeds": [
            {
                "fields":[
                    {
                        "name": "ユーザ名",
                        "value": event['detail']['userIdentity']['userName'],
                    },
                    {
                        "name": "リージョン",
                        "value": event['detail']['awsRegion'],
                        "inline": True
                    },
                    {
                        "name": "ログイン日時",
                        "value": parser.parse(event['detail']['eventTime']).astimezone(gettz('Asia/Tokyo')).strftime("%Y/%m/%d %H:%M:%S"),
                        "inline": True

                    },
            ]}
        ]
    }

    req = urllib.request.Request(
        url=url, 
        headers=headers, 
        data=json.dumps(payload).encode('utf-8')
        )
        
    with urllib.request.urlopen(req) as res:
        logger.info(res.read().decode("utf-8"))

コードのデプロイ後、上に記載したデータでテストで通知が届くことを確認しておくと確実です。

4. EventBridgeの作成

最後に、EventBridgeでCloudTrailとLambdaを繋げましょう。

EventBridgeはこのように設定します。

EventBridge > ルール > ルールを作成

サンプルイベント: AWS Console Sign In via CloudTrail
※ 設定しなくても動きますが、内容テストが使えるため設定を推奨。

イベントパターン:

{
  "detail-type": ["AWS Console Sign In via CloudTrail"],
  "detail": {
    "eventSource": ["signin.amazonaws.com"],
    "eventName": ["ConsoleLogin"]
  }
}

ターゲット指定
先ほど作成したLambdaを指定しましょう。

動作確認

設定が完了したらログアウトして、設定を行ったリージョンでログインし直しましょう。
Discordにログが出力されるはずです。

おわりに

勉強のため手動で設定しましたが、全リージョンの設定であればCloudFormationなどにした方が良さそうです。
送信コードの共通化はEventBridge イベントバスを設定した方がよさそう。
改良の余地があるので、そのあたりが今後の課題。